diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 16a7c37c3d..94fb1be737 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,10 +60,11 @@ jobs: - name: Get Packages run: flutter pub get - - name: Configure Dev env - run: echo DEV_HOST=$DEV_HOST >> .env + - name: Configure Dev config.json + run: | + printf "%s" "$CONFIG" > config.json env: - DEV_HOST: ${{ vars.DEV_HOST }} + CONFIG: ${{ vars.DEV_CONFIG }} - name: Add Firebase configuration for Dev run: | @@ -72,7 +73,7 @@ jobs: GOOGLE_SERVICES_DEV_JSON_BASE64: ${{ secrets.GOOGLE_SERVICES_DEV_JSON_BASE64 }} - name: Build Dev 🔧 - run: flutter build ${{ matrix.target }} + run: flutter build ${{ matrix.target }} --dart-define-from-file=config.json - name: Upload Artifacts uses: actions/upload-artifact@v4 diff --git a/.github/workflows/release-mobile.yml b/.github/workflows/release-mobile.yml index a490d11bb1..7623f778aa 100644 --- a/.github/workflows/release-mobile.yml +++ b/.github/workflows/release-mobile.yml @@ -71,27 +71,19 @@ jobs: - name: Get Packages run: flutter pub get - - name: Configure Alpha env + - name: Configure Alpha config.json if: needs.extract-version.outputs.isAlpha == 'true' run: | - echo ALPHA_HOST=$ALPHA_HOST >> .env - echo PLAUSIBLE_HOST=$PLAUSIBLE_HOST >> .env - echo PLAUSIBLE_DOMAIN=$PLAUSIBLE_DOMAIN >> .env + printf "%s" "$CONFIG" > config.json env: - ALPHA_HOST: ${{ vars.ALPHA_HOST }} - PLAUSIBLE_HOST: ${{ secrets.PLAUSIBLE_ALPHA_HOST }} - PLAUSIBLE_DOMAIN: ${{ secrets.PLAUSIBLE_ALPHA_DOMAIN }} - - - name: Configure production env + CONFIG: ${{ vars.ALPHA_CONFIG }} + + - name: Configure Alpha config.json if: needs.extract-version.outputs.isAlpha == 'false' run: | - echo PROD_HOST=$PROD_HOST >> .env - echo PLAUSIBLE_HOST=$PLAUSIBLE_HOST >> .env - echo PLAUSIBLE_DOMAIN=$PLAUSIBLE_DOMAIN >> .env + printf "%s" "$CONFIG" > config.json env: - PROD_HOST: ${{ vars.PROD_HOST }} - PLAUSIBLE_HOST: ${{ secrets.PLAUSIBLE_HOST }} - PLAUSIBLE_DOMAIN: ${{ secrets.PLAUSIBLE_DOMAIN }} + CONFIG: ${{ vars.PROD_CONFIG }} - name: Add Firebase configuration for Alpha if: needs.extract-version.outputs.isAlpha == 'true' @@ -127,11 +119,11 @@ jobs: - name: Build Alpha 🔧 if: needs.extract-version.outputs.isAlpha == 'true' - run: flutter build ${{ matrix.target }} --flavor=alpha --release --build-name ${{ needs.extract-version.outputs.versionName }} --build-number ${{ needs.extract-version.outputs.versionCode }} + run: flutter build ${{ matrix.target }} --flavor=alpha --dart-define-from-file=config.json --release --build-name ${{ needs.extract-version.outputs.versionName }} --build-number ${{ needs.extract-version.outputs.versionCode }} - name: Build production 🔧 if: needs.extract-version.outputs.isAlpha == 'false' - run: flutter build ${{ matrix.target }} --flavor=prod --release --build-name ${{ needs.extract-version.outputs.versionName }} --build-number ${{ needs.extract-version.outputs.versionCode }} + run: flutter build ${{ matrix.target }} --flavor=prod --dart-define-from-file=config.json --release --build-name ${{ needs.extract-version.outputs.versionName }} --build-number ${{ needs.extract-version.outputs.versionCode }} - name: Upload Artifacts uses: actions/upload-artifact@v4 diff --git a/.github/workflows/release-web.yml b/.github/workflows/release-web.yml index 9259e18547..d3d3fdd774 100644 --- a/.github/workflows/release-web.yml +++ b/.github/workflows/release-web.yml @@ -54,31 +54,65 @@ jobs: - name: Get Packages run: flutter pub get + - name: Configure web files + run: | + sed -i 's|{{ APP_NAME }}|'"$APP_NAME"'|g' web/index.html + sed -i 's|{{ SCHOOL_NAME }}|'"$SCHOOL_NAME"'|g' web/index.html + sed -i 's|{{ ENTITY_NAME }}|'"$ENTITY_NAME"'|g' web/index.html + env: + APP_NAME: ${{ vars.APP_NAME }} + SCHOOL_NAME: ${{ vars.SCHOOL_NAME }} + ENTITY_NAME: ${{ vars.ENTITY_NAME }} + + - name: Move assets + run: | + cp -r -f $LOGIN_ASSET_PATH assets/images/login.webp + cp -r -f $BACK_ASSET_PATH assets/web/back.webp + cp -r -f $LOGO_ASSET_PATH assets/images/logo_prod.png + cp -r -f $ICON_ASSET_PATH assets/images/icon_prod.png + cp -r -f $ICON_ANDROID_PATH/* android/app/src/prod/ + cp -r -f $ICON_IOS_PATH/* ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/ + cp -r -f $ICON_WEB_PATH/* web/Prod/ + env: + LOGIN_ASSET_PATH: ${{ vars.LOGIN_ASSET_PATH }} + BACK_ASSET_PATH: ${{ vars.BACK_ASSET_PATH }} + LOGO_ASSET_PATH: ${{ vars.LOGO_ASSET_PATH }} + ICON_ASSET_PATH: ${{ vars.ICON_ASSET_PATH }} + ICON_ANDROID_PATH: ${{ vars.ICON_ANDROID_PATH }} + ICON_IOS_PATH: ${{ vars.ICON_IOS_PATH }} + ICON_WEB_PATH: ${{ vars.ICON_WEB_PATH }} + - name: Configure env run: | echo PROD_HOST=$PROD_HOST >> .env echo ALPHA_HOST=$ALPHA_HOST >> .env + echo PAYMENT_NAME=$PAYMENT_NAME >> .env + echo SCHOOL_NAME=$SCHOOL_NAME >> .env + echo APP_NAME=$APP_NAME >> .env + echo APP_ID_PREFIX=$APP_ID_PREFIX >> .env + echo TITAN_URL=$TITAN_URL >> .env env: PROD_HOST: ${{ vars.PROD_HOST }} ALPHA_HOST: ${{ vars.ALPHA_HOST }} + PAYMENT_NAME: ${{ vars.PAYMENT_NAME }} + SCHOOL_NAME: ${{ vars.SCHOOL_NAME }} + APP_NAME: ${{ vars.APP_NAME }} + APP_ID_PREFIX: ${{ vars.APP_ID_PREFIX }} + TITAN_URL: ${{ vars.TITAN_URL }} - name: Configure Alpha env if: needs.extract-version.outputs.isAlpha == 'true' run: | - echo PLAUSIBLE_HOST=$PLAUSIBLE_ALPHA_HOST >> .env - echo PLAUSIBLE_DOMAIN=$PLAUSIBLE_ALPHA_DOMAIN >> .env + printf "%s" "$CONFIG" > config.json env: - PLAUSIBLE_ALPHA_HOST: ${{ secrets.PLAUSIBLE_ALPHA_HOST }} - PLAUSIBLE_ALPHA_DOMAIN: ${{ secrets.PLAUSIBLE_ALPHA_DOMAIN }} + CONFIG: ${{ vars.ALPHA_CONFIG }} - - name: Configure Prod env + - name: Configure Prod config.json if: needs.extract-version.outputs.isAlpha == 'false' run: | - echo PLAUSIBLE_HOST=$PLAUSIBLE_PROD_HOST >> .env - echo PLAUSIBLE_DOMAIN=$PLAUSIBLE_PROD_DOMAIN >> .env + printf "%s" "$CONFIG" > config.json env: - PLAUSIBLE_PROD_HOST: ${{ secrets.PLAUSIBLE_HOST }} - PLAUSIBLE_PROD_DOMAIN: ${{ secrets.PLAUSIBLE_DOMAIN }} + CONFIG: ${{ vars.PROD_CONFIG }} - name: Set Alpha icons if: needs.extract-version.outputs.isAlpha == 'true' @@ -90,15 +124,20 @@ jobs: if: needs.extract-version.outputs.isAlpha == 'false' run: | cp -f web/Prod/favicon.png web/favicon.png + mkdir -p web/icons cp -f web/Prod/icons/* web/icons/ - - name: Build Alpha 🔧 - if: needs.extract-version.outputs.isAlpha == 'true' - run: flutter build web --release --dart-define=flavor=alpha - - - name: Build Prod 🔧 - if: needs.extract-version.outputs.isAlpha == 'false' - run: flutter build web --release --dart-define=flavor=prod + - name: Replace variables in index.html and manifest.json + run: | + APP_NAME=$(cat config.json | jq -r '.APP_NAME') + SCHOOL_NAME=$(cat config.json | jq -r '.SCHOOL_NAME') + sed -i "s/{{APP_NAME}}/$APP_NAME/g" web/index.html + sed -i "s/{{SCHOOL_NAME}}/$SCHOOL_NAME/g" web/index.html + sed -i "s/{{APP_NAME}}/$APP_NAME/g" web/manifest.json + sed -i "s/{{SCHOOL_NAME}}/$SCHOOL_NAME/g" web/manifest.json + + - name: Build 🔧 + run: flutter build web --release --dart-define-from-file=config.json - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/.gitignore b/.gitignore index 3a2448718e..beb797db5c 100644 --- a/.gitignore +++ b/.gitignore @@ -44,10 +44,13 @@ app.*.map.json /android/app/profile /android/app/release -# env -.env +# Config +config/* +!config/config-template.json # Firebase +google-services.json +GoogleService-Info.plist android/app/src/alpha/google-services.json android/app/src/dev/google-services.json android/app/src/prod/google-services.json @@ -63,3 +66,27 @@ coverage/ # Platforms not supported by this project /windows/ + +# Missing translations +missing.txt + +# Fastlane +android/fastlane/metadata +android/fastlane/report.xml +ios/fastlane/report.xml +android/fastlane-service-account.json +ios/app-store-connect-api.p8 + +# Generated xcode configuration from dart defines +Dart-Defines.xcconfig + +# Config +config/ +!config/config-template.json + +# Folder to store various keys and secrets +keys + +MainActivity.kt + +.env \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index f15439e506..6cab8f2e9e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,8 +11,7 @@ "args": [ "--flavor", "dev", - "--dart-define", - "flavor=dev", + "--dart-define-from-file=config/config-dev.json", "--web-port", "3000" ] @@ -25,8 +24,7 @@ "args": [ "--flavor", "dev", - "--dart-define", - "flavor=dev", + "--dart-define-from-file=config/config-dev.json", "--web-port", "3000" ] @@ -38,8 +36,7 @@ "args": [ "--flavor", "alpha", - "--dart-define", - "flavor=alpha", + "--dart-define-from-file=config/config-alpha.json", "--web-port", "3000" ] @@ -51,8 +48,7 @@ "args": [ "--flavor", "prod", - "--dart-define", - "flavor=prod", + "--dart-define-from-file=config/config-prod.json", "--web-port", "3000" ] diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000000..a5d4c5a9e1 --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +gem "abbrev" +gem "fastlane" \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000000..f07f6be8b1 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,231 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.7) + base64 + nkf + rexml + abbrev (0.1.2) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.4.0) + aws-partitions (1.1147.0) + aws-sdk-core (3.229.0) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal + jmespath (~> 1, >= 1.6.1) + logger + aws-sdk-kms (1.110.0) + aws-sdk-core (~> 3, >= 3.228.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.196.1) + aws-sdk-core (~> 3, >= 3.228.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.12.1) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.3.0) + bigdecimal (3.2.2) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + declarative (0.0.20) + digest-crc (0.7.0) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20240107) + dotenv (2.8.1) + emoji_regex (3.2.3) + excon (0.112.0) + faraday (1.10.4) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.1) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.1.1) + multipart-post (~> 2.0) + faraday-net_http (1.0.2) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.1) + faraday (~> 1.0) + fastimage (2.4.0) + fastlane (2.228.0) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored (~> 1.2) + commander (~> 4.6) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + naturally (~> 2.2) + optparse (>= 0.1.1, < 1.0.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.5) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 3) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.4.1) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.54.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.3) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.8.0) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.5.0) + google-cloud-storage (1.47.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.31.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.8) + domain_name (~> 0.5) + httpclient (2.9.0) + mutex_m + jmespath (1.6.2) + json (2.13.2) + jwt (2.10.2) + base64 + logger (1.7.0) + mini_magick (4.13.2) + mini_mime (1.1.5) + multi_json (1.17.0) + multipart-post (2.4.1) + mutex_m (0.3.0) + nanaimo (0.4.0) + naturally (2.3.0) + nkf (0.2.0) + optparse (0.6.0) + os (1.1.4) + plist (3.7.2) + public_suffix (6.0.2) + rake (13.3.0) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.4.1) + rouge (3.28.0) + ruby2_keywords (0.0.5) + rubyzip (2.4.1) + security (0.1.5) + signet (0.20.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + sysrandom (1.0.5) + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unicode-display_width (2.6.0) + word_wrap (1.0.0) + xcodeproj (1.27.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) + xcpretty (0.4.1) + rouge (~> 3.28.0) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + arm64-darwin-23 + ruby + +DEPENDENCIES + abbrev + fastlane + +BUNDLED WITH + 2.7.1 diff --git a/README.md b/README.md index aba64eca23..ba1c3887be 100644 --- a/README.md +++ b/README.md @@ -170,3 +170,17 @@ flutter pub run flutter_launcher_icons [Guided upgrade using Android Studio](https://docs.flutter.dev/release/breaking-changes/android-java-gradle-migration-guide#solution-1-guided-fix-using-android-studio) [Java and Gradle compatibility](https://docs.gradle.org/current/userguide/compatibility.html) + +# Configuring fastlane + +Google service account + +``` +android/fastlane-service-account.json +``` + +Apple + +``` +ios/app-store-connect-api.p8 +``` diff --git a/android/Gemfile b/android/Gemfile new file mode 100644 index 0000000000..fdf182dfa5 --- /dev/null +++ b/android/Gemfile @@ -0,0 +1,5 @@ +source "https://rubygems.org" + +gem "abbrev" +gem "fastlane" +gem "ostruct" diff --git a/android/Gemfile.lock b/android/Gemfile.lock new file mode 100644 index 0000000000..20e10f585d --- /dev/null +++ b/android/Gemfile.lock @@ -0,0 +1,233 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.8) + abbrev (0.1.2) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.4.0) + aws-partitions (1.1187.0) + aws-sdk-core (3.239.1) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal + jmespath (~> 1, >= 1.6.1) + logger + aws-sdk-kms (1.118.0) + aws-sdk-core (~> 3, >= 3.239.1) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.205.0) + aws-sdk-core (~> 3, >= 3.234.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.12.1) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.3.0) + bigdecimal (3.3.1) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + csv (3.3.5) + declarative (0.0.20) + digest-crc (0.7.0) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20240107) + dotenv (2.8.1) + emoji_regex (3.2.3) + excon (0.112.0) + faraday (1.10.4) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.8) + faraday (>= 0.8.0) + http-cookie (>= 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.1) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.1.1) + multipart-post (~> 2.0) + faraday-net_http (1.0.2) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.1) + faraday (~> 1.0) + fastimage (2.4.0) + fastlane (2.229.0) + CFPropertyList (>= 2.3, < 4.0.0) + abbrev (~> 0.1.2) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored (~> 1.2) + commander (~> 4.6) + csv (~> 3.3) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + mutex_m (~> 0.3.0) + naturally (~> 2.2) + optparse (>= 0.1.1, < 1.0.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.5) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 3) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.4.1) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.54.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.3) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.8.0) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.5.0) + google-cloud-storage (1.47.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.31.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.8) + domain_name (~> 0.5) + httpclient (2.9.0) + mutex_m + jmespath (1.6.2) + json (2.16.0) + jwt (2.10.2) + base64 + logger (1.7.0) + mini_magick (4.13.2) + mini_mime (1.1.5) + multi_json (1.17.0) + multipart-post (2.4.1) + mutex_m (0.3.0) + nanaimo (0.4.0) + naturally (2.3.0) + optparse (0.8.0) + os (1.1.4) + ostruct (0.6.1) + plist (3.7.2) + public_suffix (6.0.2) + rake (13.3.1) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.4.4) + rouge (3.28.0) + ruby2_keywords (0.0.5) + rubyzip (2.4.1) + security (0.1.5) + signet (0.21.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 4.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + sysrandom (1.0.5) + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unicode-display_width (2.6.0) + word_wrap (1.0.0) + xcodeproj (1.27.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) + xcpretty (0.4.1) + rouge (~> 3.28.0) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + arm64-darwin-23 + ruby + +DEPENDENCIES + abbrev + fastlane + ostruct + +BUNDLED WITH + 2.7.1 diff --git a/android/app/MainActivity.template.kt b/android/app/MainActivity.template.kt new file mode 100644 index 0000000000..65ef98d6fb --- /dev/null +++ b/android/app/MainActivity.template.kt @@ -0,0 +1,12 @@ +package __PACKAGE__ + +import androidx.annotation.NonNull +import io.flutter.embedding.android.FlutterFragmentActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugins.GeneratedPluginRegistrant + +class MainActivity: FlutterFragmentActivity() { + override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { + GeneratedPluginRegistrant.registerWith(flutterEngine) + } +} \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 344bbcf3bb..84554a9f1a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -27,15 +27,56 @@ if (flutterVersionName == null) { // App bundle signing // https://docs.flutter.dev/deployment/android def keystoreProperties = new Properties() - def keystorePropertiesFile = rootProject.file('key.properties') - if (keystorePropertiesFile.exists()) { - keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) - } +def keystorePropertiesFile = rootProject.file('key.properties') +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} + +// Extract variables from Dart defines +// Based on https://medium.com/@mahdi.yami3235/mastering-native-configurations-in-flutter-the-power-of-dart-define-10f2b89922dc +def dartEnvironmentVariables = []; +if (project.hasProperty('dart-defines')) { + dartEnvironmentVariables = project.property('dart-defines') + .split(',') + .collectEntries { entry -> + def pair = new String(entry.decodeBase64(), 'UTF-8').split('=') + [(pair.first()): pair.last()] + } +} + +def appIdPrefix = dartEnvironmentVariables.APP_ID_PREFIX +if (appIdPrefix == null) { + throw new GradleException("Dart define APP_ID_PREFIX not found.") +} + +def appName = dartEnvironmentVariables.APP_NAME +if (appName == null) { + throw new GradleException("Dart define APP_NAME not found.") +} + +def generateMainActivityTask = tasks.register("generateMainActivity") { + doLast { + def packageName = "${appIdPrefix}.titan" + + def templateFile = file("$projectDir/MainActivity.template.kt") + def packageDir = packageName.replace('.', '/') + def outputDir = file("$projectDir/src/main/kotlin/$packageDir") + outputDir.mkdirs() + def outputFile = new File(outputDir, "MainActivity.kt") + + def content = templateFile.text + .replace("__PACKAGE__", packageName) + + outputFile.text = content + } +} + +preBuild.dependsOn(generateMainActivityTask) android { compileSdkVersion flutter.compileSdkVersion ndkVersion "27.0.12077973" - namespace "fr.myecl.titan" + namespace "${appIdPrefix}.titan" compileOptions { coreLibraryDesugaringEnabled true @@ -44,7 +85,7 @@ android { } kotlinOptions { - jvmTarget = '17' + jvmTarget = JavaVersion.VERSION_17.toString() } sourceSets { @@ -52,7 +93,7 @@ android { } defaultConfig { - applicationId "fr.myecl.titan" + applicationId "${appIdPrefix}.titan" minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() @@ -78,23 +119,23 @@ android { productFlavors { prod { dimension "default" - resValue "string", "app_name", "MyECL" - resValue "string", "flavor_url_scheme", "titan" - manifestPlaceholders = ['appAuthRedirectScheme': 'fr.myecl.titan'] + resValue "string", "app_name", "${appName}" + resValue "string", "flavor_url_scheme", "${appIdPrefix}.titan" + manifestPlaceholders = ['appAuthRedirectScheme': "${appIdPrefix}.titan"] } alpha { dimension "default" - resValue "string", "app_name", "MyECL Alpha" - resValue "string", "flavor_url_scheme", "titan.alpha" + resValue "string", "app_name", "${appName} Alpha" + resValue "string", "flavor_url_scheme", "${appIdPrefix}.titan.alpha" applicationIdSuffix ".alpha" - manifestPlaceholders = ['appAuthRedirectScheme': 'fr.myecl.titan.alpha'] + manifestPlaceholders = ['appAuthRedirectScheme': "${appIdPrefix}.titan.alpha"] } dev { dimension "default" - resValue "string", "app_name", "MyECL Dev" - resValue "string", "flavor_url_scheme", "titan.dev" + resValue "string", "app_name", "${appName} Dev" + resValue "string", "flavor_url_scheme", "${appIdPrefix}.titan.dev" applicationIdSuffix ".dev" - manifestPlaceholders = ['appAuthRedirectScheme': 'fr.myecl.titan.dev'] + manifestPlaceholders = ['appAuthRedirectScheme': "${appIdPrefix}.titan.dev"] } } } @@ -110,6 +151,6 @@ dependencies { android { defaultConfig { - minSdkVersion 23 + minSdkVersion flutter.minSdkVersion } } diff --git a/android/app/src/alpha/res/mipmap-hdpi/ic_launcher.png b/android/app/src/alpha/res/mipmap-hdpi/ic_launcher.png index 5975719cc4..63fb07e2aa 100644 Binary files a/android/app/src/alpha/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/alpha/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/alpha/res/mipmap-mdpi/ic_launcher.png b/android/app/src/alpha/res/mipmap-mdpi/ic_launcher.png index 12a4189321..e2afa718b5 100644 Binary files a/android/app/src/alpha/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/alpha/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/alpha/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/alpha/res/mipmap-xhdpi/ic_launcher.png index 73c623abfc..0f426a68b3 100644 Binary files a/android/app/src/alpha/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/alpha/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/alpha/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/alpha/res/mipmap-xxhdpi/ic_launcher.png index 447a4c4130..eeaa8c37cb 100644 Binary files a/android/app/src/alpha/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/alpha/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/alpha/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/alpha/res/mipmap-xxxhdpi/ic_launcher.png index d7b0e582ad..81daad90ce 100644 Binary files a/android/app/src/alpha/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/alpha/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 32465af703..ff47992dcc 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -19,8 +19,8 @@ - - + + diff --git a/android/app/src/dev/res/mipmap-hdpi/ic_launcher.png b/android/app/src/dev/res/mipmap-hdpi/ic_launcher.png index 82f3ab495d..75307dacb7 100644 Binary files a/android/app/src/dev/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/dev/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/dev/res/mipmap-mdpi/ic_launcher.png b/android/app/src/dev/res/mipmap-mdpi/ic_launcher.png index 596af2305d..9804166fd4 100644 Binary files a/android/app/src/dev/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/dev/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/dev/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/dev/res/mipmap-xhdpi/ic_launcher.png index 05a5008b40..ae5df8c613 100644 Binary files a/android/app/src/dev/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/dev/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher.png index c38790da64..f8df98fbed 100644 Binary files a/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher.png index c7c8088abc..466a0d1d20 100644 Binary files a/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/emlyon/.gitkeep b/android/app/src/emlyon/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/android/app/src/emlyon/res/mipmap-hdpi/ic_launcher.png b/android/app/src/emlyon/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..5ebb4d61f2 Binary files /dev/null and b/android/app/src/emlyon/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/emlyon/res/mipmap-mdpi/ic_launcher.png b/android/app/src/emlyon/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000..3cdca2df2b Binary files /dev/null and b/android/app/src/emlyon/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/emlyon/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/emlyon/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..24f6299210 Binary files /dev/null and b/android/app/src/emlyon/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/emlyon/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/emlyon/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..e0effec346 Binary files /dev/null and b/android/app/src/emlyon/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/emlyon/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/emlyon/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..23cdb9551c Binary files /dev/null and b/android/app/src/emlyon/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 05a8c3c7df..01775634a8 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -31,26 +31,22 @@ - - + + - - - + android:name="com.yalantis.ucrop.UCropActivity" + android:screenOrientation="portrait" + android:theme="@style/Theme.AppCompat.Light.NoActionBar"/> + + diff --git a/android/app/src/main/kotlin/com/example/myecl/MainActivity.kt b/android/app/src/main/kotlin/fr/proximapp/myemapp/titan/MainActivity.kt similarity index 91% rename from android/app/src/main/kotlin/com/example/myecl/MainActivity.kt rename to android/app/src/main/kotlin/fr/proximapp/myemapp/titan/MainActivity.kt index 072c7de81f..76bd46e7e0 100644 --- a/android/app/src/main/kotlin/com/example/myecl/MainActivity.kt +++ b/android/app/src/main/kotlin/fr/proximapp/myemapp/titan/MainActivity.kt @@ -1,4 +1,4 @@ -package fr.myecl.titan +package fr.proximapp.myemapp.titan import androidx.annotation.NonNull import io.flutter.embedding.android.FlutterFragmentActivity diff --git a/android/app/src/prod/res/mipmap-hdpi/ic_launcher.png b/android/app/src/prod/res/mipmap-hdpi/ic_launcher.png index 197ffde8b0..5ebb4d61f2 100644 Binary files a/android/app/src/prod/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/prod/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/prod/res/mipmap-mdpi/ic_launcher.png b/android/app/src/prod/res/mipmap-mdpi/ic_launcher.png index 203b7356c3..3cdca2df2b 100644 Binary files a/android/app/src/prod/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/prod/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/prod/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/prod/res/mipmap-xhdpi/ic_launcher.png index 4393a1f940..24f6299210 100644 Binary files a/android/app/src/prod/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/prod/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/prod/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/prod/res/mipmap-xxhdpi/ic_launcher.png index 32b36e3ae1..e0effec346 100644 Binary files a/android/app/src/prod/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/prod/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/prod/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/prod/res/mipmap-xxxhdpi/ic_launcher.png index ed16a5f342..23cdb9551c 100644 Binary files a/android/app/src/prod/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/prod/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/fastlane/Appfile b/android/fastlane/Appfile new file mode 100644 index 0000000000..a7c3256fe5 --- /dev/null +++ b/android/fastlane/Appfile @@ -0,0 +1,17 @@ +json_key_file("./fastlane-service-account.json") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one +package_name("fr.proximapp.myemapp.titan") # e.g. com.krausefx.app + +for_platform :android do + for_lane :beta do |options| + flavor = options[:flavor] # prod, alpha, dev + app_id_prefix = options[:prefix] # fr.myecl + app_name = options[:name] # MyECL + + if flavor == "prod" + package_name("#{app_id_prefix}.titan") + else + package_name("#{app_id_prefix}.titan.#{flavor}") + end + + end +end \ No newline at end of file diff --git a/android/fastlane/Fastfile b/android/fastlane/Fastfile new file mode 100644 index 0000000000..e2541d4409 --- /dev/null +++ b/android/fastlane/Fastfile @@ -0,0 +1,33 @@ +# This file contains the fastlane.tools configuration +# You can find the documentation at https://docs.fastlane.tools +# +# For a list of all available actions, check out +# +# https://docs.fastlane.tools/actions +# +# For a list of all available plugins, check out +# +# https://docs.fastlane.tools/plugins/available-plugins +# + +# Uncomment the line if you want fastlane to automatically update itself +# update_fastlane + +default_platform(:android) + +platform :android do + + desc "Submit a new Beta Build to Google Play" + lane :beta do |options| + flavor = options[:flavor] # prod, alpha, dev + + sh "flutter build appbundle --dart-define-from-file=config/config-#{flavor}.json --release --flavor='#{flavor}'" + + upload_to_play_store( + track: 'internal', + aab: "../build/app/outputs/bundle/#{flavor}Release/app-#{flavor}-release.aab", + release_status: 'draft' + ) + + end +end diff --git a/android/fastlane/README.md b/android/fastlane/README.md new file mode 100644 index 0000000000..64de184714 --- /dev/null +++ b/android/fastlane/README.md @@ -0,0 +1,32 @@ +fastlane documentation +---- + +# Installation + +Make sure you have the latest version of the Xcode command line tools installed: + +```sh +xcode-select --install +``` + +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) + +# Available Actions + +## Android + +### android beta + +```sh +[bundle exec] fastlane android beta +``` + +Submit a new Beta Build to Google Play + +---- + +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 90ead1df47..996471e57c 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed Nov 09 22:02:36 CET 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/android/settings.gradle b/android/settings.gradle index 3c7f977036..453df9d7b5 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -19,8 +19,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version '8.10.0' apply false - id "org.jetbrains.kotlin.android" version "1.8.21" apply false + id "com.android.application" version '8.12.3' apply false + id "org.jetbrains.kotlin.android" version "2.2.21" apply false id "com.google.gms.google-services" version "4.3.15" apply false } diff --git a/assets/emlyon/back.webp b/assets/emlyon/back.webp new file mode 100644 index 0000000000..f5f2c9f43c Binary files /dev/null and b/assets/emlyon/back.webp differ diff --git a/assets/emlyon/icon.png b/assets/emlyon/icon.png new file mode 100644 index 0000000000..1f02eec990 Binary files /dev/null and b/assets/emlyon/icon.png differ diff --git a/assets/emlyon/login.webp b/assets/emlyon/login.webp new file mode 100644 index 0000000000..2db1227c14 Binary files /dev/null and b/assets/emlyon/login.webp differ diff --git a/assets/emlyon/logo.png b/assets/emlyon/logo.png new file mode 100644 index 0000000000..cbe8e9cb56 Binary files /dev/null and b/assets/emlyon/logo.png differ diff --git a/assets/images/icon_alpha.png b/assets/images/icon_alpha.png index bd61be0b04..1a07d8b570 100644 Binary files a/assets/images/icon_alpha.png and b/assets/images/icon_alpha.png differ diff --git a/assets/images/icon_prod.png b/assets/images/icon_prod.png index 0809659300..1f02eec990 100644 Binary files a/assets/images/icon_prod.png and b/assets/images/icon_prod.png differ diff --git a/assets/images/login.svg b/assets/images/login.svg deleted file mode 100644 index 68fae2ebd5..0000000000 --- a/assets/images/login.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/images/login.webp b/assets/images/login.webp new file mode 100644 index 0000000000..37035d60a2 Binary files /dev/null and b/assets/images/login.webp differ diff --git a/assets/images/logo_alpha.png b/assets/images/logo_alpha.png index c715b0db13..f67c3318f7 100644 Binary files a/assets/images/logo_alpha.png and b/assets/images/logo_alpha.png differ diff --git a/assets/images/logo_prod.png b/assets/images/logo_prod.png index b39f1fefc4..cbe8e9cb56 100644 Binary files a/assets/images/logo_prod.png and b/assets/images/logo_prod.png differ diff --git a/assets/images/proximapp.png b/assets/images/proximapp.png new file mode 100644 index 0000000000..5410eefdd7 Binary files /dev/null and b/assets/images/proximapp.png differ diff --git a/assets/images/vache.png b/assets/images/vache.png new file mode 100644 index 0000000000..9c70f81a95 Binary files /dev/null and b/assets/images/vache.png differ diff --git a/config/config-template.json b/config/config-template.json new file mode 100644 index 0000000000..ccb88521fc --- /dev/null +++ b/config/config-template.json @@ -0,0 +1,12 @@ +{ + "flavor": "", + "TITAN_URL": "http://localhost:3000", + // A trailing slash is required for the BACKEND_HOST + "BACKEND_HOST": "", + "SCHOOL_NAME": "", + "PAYMENT_NAME": "", + "APP_ID_PREFIX": "", + "APP_NAME": "", + "PLAUSIBLE_HOST": "", + "PLAUSIBLE_DOMAIN": "" +} diff --git a/ios/Debug-Alpha.xcconfig b/ios/Debug-Alpha.xcconfig new file mode 100644 index 0000000000..ea0003d84c --- /dev/null +++ b/ios/Debug-Alpha.xcconfig @@ -0,0 +1,3 @@ +#include "Flutter/Debug.xcconfig" + +PRODUCT_BUNDLE_IDENTIFIER = $(APP_ID_PREFIX).titan.alpha \ No newline at end of file diff --git a/ios/Debug-Dev.xcconfig b/ios/Debug-Dev.xcconfig new file mode 100644 index 0000000000..e20ac2964d --- /dev/null +++ b/ios/Debug-Dev.xcconfig @@ -0,0 +1,3 @@ +#include "Flutter/Debug.xcconfig" + +PRODUCT_BUNDLE_IDENTIFIER = $(APP_ID_PREFIX).titan.dev \ No newline at end of file diff --git a/ios/Debug-Prod.xcconfig b/ios/Debug-Prod.xcconfig new file mode 100644 index 0000000000..f5151758c8 --- /dev/null +++ b/ios/Debug-Prod.xcconfig @@ -0,0 +1,3 @@ +#include "Flutter/Debug.xcconfig" + +PRODUCT_BUNDLE_IDENTIFIER = $(APP_ID_PREFIX).titan \ No newline at end of file diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 7c56964006..1dc6cf7652 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/ios/Flutter/Debug-Alpha.xcconfig b/ios/Flutter/Debug-Alpha.xcconfig new file mode 100644 index 0000000000..ea0003d84c --- /dev/null +++ b/ios/Flutter/Debug-Alpha.xcconfig @@ -0,0 +1,3 @@ +#include "Flutter/Debug.xcconfig" + +PRODUCT_BUNDLE_IDENTIFIER = $(APP_ID_PREFIX).titan.alpha \ No newline at end of file diff --git a/ios/Flutter/Debug-Dev.xcconfig b/ios/Flutter/Debug-Dev.xcconfig new file mode 100644 index 0000000000..e20ac2964d --- /dev/null +++ b/ios/Flutter/Debug-Dev.xcconfig @@ -0,0 +1,3 @@ +#include "Flutter/Debug.xcconfig" + +PRODUCT_BUNDLE_IDENTIFIER = $(APP_ID_PREFIX).titan.dev \ No newline at end of file diff --git a/ios/Flutter/Debug-Prod.xcconfig b/ios/Flutter/Debug-Prod.xcconfig new file mode 100644 index 0000000000..f5151758c8 --- /dev/null +++ b/ios/Flutter/Debug-Prod.xcconfig @@ -0,0 +1,3 @@ +#include "Flutter/Debug.xcconfig" + +PRODUCT_BUNDLE_IDENTIFIER = $(APP_ID_PREFIX).titan \ No newline at end of file diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index ec97fc6f30..ea03e039c4 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1,2 +1,3 @@ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" +#include "Flutter/Dart-Defines.xcconfig" diff --git a/ios/Flutter/Release-Alpha.xcconfig b/ios/Flutter/Release-Alpha.xcconfig new file mode 100644 index 0000000000..bdda8fe77c --- /dev/null +++ b/ios/Flutter/Release-Alpha.xcconfig @@ -0,0 +1,3 @@ +#include "Flutter/Release.xcconfig" + +PRODUCT_BUNDLE_IDENTIFIER = $(APP_ID_PREFIX).titan.alpha \ No newline at end of file diff --git a/ios/Flutter/Release-Dev.xcconfig b/ios/Flutter/Release-Dev.xcconfig new file mode 100644 index 0000000000..ba2e8d4e55 --- /dev/null +++ b/ios/Flutter/Release-Dev.xcconfig @@ -0,0 +1,3 @@ +#include "Flutter/Release.xcconfig" + +PRODUCT_BUNDLE_IDENTIFIER = $(APP_ID_PREFIX).titan.dev \ No newline at end of file diff --git a/ios/Flutter/Release-Prod.xcconfig b/ios/Flutter/Release-Prod.xcconfig new file mode 100644 index 0000000000..e078ad4b7d --- /dev/null +++ b/ios/Flutter/Release-Prod.xcconfig @@ -0,0 +1,3 @@ +#include "Flutter/Release.xcconfig" + +PRODUCT_BUNDLE_IDENTIFIER = $(APP_ID_PREFIX).titan \ No newline at end of file diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index c4855bfe20..95f3c233df 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1,2 +1,3 @@ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" +#include "Flutter/Dart-Defines.xcconfig" \ No newline at end of file diff --git a/ios/Flutter/extract_dart_defines.sh b/ios/Flutter/extract_dart_defines.sh new file mode 100644 index 0000000000..47ae494d9f --- /dev/null +++ b/ios/Flutter/extract_dart_defines.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +# This script extracts the Dart defines passed from Flutter and creates an +# Xcode configuration file `Dart-Defines.xcconfig` which variables will +# be accessible during the xcode compilation steps. +# Based on https://medium.com/@mahdi.yami3235/mastering-native-configurations-in-flutter-the-power-of-dart-define-10f2b89922dc + +SRCROOT="${SRCROOT:-$(pwd)}" + +OUTPUT_FILE="${SRCROOT}/Flutter/Dart-Defines.xcconfig" +: > $OUTPUT_FILE + +function decode_url() { echo "${*}" | base64 --decode; } + +IFS=',' read -r -a define_items <<<"$DART_DEFINES" + +for index in "${!define_items[@]}" +do + item=$(decode_url "${define_items[$index]}") + + lowercase_item=$(echo "$item" | tr '[:upper:]' '[:lower:]') + if [[ $lowercase_item != flutter* ]]; then + echo "$item" >> "$OUTPUT_FILE" + fi +done \ No newline at end of file diff --git a/ios/Gemfile b/ios/Gemfile new file mode 100644 index 0000000000..b5efb81871 --- /dev/null +++ b/ios/Gemfile @@ -0,0 +1,16 @@ +source "https://rubygems.org" + +gem "abbrev" + + +gem "cocoapods", "1.16.2" +gem "cocoapods-keys" +gem "benchmark" + +gem "fastlane" +gem "ostruct" + +gem "nkf" + +plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') +eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock new file mode 100644 index 0000000000..31ca9de4b3 --- /dev/null +++ b/ios/Gemfile.lock @@ -0,0 +1,313 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.8) + abbrev (0.1.2) + activesupport (5.2.8.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) + minitest (~> 5.1) + tzinfo (~> 1.1) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + algoliasearch (1.27.5) + httpclient (~> 2.8, >= 2.8.3) + json (>= 1.5.1) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.4.0) + aws-partitions (1.1187.0) + aws-sdk-core (3.239.1) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal + jmespath (~> 1, >= 1.6.1) + logger + aws-sdk-kms (1.118.0) + aws-sdk-core (~> 3, >= 3.239.1) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.205.0) + aws-sdk-core (~> 3, >= 3.234.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.12.1) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.3.0) + benchmark (0.4.1) + bigdecimal (3.3.1) + claide (1.1.0) + cocoapods (1.16.2) + addressable (~> 2.8) + claide (>= 1.0.2, < 2.0) + cocoapods-core (= 1.16.2) + cocoapods-deintegrate (>= 1.0.3, < 2.0) + cocoapods-downloader (>= 2.1, < 3.0) + cocoapods-plugins (>= 1.0.0, < 2.0) + cocoapods-search (>= 1.0.0, < 2.0) + cocoapods-trunk (>= 1.6.0, < 2.0) + cocoapods-try (>= 1.1.0, < 2.0) + colored2 (~> 3.1) + escape (~> 0.0.4) + fourflusher (>= 2.3.0, < 3.0) + gh_inspector (~> 1.0) + molinillo (~> 0.8.0) + nap (~> 1.0) + ruby-macho (>= 2.3.0, < 3.0) + xcodeproj (>= 1.27.0, < 2.0) + cocoapods-core (1.16.2) + activesupport (>= 5.0, < 8) + addressable (~> 2.8) + algoliasearch (~> 1.0) + concurrent-ruby (~> 1.1) + fuzzy_match (~> 2.0.4) + nap (~> 1.0) + netrc (~> 0.11) + public_suffix (~> 4.0) + typhoeus (~> 1.0) + cocoapods-deintegrate (1.0.5) + cocoapods-downloader (2.1) + cocoapods-keys (2.3.1) + dotenv + ruby-keychain + cocoapods-plugins (1.0.0) + nap + cocoapods-search (1.0.1) + cocoapods-trunk (1.6.0) + nap (>= 0.8, < 2.0) + netrc (~> 0.11) + cocoapods-try (1.2.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + concurrent-ruby (1.3.5) + csv (3.3.5) + declarative (0.0.20) + digest-crc (0.7.0) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20240107) + dotenv (2.8.1) + emoji_regex (3.2.3) + escape (0.0.4) + ethon (0.16.0) + ffi (>= 1.15.0) + excon (0.112.0) + faraday (1.10.4) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.8) + faraday (>= 0.8.0) + http-cookie (>= 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.1) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.1.1) + multipart-post (~> 2.0) + faraday-net_http (1.0.2) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.1) + faraday (~> 1.0) + fastimage (2.4.0) + fastlane (2.229.0) + CFPropertyList (>= 2.3, < 4.0.0) + abbrev (~> 0.1.2) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored (~> 1.2) + commander (~> 4.6) + csv (~> 3.3) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + mutex_m (~> 0.3.0) + naturally (~> 2.2) + optparse (>= 0.1.1, < 1.0.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.5) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 3) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.4.1) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-plugin-json (1.1.7) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) + ffi (1.17.2) + ffi (1.17.2-arm64-darwin) + fourflusher (2.3.1) + fuzzy_match (2.0.4) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.54.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.3) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.8.0) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.5.0) + google-cloud-storage (1.47.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.31.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.8) + domain_name (~> 0.5) + httpclient (2.9.0) + mutex_m + i18n (1.14.7) + concurrent-ruby (~> 1.0) + jmespath (1.6.2) + json (2.16.0) + jwt (2.10.2) + base64 + logger (1.7.0) + mini_magick (4.13.2) + mini_mime (1.1.5) + minitest (5.25.5) + molinillo (0.8.0) + multi_json (1.17.0) + multipart-post (2.4.1) + mutex_m (0.3.0) + nanaimo (0.4.0) + nap (1.1.0) + naturally (2.3.0) + netrc (0.11.0) + nkf (0.2.0) + og-corefoundation (0.2.3) + ffi + optparse (0.8.0) + os (1.1.4) + ostruct (0.6.1) + plist (3.7.2) + public_suffix (4.0.7) + rake (13.3.1) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.4.4) + rouge (3.28.0) + ruby-keychain (0.4.0) + ffi + og-corefoundation (~> 0.2.0) + ruby-macho (2.5.1) + ruby2_keywords (0.0.5) + rubyzip (2.4.1) + security (0.1.5) + signet (0.21.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 4.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + sysrandom (1.0.5) + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + thread_safe (0.3.6) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + typhoeus (1.4.1) + ethon (>= 0.9.0) + tzinfo (1.2.11) + thread_safe (~> 0.1) + uber (0.1.0) + unicode-display_width (2.6.0) + word_wrap (1.0.0) + xcodeproj (1.27.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) + xcpretty (0.4.1) + rouge (~> 3.28.0) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + arm64-darwin-23 + +DEPENDENCIES + abbrev + benchmark + cocoapods (= 1.16.2) + cocoapods-keys + fastlane + fastlane-plugin-json + nkf + ostruct + +BUNDLED WITH + 2.7.1 diff --git a/ios/GoogleService-Info.plist b/ios/GoogleService-Info.plist deleted file mode 100644 index 14ad19aa27..0000000000 --- a/ios/GoogleService-Info.plist +++ /dev/null @@ -1,34 +0,0 @@ - - - - - CLIENT_ID - 111651829257-sovttf6mt8fqjfeq8pi8v5hto7465ldv.apps.googleusercontent.com - REVERSED_CLIENT_ID - com.googleusercontent.apps.111651829257-sovttf6mt8fqjfeq8pi8v5hto7465ldv - API_KEY - AIzaSyCDW6mgU8-i4Ot-XB-bQLs_FsBjXQ74gY4 - GCM_SENDER_ID - 111651829257 - PLIST_VERSION - 1 - BUNDLE_ID - fr.myecl.titan.alpha - PROJECT_ID - myecl-eclair - STORAGE_BUCKET - myecl-eclair.appspot.com - IS_ADS_ENABLED - - IS_ANALYTICS_ENABLED - - IS_APPINVITE_ENABLED - - IS_GCM_ENABLED - - IS_SIGNIN_ENABLED - - GOOGLE_APP_ID - 1:111651829257:ios:2584a091db1d9dd53f6ad8 - - \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 1b61adda83..98b8b95e2a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -89,16 +89,6 @@ PODS: - GoogleDataTransport (10.1.0): - nanopb (~> 3.30910.0) - PromisesObjC (~> 2.4) - - GoogleMLKit/BarcodeScanning (7.0.0): - - GoogleMLKit/MLKitCore - - MLKitBarcodeScanning (~> 6.0.0) - - GoogleMLKit/MLKitCore (7.0.0): - - MLKitCommon (~> 12.0.0) - - GoogleToolboxForMac/Defines (4.2.1) - - GoogleToolboxForMac/Logger (4.2.1): - - GoogleToolboxForMac/Defines (= 4.2.1) - - "GoogleToolboxForMac/NSData+zlib (4.2.1)": - - GoogleToolboxForMac/Defines (= 4.2.1) - GoogleUtilities/AppDelegateSwizzler (8.1.0): - GoogleUtilities/Environment - GoogleUtilities/Logger @@ -123,7 +113,6 @@ PODS: - GoogleUtilities/UserDefaults (8.1.0): - GoogleUtilities/Logger - GoogleUtilities/Privacy - - GTMSessionFetcher/Core (3.5.0) - image_cropper (0.0.4): - Flutter - TOCropViewController (~> 2.7.4) @@ -132,26 +121,9 @@ PODS: - local_auth_darwin (0.0.1): - Flutter - FlutterMacOS - - MLImage (1.0.0-beta6) - - MLKitBarcodeScanning (6.0.0): - - MLKitCommon (~> 12.0) - - MLKitVision (~> 8.0) - - MLKitCommon (12.0.0): - - GoogleDataTransport (~> 10.0) - - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) - - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" - - GoogleUtilities/Logger (~> 8.0) - - GoogleUtilities/UserDefaults (~> 8.0) - - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) - - MLKitVision (8.0.0): - - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) - - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" - - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) - - MLImage (= 1.0.0-beta6) - - MLKitCommon (~> 12.0) - - mobile_scanner (6.0.2): + - mobile_scanner (7.0.0): - Flutter - - GoogleMLKit/BarcodeScanning (~> 7.0.0) + - FlutterMacOS - nanopb (3.30910.0): - nanopb/decode (= 3.30910.0) - nanopb/encode (= 3.30910.0) @@ -195,7 +167,7 @@ DEPENDENCIES: - image_cropper (from `.symlinks/plugins/image_cropper/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - - mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`) + - mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - pdfx (from `.symlinks/plugins/pdfx/ios`) @@ -215,14 +187,7 @@ SPEC REPOS: - FirebaseInstallations - FirebaseMessaging - GoogleDataTransport - - GoogleMLKit - - GoogleToolboxForMac - GoogleUtilities - - GTMSessionFetcher - - MLImage - - MLKitBarcodeScanning - - MLKitCommon - - MLKitVision - nanopb - PromisesObjC - SDWebImage @@ -257,7 +222,7 @@ EXTERNAL SOURCES: local_auth_darwin: :path: ".symlinks/plugins/local_auth_darwin/darwin" mobile_scanner: - :path: ".symlinks/plugins/mobile_scanner/ios" + :path: ".symlinks/plugins/mobile_scanner/darwin" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: @@ -288,23 +253,16 @@ SPEC CHECKSUMS: FirebaseCoreInternal: ef4505d2afb1d0ebbc33162cb3795382904b5679 FirebaseInstallations: 9980995bdd06ec8081dfb6ab364162bdd64245c3 FirebaseMessaging: 2b9f56aa4ed286e1f0ce2ee1d413aabb8f9f5cb9 - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 flutter_appauth: d4abcf54856e5d8ba82ed7646ffc83245d4aa448 flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb flutter_secure_storage_darwin: ce237a8775b39723566dc72571190a3769d70468 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 - GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318 - GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 - GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 image_cropper: c4326ea50132b1e1564499e5d32a84f01fb03537 image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 - MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56 - MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2 - MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d - MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e - mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036 + mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 diff --git a/ios/Release-Alpha.xcconfig b/ios/Release-Alpha.xcconfig new file mode 100644 index 0000000000..bdda8fe77c --- /dev/null +++ b/ios/Release-Alpha.xcconfig @@ -0,0 +1,3 @@ +#include "Flutter/Release.xcconfig" + +PRODUCT_BUNDLE_IDENTIFIER = $(APP_ID_PREFIX).titan.alpha \ No newline at end of file diff --git a/ios/Release-Dev.xcconfig b/ios/Release-Dev.xcconfig new file mode 100644 index 0000000000..ba2e8d4e55 --- /dev/null +++ b/ios/Release-Dev.xcconfig @@ -0,0 +1,3 @@ +#include "Flutter/Release.xcconfig" + +PRODUCT_BUNDLE_IDENTIFIER = $(APP_ID_PREFIX).titan.dev \ No newline at end of file diff --git a/ios/Release-Prod.xcconfig b/ios/Release-Prod.xcconfig new file mode 100644 index 0000000000..e078ad4b7d --- /dev/null +++ b/ios/Release-Prod.xcconfig @@ -0,0 +1,3 @@ +#include "Flutter/Release.xcconfig" + +PRODUCT_BUNDLE_IDENTIFIER = $(APP_ID_PREFIX).titan \ No newline at end of file diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index c021c407ab..c88b3c9e40 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -9,13 +9,18 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 2E449DB12AAC978B002A62E0 /* config in Resources */ = {isa = PBXBuildFile; fileRef = 2E449DB02AAC978B002A62E0 /* config */; }; + 2E7BE80D2E52386A0019074E /* Release-Prod.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 2E7BE80C2E52386A0019074E /* Release-Prod.xcconfig */; }; + 2E7BE80F2E5238710019074E /* Release-Alpha.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 2E7BE80E2E5238710019074E /* Release-Alpha.xcconfig */; }; + 2E7BE8112E5238800019074E /* Release-Dev.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 2E7BE8102E5238800019074E /* Release-Dev.xcconfig */; }; + 2E7BE8132E5238B50019074E /* Debug-Prod.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 2E7BE8122E5238B50019074E /* Debug-Prod.xcconfig */; }; + 2E7BE8152E5238BE0019074E /* Debug-Alpha.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 2E7BE8142E5238BE0019074E /* Debug-Alpha.xcconfig */; }; + 2E7BE8172E5238C60019074E /* Debug-Dev.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 2E7BE8162E5238C60019074E /* Debug-Dev.xcconfig */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B48C9D5838D13816EADEEBE /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 41DB4EFE6EC199B0BB0C20E7 /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - E58A76F02CB6805B0024F420 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = E58A76EF2CB6805B0024F420 /* GoogleService-Info.plist */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -37,6 +42,12 @@ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 159AB80AD2B51C6D7D28AE76 /* Pods-Runner.profile-prod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile-prod.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile-prod.xcconfig"; sourceTree = ""; }; 2E449DB02AAC978B002A62E0 /* config */ = {isa = PBXFileReference; lastKnownFileType = folder; path = config; sourceTree = ""; }; + 2E7BE80C2E52386A0019074E /* Release-Prod.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Release-Prod.xcconfig"; sourceTree = ""; }; + 2E7BE80E2E5238710019074E /* Release-Alpha.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Release-Alpha.xcconfig"; sourceTree = ""; }; + 2E7BE8102E5238800019074E /* Release-Dev.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Release-Dev.xcconfig"; sourceTree = ""; }; + 2E7BE8122E5238B50019074E /* Debug-Prod.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Debug-Prod.xcconfig"; sourceTree = ""; }; + 2E7BE8142E5238BE0019074E /* Debug-Alpha.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Debug-Alpha.xcconfig"; sourceTree = ""; }; + 2E7BE8162E5238C60019074E /* Debug-Dev.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Debug-Dev.xcconfig"; sourceTree = ""; }; 2EB49A532937B564004F7D93 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 3EF737888DC7420CCB3C7E40 /* Pods-Runner.profile-dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile-dev.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile-dev.xcconfig"; sourceTree = ""; }; @@ -60,7 +71,6 @@ CF600A22821ED74D8DCE4F05 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; D13F3C7F59A7D2FDD62D552A /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; D971B68565D0F571B60A89AA /* Pods-Runner.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release-alpha.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release-alpha.xcconfig"; sourceTree = ""; }; - E58A76EF2CB6805B0024F420 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -89,13 +99,18 @@ 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( - E58A76EF2CB6805B0024F420 /* GoogleService-Info.plist */, 2E449DB02AAC978B002A62E0 /* config */, 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, CF897D9A422151DDBD917267 /* Pods */, BB1ED0B3ADEE5307F6A2B2DC /* Frameworks */, + 2E7BE80C2E52386A0019074E /* Release-Prod.xcconfig */, + 2E7BE80E2E5238710019074E /* Release-Alpha.xcconfig */, + 2E7BE8102E5238800019074E /* Release-Dev.xcconfig */, + 2E7BE8122E5238B50019074E /* Debug-Prod.xcconfig */, + 2E7BE8142E5238BE0019074E /* Debug-Alpha.xcconfig */, + 2E7BE8162E5238C60019074E /* Debug-Dev.xcconfig */, ); sourceTree = ""; }; @@ -215,12 +230,17 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2E7BE8112E5238800019074E /* Release-Dev.xcconfig in Resources */, + 2E7BE8152E5238BE0019074E /* Debug-Alpha.xcconfig in Resources */, 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 2E7BE80F2E5238710019074E /* Release-Alpha.xcconfig in Resources */, + 2E7BE8132E5238B50019074E /* Debug-Prod.xcconfig in Resources */, + 2E7BE8172E5238C60019074E /* Debug-Dev.xcconfig in Resources */, + 2E7BE80D2E52386A0019074E /* Release-Prod.xcconfig in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 2E449DB12AAC978B002A62E0 /* config in Resources */, - E58A76F02CB6805B0024F420 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -408,7 +428,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; @@ -419,24 +439,22 @@ }; 249021D4217E4FDB00AE95B9 /* Profile-Prod */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + baseConfigurationReference = 2E7BE80C2E52386A0019074E /* Release-Prod.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-Prod"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = D7YW2UB8DQ; + DEVELOPMENT_TEAM = 25XYVB6GDY; ENABLE_BITCODE = NO; - FLAVOR_URL_SCHEME = titan; INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = MyECL; + INFOPLIST_KEY_CFBundleDisplayName = "$(APP_NAME)"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = fr.myecl.titan; - PRODUCT_NAME = MyECL; + PRODUCT_NAME = "Titan"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -491,7 +509,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -501,24 +519,22 @@ }; 2E449DA32AAC86C2002A62E0 /* Debug-Dev */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + baseConfigurationReference = 2E7BE8162E5238C60019074E /* Debug-Dev.xcconfig */; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-Dev"; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-Dev; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = D7YW2UB8DQ; + DEVELOPMENT_TEAM = 25XYVB6GDY; ENABLE_BITCODE = NO; - FLAVOR_URL_SCHEME = titan.dev; INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "MyECL Dev"; + INFOPLIST_KEY_CFBundleDisplayName = "$(APP_NAME) Dev"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = fr.myecl.titan.dev; - PRODUCT_NAME = "MyECL Dev"; + PRODUCT_NAME = "Titan"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -568,7 +584,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; @@ -581,24 +597,22 @@ }; 2E449DA52AAC86C9002A62E0 /* Release-Dev */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + baseConfigurationReference = 2E7BE8102E5238800019074E /* Release-Dev.xcconfig */; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-Dev"; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-Dev; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = D7YW2UB8DQ; + DEVELOPMENT_TEAM = 25XYVB6GDY; ENABLE_BITCODE = NO; - FLAVOR_URL_SCHEME = titan.dev; INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "MyECL Dev"; + INFOPLIST_KEY_CFBundleDisplayName = "$(APP_NAME) Dev"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = fr.myecl.titan.dev; - PRODUCT_NAME = "MyECL Dev"; + PRODUCT_NAME = "Titan"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -647,7 +661,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; @@ -658,24 +672,22 @@ }; 2E449DA72AAC86CD002A62E0 /* Profile-Dev */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + baseConfigurationReference = 2E7BE8102E5238800019074E /* Release-Dev.xcconfig */; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-Dev"; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-Dev; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = D7YW2UB8DQ; + DEVELOPMENT_TEAM = 25XYVB6GDY; ENABLE_BITCODE = NO; - FLAVOR_URL_SCHEME = titan.dev; INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "MyECL Dev"; + INFOPLIST_KEY_CFBundleDisplayName = "$(APP_NAME) Dev"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = fr.myecl.titan.dev; - PRODUCT_NAME = "MyECL Dev"; + PRODUCT_NAME = "Titan"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -730,7 +742,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -740,24 +752,22 @@ }; 2E449DA92AAC874A002A62E0 /* Debug-Alpha */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + baseConfigurationReference = 2E7BE8142E5238BE0019074E /* Debug-Alpha.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-Alpha"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = D7YW2UB8DQ; + DEVELOPMENT_TEAM = 25XYVB6GDY; ENABLE_BITCODE = NO; - FLAVOR_URL_SCHEME = titan.alpha; INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "MyECL Alpha"; + INFOPLIST_KEY_CFBundleDisplayName = "$(APP_NAME) Alpha"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = fr.myecl.titan.alpha; - PRODUCT_NAME = "MyECL Alpha"; + PRODUCT_NAME = "Titan"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -807,7 +817,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; @@ -820,24 +830,22 @@ }; 2E449DAB2AAC8751002A62E0 /* Release-Alpha */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + baseConfigurationReference = 2E7BE80E2E5238710019074E /* Release-Alpha.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-Alpha"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = D7YW2UB8DQ; + DEVELOPMENT_TEAM = 25XYVB6GDY; ENABLE_BITCODE = NO; - FLAVOR_URL_SCHEME = titan.alpha; INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "MyECL Alpha"; + INFOPLIST_KEY_CFBundleDisplayName = "$(APP_NAME) Alpha"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = fr.myecl.titan.alpha; - PRODUCT_NAME = "MyECL Alpha"; + PRODUCT_NAME = "Titan"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -886,7 +894,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; @@ -897,24 +905,22 @@ }; 2E449DAD2AAC8759002A62E0 /* Profile-Alpha */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + baseConfigurationReference = 2E7BE80E2E5238710019074E /* Release-Alpha.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-Alpha"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = D7YW2UB8DQ; + DEVELOPMENT_TEAM = 25XYVB6GDY; ENABLE_BITCODE = NO; - FLAVOR_URL_SCHEME = titan.alpha; INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = "MyECL Alpha"; + INFOPLIST_KEY_CFBundleDisplayName = "$(APP_NAME) Alpha"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = fr.myecl.titan.alpha; - PRODUCT_NAME = "MyECL Alpha"; + PRODUCT_NAME = "Titan"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -969,7 +975,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -1019,7 +1025,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; @@ -1032,24 +1038,22 @@ }; 97C147061CF9000F007C117D /* Debug-Prod */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + baseConfigurationReference = 2E7BE8122E5238B50019074E /* Debug-Prod.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-Prod"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = D7YW2UB8DQ; + DEVELOPMENT_TEAM = 25XYVB6GDY; ENABLE_BITCODE = NO; - FLAVOR_URL_SCHEME = titan; INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = MyECL; + INFOPLIST_KEY_CFBundleDisplayName = "$(APP_NAME)"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = fr.myecl.titan; - PRODUCT_NAME = MyECL; + PRODUCT_NAME = "Titan"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -1059,24 +1063,22 @@ }; 97C147071CF9000F007C117D /* Release-Prod */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + baseConfigurationReference = 2E7BE80C2E52386A0019074E /* Release-Prod.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-Prod"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = D7YW2UB8DQ; + DEVELOPMENT_TEAM = 25XYVB6GDY; ENABLE_BITCODE = NO; - FLAVOR_URL_SCHEME = titan; INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = MyECL; + INFOPLIST_KEY_CFBundleDisplayName = "$(APP_NAME)"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = fr.myecl.titan; - PRODUCT_NAME = MyECL; + PRODUCT_NAME = "Titan"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Alpha.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Alpha.xcscheme index ab7e0b354f..ba509abcdd 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Alpha.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Alpha.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + @@ -46,7 +64,7 @@ @@ -63,7 +81,7 @@ diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Dev.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Dev.xcscheme index 1456bcc6c7..a300e0b5f2 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Dev.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Dev.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + @@ -26,6 +44,7 @@ buildConfiguration = "Debug-Dev" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES" shouldAutocreateTestPlan = "YES"> @@ -33,6 +52,7 @@ buildConfiguration = "Release-Dev" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" @@ -44,7 +64,7 @@ @@ -61,7 +81,7 @@ diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Prod.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Prod.xcscheme index 0407e9828e..32325c03cd 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Prod.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Prod.xcscheme @@ -1,10 +1,28 @@ + version = "1.7"> + + + + + + + + + + @@ -26,12 +44,13 @@ buildConfiguration = "Debug-Prod" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> @@ -43,6 +62,7 @@ buildConfiguration = "Release-Prod" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" @@ -54,7 +74,7 @@ @@ -71,7 +91,7 @@ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-1024x1024@1x.png index 9da7e707b2..9728b96ae7 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-20x20@1x.png index 9441acdc4d..776b785d92 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-20x20@2x.png index 6ab348d7a1..abbaf3a33b 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-20x20@3x.png index acd17bdae9..adb5450e40 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-29x29@1x.png index 010b95a6d2..d574f9f4cd 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-29x29@2x.png index 5e616f5429..4407153000 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-29x29@3x.png index d62f7743d5..277b8f7a0d 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-40x40@1x.png index 6ab348d7a1..abbaf3a33b 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-40x40@2x.png index 2ad49d6970..0e3e89423c 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-40x40@3x.png index f1bef4db66..e5b151c4c3 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-50x50@1x.png index ce9fb2de73..1f086f00b1 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-50x50@1x.png and b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-50x50@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-50x50@2x.png index f172b0307d..c74349fc4a 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-50x50@2x.png and b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-50x50@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-57x57@1x.png index 0222086552..0da530438a 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-57x57@1x.png and b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-57x57@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-57x57@2x.png index 23a4db420d..9a7deb3e46 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-57x57@2x.png and b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-57x57@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-60x60@2x.png index f1bef4db66..e5b151c4c3 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-60x60@3x.png index 7051b1aa6b..72d0f76168 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-72x72@1x.png index 5975719cc4..63fb07e2aa 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-72x72@1x.png and b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-72x72@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-72x72@2x.png new file mode 100644 index 0000000000..eeaa8c37cb Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-72x72@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-76x76@1x.png index d52c4119cf..112c01e9a7 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-76x76@2x.png index aa508c504e..50d9014194 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-83.5x83.5@2x.png index 322f24e97b..8d6efe8e9a 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/AppIcon-Alpha-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/Contents.json index f419b39775..a8ebb59775 100644 --- a/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/Contents.json +++ b/ios/Runner/Assets.xcassets/AppIcon-Alpha.appiconset/Contents.json @@ -1 +1 @@ -{"images":[{"size":"20x20","idiom":"iphone","filename":"AppIcon-Alpha-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"AppIcon-Alpha-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-Alpha-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-Alpha-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-Alpha-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-Alpha-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-Alpha-40x40@3x.png","scale":"3x"},{"size":"50x50","idiom":"ipad","filename":"AppIcon-Alpha-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"AppIcon-Alpha-50x50@2x.png","scale":"2x"},{"size":"57x57","idiom":"iphone","filename":"AppIcon-Alpha-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"AppIcon-Alpha-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-Alpha-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-Alpha-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-Alpha-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-Alpha-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-Alpha-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-Alpha-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-Alpha-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-Alpha-40x40@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"AppIcon-Alpha-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"AppIcon-Alpha-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-Alpha-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-Alpha-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"AppIcon-Alpha-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"AppIcon-Alpha-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} \ No newline at end of file +{"images":[{"size":"20x20","idiom":"iphone","filename":"AppIcon-Alpha-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"AppIcon-Alpha-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-Alpha-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-Alpha-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-Alpha-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-Alpha-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-Alpha-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"AppIcon-Alpha-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"AppIcon-Alpha-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-Alpha-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-Alpha-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-Alpha-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-Alpha-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-Alpha-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-Alpha-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-Alpha-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-Alpha-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"AppIcon-Alpha-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"AppIcon-Alpha-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"AppIcon-Alpha-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"AppIcon-Alpha-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-Alpha-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-Alpha-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"AppIcon-Alpha-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"AppIcon-Alpha-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-1024x1024@1x.png index db9a28d2e5..9607d660dc 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-20x20@1x.png index bcc33fa7e1..a11aad9571 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-20x20@2x.png index d1be804695..3c6ffbed38 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-20x20@3x.png index 7b33868896..57df5e4cb6 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-29x29@1x.png index cd5b5af6e3..7a49a9efba 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-29x29@2x.png index 6b70d58d9a..16e70658fa 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-29x29@3x.png index 6cb76a1c17..91fbfa36b2 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-40x40@1x.png index d1be804695..3c6ffbed38 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-40x40@2x.png index ad4f38aa71..1cecf0301e 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-40x40@3x.png index dfea51f84c..31dfe916dd 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-50x50@1x.png index 41d3696a90..374ffbdf15 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-50x50@1x.png and b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-50x50@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-50x50@2x.png index 9f74da6214..238af1e686 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-50x50@2x.png and b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-50x50@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-57x57@1x.png index f1bb563a3c..a2620843f8 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-57x57@1x.png and b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-57x57@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-57x57@2x.png index 9fff825ff6..01f3f67464 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-57x57@2x.png and b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-57x57@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-60x60@2x.png index dfea51f84c..31dfe916dd 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-60x60@3x.png index 06e5e332b7..a9564a8950 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-72x72@1x.png index 82f3ab495d..75307dacb7 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-72x72@1x.png and b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-72x72@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-72x72@2x.png index c38790da64..f8df98fbed 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-72x72@2x.png and b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-72x72@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-76x76@1x.png index df829f8755..d759903a18 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-76x76@2x.png index 25b65d7f22..beb5427628 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-83.5x83.5@2x.png index 17ca4a146d..b4f7957631 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/AppIcon-Dev-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/Contents.json index f770de70ad..8534d59076 100644 --- a/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/Contents.json +++ b/ios/Runner/Assets.xcassets/AppIcon-Dev.appiconset/Contents.json @@ -1 +1 @@ -{"images":[{"size":"20x20","idiom":"iphone","filename":"AppIcon-Dev-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"AppIcon-Dev-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-Dev-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-Dev-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-Dev-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-Dev-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-Dev-40x40@3x.png","scale":"3x"},{"size":"50x50","idiom":"ipad","filename":"AppIcon-Dev-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"AppIcon-Dev-50x50@2x.png","scale":"2x"},{"size":"57x57","idiom":"iphone","filename":"AppIcon-Dev-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"AppIcon-Dev-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-Dev-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-Dev-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-Dev-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-Dev-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-Dev-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-Dev-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-Dev-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-Dev-40x40@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"AppIcon-Dev-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"AppIcon-Dev-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-Dev-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-Dev-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"AppIcon-Dev-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"AppIcon-Dev-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} \ No newline at end of file +{"images":[{"size":"20x20","idiom":"iphone","filename":"AppIcon-Dev-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"AppIcon-Dev-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-Dev-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-Dev-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-Dev-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-Dev-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-Dev-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"AppIcon-Dev-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"AppIcon-Dev-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-Dev-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-Dev-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-Dev-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-Dev-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-Dev-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-Dev-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-Dev-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-Dev-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"AppIcon-Dev-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"AppIcon-Dev-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"AppIcon-Dev-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"AppIcon-Dev-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-Dev-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-Dev-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"AppIcon-Dev-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"AppIcon-Dev-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-1024x1024@1x.png index f4f4fb5e4c..05f2f251cc 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-20x20@1x.png index 7f6248337c..dace9d0d35 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-20x20@2x.png index 38a0d15f3b..835eee837c 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-20x20@3x.png index 9f6d6de02e..40b6cff004 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-29x29@1x.png index 08d68e2795..deda39e026 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-29x29@2x.png index 1d2ea2dea6..8df025c26d 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-29x29@3x.png index d18a8fafbe..53b36fb2f6 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-40x40@1x.png index 38a0d15f3b..835eee837c 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-40x40@2x.png index e6bc469506..26c54e8fc6 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-40x40@3x.png index 94cb19e728..add9a5fbbf 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-50x50@1x.png index 33149ed81c..5dc6d040f5 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-50x50@1x.png and b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-50x50@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-50x50@2x.png index 642ae41762..534a92bf73 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-50x50@2x.png and b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-50x50@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-57x57@1x.png index 4710f642fc..401720c06a 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-57x57@1x.png and b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-57x57@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-57x57@2x.png index cdfcfacb81..3852ee3470 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-57x57@2x.png and b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-57x57@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-60x60@2x.png index 94cb19e728..add9a5fbbf 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-60x60@3x.png index cf93f35d31..81629613e9 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-72x72@1x.png index 197ffde8b0..5ebb4d61f2 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-72x72@1x.png and b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-72x72@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-72x72@2x.png index 32b36e3ae1..e0effec346 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-72x72@2x.png and b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-72x72@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-76x76@1x.png index c255f12ceb..681bbe5e0c 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-76x76@2x.png index 1db400b9ff..e74b6ab5a9 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-83.5x83.5@2x.png index 3677ab02f3..993748f284 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/AppIcon-Prod-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/Contents.json index 19cbfde4a6..3dede4dc9f 100644 --- a/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/Contents.json +++ b/ios/Runner/Assets.xcassets/AppIcon-Prod.appiconset/Contents.json @@ -1 +1 @@ -{"images":[{"size":"20x20","idiom":"iphone","filename":"AppIcon-Prod-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"AppIcon-Prod-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-Prod-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-Prod-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-Prod-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-Prod-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-Prod-40x40@3x.png","scale":"3x"},{"size":"50x50","idiom":"ipad","filename":"AppIcon-Prod-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"AppIcon-Prod-50x50@2x.png","scale":"2x"},{"size":"57x57","idiom":"iphone","filename":"AppIcon-Prod-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"AppIcon-Prod-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-Prod-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-Prod-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-Prod-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-Prod-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-Prod-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-Prod-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-Prod-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-Prod-40x40@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"AppIcon-Prod-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"AppIcon-Prod-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-Prod-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-Prod-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"AppIcon-Prod-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"AppIcon-Prod-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} \ No newline at end of file +{"images":[{"size":"20x20","idiom":"iphone","filename":"AppIcon-Prod-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"AppIcon-Prod-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-Prod-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-Prod-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-Prod-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-Prod-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-Prod-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"AppIcon-Prod-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"AppIcon-Prod-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-Prod-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-Prod-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-Prod-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-Prod-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-Prod-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-Prod-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-Prod-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-Prod-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"AppIcon-Prod-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"AppIcon-Prod-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"AppIcon-Prod-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"AppIcon-Prod-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-Prod-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-Prod-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"AppIcon-Prod-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"AppIcon-Prod-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-1024x1024@1x.png new file mode 100644 index 0000000000..05f2f251cc Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-20x20@1x.png new file mode 100644 index 0000000000..dace9d0d35 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-20x20@2x.png new file mode 100644 index 0000000000..835eee837c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-20x20@3x.png new file mode 100644 index 0000000000..40b6cff004 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-29x29@1x.png new file mode 100644 index 0000000000..deda39e026 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-29x29@2x.png new file mode 100644 index 0000000000..8df025c26d Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-29x29@3x.png new file mode 100644 index 0000000000..53b36fb2f6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-40x40@1x.png new file mode 100644 index 0000000000..835eee837c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-40x40@2x.png new file mode 100644 index 0000000000..26c54e8fc6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-40x40@3x.png new file mode 100644 index 0000000000..add9a5fbbf Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-50x50@1x.png new file mode 100644 index 0000000000..5dc6d040f5 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-50x50@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-50x50@2x.png new file mode 100644 index 0000000000..534a92bf73 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-50x50@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-57x57@1x.png new file mode 100644 index 0000000000..401720c06a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-57x57@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-57x57@2x.png new file mode 100644 index 0000000000..3852ee3470 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-57x57@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-60x60@2x.png new file mode 100644 index 0000000000..add9a5fbbf Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-60x60@3x.png new file mode 100644 index 0000000000..81629613e9 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-72x72@1x.png new file mode 100644 index 0000000000..5ebb4d61f2 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-72x72@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-72x72@2x.png new file mode 100644 index 0000000000..e0effec346 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-72x72@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-76x76@1x.png new file mode 100644 index 0000000000..681bbe5e0c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-76x76@2x.png new file mode 100644 index 0000000000..e74b6ab5a9 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-83.5x83.5@2x.png new file mode 100644 index 0000000000..993748f284 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/AppIcon-Prod-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/Contents.json new file mode 100644 index 0000000000..3dede4dc9f --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon-emlyon.appiconset/Contents.json @@ -0,0 +1 @@ +{"images":[{"size":"20x20","idiom":"iphone","filename":"AppIcon-Prod-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"AppIcon-Prod-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-Prod-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-Prod-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"AppIcon-Prod-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-Prod-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"AppIcon-Prod-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"AppIcon-Prod-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"AppIcon-Prod-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-Prod-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"AppIcon-Prod-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-Prod-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"AppIcon-Prod-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-Prod-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"AppIcon-Prod-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-Prod-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"AppIcon-Prod-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"AppIcon-Prod-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"AppIcon-Prod-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"AppIcon-Prod-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"AppIcon-Prod-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-Prod-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"AppIcon-Prod-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"AppIcon-Prod-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"AppIcon-Prod-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} \ No newline at end of file diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index bece8e5086..480d0b7317 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -6,8 +6,6 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - $(PRODUCT_NAME) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -15,7 +13,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - myecl + $(APP_NAME) CFBundlePackageType APPL CFBundleShortVersionString @@ -28,10 +26,10 @@ CFBundleTypeRole Editor CFBundleURLName - fr.myecl.titan + $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleURLSchemes - $(FLAVOR_URL_SCHEME) + $(PRODUCT_BUNDLE_IDENTIFIER) diff --git a/ios/fastlane/Appfile b/ios/fastlane/Appfile new file mode 100644 index 0000000000..f0253a88e8 --- /dev/null +++ b/ios/fastlane/Appfile @@ -0,0 +1,30 @@ +app_identifier("fr.proximapp.myemapp.titan") # The bundle identifier of your app +apple_id("pnp@em-lyon.com") # Your Apple Developer Portal username + +itc_team_id("121183110") # App Store Connect Team ID +team_id("25XYVB6GDY") # Developer Portal Team ID + + +for_platform :ios do + for_lane :beta do |options| + flavor = options[:flavor] # prod, alpha, dev + + + config_json = read_json( + json_path: "./../config/config-#{flavor}.json" + ) + + app_id_prefix = config_json[:APP_ID_PREFIX] # fr.myecl + app_name = config_json[:APP_NAME] # MyECL + + if flavor == "prod" + app_identifier("#{app_id_prefix}.titan") + else + app_identifier("#{app_id_prefix}.titan.#{flavor}") + end + + end +end + +# For more information about the Appfile, see: +# https://docs.fastlane.tools/advanced/#appfile diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile new file mode 100644 index 0000000000..12a65c39df --- /dev/null +++ b/ios/fastlane/Fastfile @@ -0,0 +1,46 @@ +# This file contains the fastlane.tools configuration +# You can find the documentation at https://docs.fastlane.tools +# +# For a list of all available actions, check out +# +# https://docs.fastlane.tools/actions +# +# For a list of all available plugins, check out +# +# https://docs.fastlane.tools/plugins/available-plugins +# + +# Uncomment the line if you want fastlane to automatically update itself +# update_fastlane + +default_platform(:ios) + +platform :ios do + desc "Submit a new Beta Build to App Store" + lane :beta do |options| + flavor = options[:flavor] # prod, alpha, dev + + config_json = read_json( + json_path: "./../config/config-#{flavor}.json" + ) + + app_id_prefix = config_json[:APP_ID_PREFIX] # fr.myecl + app_name = config_json[:APP_NAME] # MyECL + + api_key = app_store_connect_api_key( + key_id: "LJN944T6G6", + issuer_id: "8f69d502-442d-459b-a1df-84a3cb7ca7b2", + key_filepath: "./app-store-connect-api.p8", + duration: 1200, # optional (maximum 1200) + in_house: false # optional but may be required if using match/sigh + ) + + sh "flutter build ipa --dart-define-from-file=config/config-#{flavor}.json --release --flavor='#{flavor}' --export-method=app-store" + + upload_to_testflight( + ipa: "../build/ios/ipa/#{app_name}.ipa", + skip_waiting_for_build_processing: true, + api_key: api_key + ) + end +end diff --git a/ios/fastlane/Pluginfile b/ios/fastlane/Pluginfile new file mode 100644 index 0000000000..bffd8eb24d --- /dev/null +++ b/ios/fastlane/Pluginfile @@ -0,0 +1,5 @@ +# Autogenerated by fastlane +# +# Ensure this file is checked in to source control! + +gem 'fastlane-plugin-json' diff --git a/ios/fastlane/README.md b/ios/fastlane/README.md new file mode 100644 index 0000000000..da1fda6ed0 --- /dev/null +++ b/ios/fastlane/README.md @@ -0,0 +1,32 @@ +fastlane documentation +---- + +# Installation + +Make sure you have the latest version of the Xcode command line tools installed: + +```sh +xcode-select --install +``` + +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) + +# Available Actions + +## iOS + +### ios beta + +```sh +[bundle exec] fastlane ios beta +``` + +Submit a new Beta Build to App Store + +---- + +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000000..c2f3c4ffd7 --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,4 @@ +arb-dir: lib/l10n +template-arb-file: app_fr.arb +output-localization-file: app_localizations.dart +untranslated-messages-file: missing.txt diff --git a/lib/admin/admin.dart b/lib/admin/admin.dart new file mode 100644 index 0000000000..b5bbd01d33 --- /dev/null +++ b/lib/admin/admin.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:titan/admin/router.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/ui/widgets/top_bar.dart'; + +class AdminTemplate extends StatelessWidget { + final Widget child; + const AdminTemplate({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return Container( + color: ColorConstants.background, + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + TopBar(root: AdminRouter.root), + Expanded(child: child), + ], + ), + ), + ); + } +} diff --git a/lib/admin/class/assocation.dart b/lib/admin/class/assocation.dart new file mode 100644 index 0000000000..22efd6b521 --- /dev/null +++ b/lib/admin/class/assocation.dart @@ -0,0 +1,38 @@ +class Association { + Association({required this.name, required this.groupId, required this.id}); + late final String name; + late final String groupId; + late final String id; + + Association.fromJson(Map json) { + name = json['name']; + groupId = json['group_id']; + id = json['id']; + } + + Map toJson() { + final data = {}; + data['name'] = name; + data['group_id'] = groupId; + data['id'] = id; + return data; + } + + Association copyWith({String? name, String? groupId, String? id}) => + Association( + name: name ?? this.name, + groupId: groupId ?? this.groupId, + id: id ?? this.id, + ); + + Association.empty() { + name = 'Nom'; + groupId = ''; + id = ''; + } + + @override + String toString() { + return 'Association(name: $name, groupId: $groupId, id: $id)'; + } +} diff --git a/lib/admin/providers/assocation_list_provider.dart b/lib/admin/providers/assocation_list_provider.dart new file mode 100644 index 0000000000..6702b5abd0 --- /dev/null +++ b/lib/admin/providers/assocation_list_provider.dart @@ -0,0 +1,63 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/admin/class/assocation.dart'; +import 'package:titan/admin/repositories/association_repository.dart'; +import 'package:titan/tools/providers/list_notifier.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; + +class AssociationListNotifier extends ListNotifier { + final AssociationRepository associationRepository; + AssociationListNotifier({required this.associationRepository}) + : super(const AsyncValue.loading()); + + Future>> loadAssociations() async { + return await loadList(associationRepository.getAssociationList); + } + + Future createAssociation(Association association) async { + return await add(associationRepository.createAssociation, association); + } + + Future updateAssociation(Association association) async { + return await update( + associationRepository.updateAssociation, + (associations, association) => associations + ..[associations.indexWhere((g) => g.id == association.id)] = + association, + association, + ); + } + + Future deleteAssociation(Association association) async { + return await delete( + associationRepository.deleteAssociation, + (associations, association) => + associations..removeWhere((i) => i.id == association.id), + association.id, + association, + ); + } + + void setAssociation(Association association) { + state.whenData((d) { + if (d.indexWhere((g) => g.id == association.id) == -1) return; + state = AsyncValue.data( + d..[d.indexWhere((g) => g.id == association.id)] = association, + ); + }); + } +} + +final associationListProvider = + StateNotifierProvider< + AssociationListNotifier, + AsyncValue> + >((ref) { + final associationRepository = ref.watch(associationRepositoryProvider); + AssociationListNotifier provider = AssociationListNotifier( + associationRepository: associationRepository, + ); + tokenExpireWrapperAuth(ref, () async { + await provider.loadAssociations(); + }); + return provider; + }); diff --git a/lib/admin/providers/assocation_logo_provider.dart b/lib/admin/providers/assocation_logo_provider.dart new file mode 100644 index 0000000000..1a0d895cd8 --- /dev/null +++ b/lib/admin/providers/assocation_logo_provider.dart @@ -0,0 +1,45 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/admin/providers/association_logo_list.dart'; +import 'package:titan/admin/repositories/association_logo_repository.dart'; +import 'package:titan/auth/providers/openid_provider.dart'; +import 'package:titan/tools/providers/single_notifier.dart'; + +class AssociationLogoNotifier extends SingleNotifier { + final associationLogoRepository = AssociationLogoRepository(); + final AssociationLogoListNotifier associationLogoListNotifier; + AssociationLogoNotifier({ + required String token, + required this.associationLogoListNotifier, + }) : super(const AsyncValue.loading()) { + associationLogoRepository.setToken(token); + } + + Future getAssociationLogo(String id) async { + final image = await associationLogoRepository.getAssociationLogo(id); + associationLogoListNotifier.setTData(id, AsyncData([image])); + return image; + } + + Future updateAssociationLogo(String id, Uint8List bytes) async { + associationLogoListNotifier.setTData(id, const AsyncLoading()); + final image = await associationLogoRepository.addAssociationLogo(bytes, id); + associationLogoListNotifier.setTData(id, AsyncData([image])); + return image; + } +} + +final associationLogoProvider = + StateNotifierProvider>((ref) { + final token = ref.watch(tokenProvider); + final associationLogoListNotifier = ref.watch( + associationLogoListProvider.notifier, + ); + return AssociationLogoNotifier( + token: token, + associationLogoListNotifier: associationLogoListNotifier, + ); + }); diff --git a/lib/admin/providers/association_logo_list.dart b/lib/admin/providers/association_logo_list.dart new file mode 100644 index 0000000000..0ac30face6 --- /dev/null +++ b/lib/admin/providers/association_logo_list.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/tools/providers/map_provider.dart'; + +class AssociationLogoListNotifier extends MapNotifier { + AssociationLogoListNotifier() : super(); +} + +final associationLogoListProvider = + StateNotifierProvider< + AssociationLogoListNotifier, + Map>?> + >((ref) { + AssociationLogoListNotifier associationLogoNotifier = + AssociationLogoListNotifier(); + return associationLogoNotifier; + }); diff --git a/lib/admin/providers/association_logo_provider.dart b/lib/admin/providers/association_logo_provider.dart new file mode 100644 index 0000000000..5dd5038e2b --- /dev/null +++ b/lib/admin/providers/association_logo_provider.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:titan/admin/providers/associations_logo_map_provider.dart'; +import 'package:titan/admin/repositories/association_logo_repository.dart'; +import 'package:titan/tools/providers/single_notifier.dart'; + +class AssociationLogoProvider extends SingleNotifier { + final AssociationLogoRepository associationLogoRepository; + final AssociationLogoMapNotifier associationLogoMapNotifier; + final ImagePicker _picker = ImagePicker(); + + AssociationLogoProvider({ + required this.associationLogoRepository, + required this.associationLogoMapNotifier, + }) : super(const AsyncLoading()); + + Future getAssociationLogo(String associationId) async { + final image = await associationLogoRepository.getAssociationLogo( + associationId, + ); + associationLogoMapNotifier.setTData(associationId, AsyncData([image])); + state = AsyncData(image); + return image; + } + + Future setLogo(ImageSource source, String associationId) async { + final previousState = state; + state = const AsyncLoading(); + final XFile? image = await _picker.pickImage( + source: source, + imageQuality: 20, + ); + if (image != null) { + try { + final i = await associationLogoRepository.addAssociationLogo( + await image.readAsBytes(), + associationId, + ); + state = AsyncValue.data(i); + associationLogoMapNotifier.setTData(associationId, AsyncData([i])); + return true; + } catch (e) { + state = previousState; + return false; + } + } + state = previousState; + return null; + } +} + +final associationLogoProvider = + StateNotifierProvider>((ref) { + final associationLogo = ref.watch(associationLogoRepository); + final sessionPosterMapNotifier = ref.watch( + associationLogoMapProvider.notifier, + ); + return AssociationLogoProvider( + associationLogoRepository: associationLogo, + associationLogoMapNotifier: sessionPosterMapNotifier, + ); + }); diff --git a/lib/admin/providers/associations_logo_map_provider.dart b/lib/admin/providers/associations_logo_map_provider.dart new file mode 100644 index 0000000000..504052649c --- /dev/null +++ b/lib/admin/providers/associations_logo_map_provider.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/tools/providers/map_provider.dart'; + +class AssociationLogoMapNotifier extends MapNotifier { + AssociationLogoMapNotifier() : super(); +} + +final associationLogoMapProvider = + StateNotifierProvider< + AssociationLogoMapNotifier, + Map>?> + >((ref) { + AssociationLogoMapNotifier associationLogoNotifier = + AssociationLogoMapNotifier(); + return associationLogoNotifier; + }); diff --git a/lib/admin/providers/group_from_simple_group_provider.dart b/lib/admin/providers/group_from_simple_group_provider.dart new file mode 100644 index 0000000000..2beac831dc --- /dev/null +++ b/lib/admin/providers/group_from_simple_group_provider.dart @@ -0,0 +1,27 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/admin/class/group.dart'; +import 'package:titan/admin/providers/group_list_provider.dart'; +import 'package:titan/tools/providers/single_map_provider.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; + +class GroupFromSimpleGroupNotifier extends SingleMapNotifier { + GroupFromSimpleGroupNotifier() : super(); +} + +final groupFromSimpleGroupProvider = + StateNotifierProvider< + GroupFromSimpleGroupNotifier, + Map?> + >((ref) { + GroupFromSimpleGroupNotifier groupFromSimpleGroupNotifier = + GroupFromSimpleGroupNotifier(); + tokenExpireWrapperAuth(ref, () async { + final simpleGroups = ref.watch(allGroupListProvider); + simpleGroups.whenData((value) { + groupFromSimpleGroupNotifier.loadTList( + value.map((e) => e.id).toList(), + ); + }); + }); + return groupFromSimpleGroupNotifier; + }); diff --git a/lib/admin/providers/my_association_list_provider.dart b/lib/admin/providers/my_association_list_provider.dart new file mode 100644 index 0000000000..1c269ff4d3 --- /dev/null +++ b/lib/admin/providers/my_association_list_provider.dart @@ -0,0 +1,38 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/admin/class/assocation.dart'; +import 'package:titan/admin/repositories/association_repository.dart'; +import 'package:titan/tools/providers/list_notifier.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; + +class MyAssociationListNotifier extends ListNotifier { + final AssociationRepository associationRepository; + MyAssociationListNotifier({required this.associationRepository}) + : super(const AsyncValue.loading()); + + Future>> loadAssociations() async { + return await loadList(associationRepository.getMyAssociations); + } +} + +final asyncMyAssociationListProvider = + StateNotifierProvider< + MyAssociationListNotifier, + AsyncValue> + >((ref) { + final associationRepository = ref.watch(associationRepositoryProvider); + MyAssociationListNotifier provider = MyAssociationListNotifier( + associationRepository: associationRepository, + ); + tokenExpireWrapperAuth(ref, () async { + await provider.loadAssociations(); + }); + return provider; + }); + +final myAssociationListProvider = Provider>((ref) { + final asyncMyAssociationList = ref.watch(asyncMyAssociationListProvider); + return asyncMyAssociationList.maybeWhen( + data: (associations) => associations, + orElse: () => [], + ); +}); diff --git a/lib/admin/providers/school_id_provider.dart b/lib/admin/providers/school_id_provider.dart deleted file mode 100644 index 306cf6d71f..0000000000 --- a/lib/admin/providers/school_id_provider.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -class SchoolIdNotifier extends StateNotifier { - SchoolIdNotifier() : super(""); - - void setId(String id) { - state = id; - } -} - -final schoolIdProvider = StateNotifierProvider( - (ref) => SchoolIdNotifier(), -); diff --git a/lib/admin/providers/structure_manager_provider.dart b/lib/admin/providers/structure_manager_provider.dart index 7153b20c5b..95fa9ea654 100644 --- a/lib/admin/providers/structure_manager_provider.dart +++ b/lib/admin/providers/structure_manager_provider.dart @@ -4,8 +4,8 @@ import 'package:titan/user/class/simple_users.dart'; class StructureManagerProvider extends StateNotifier { StructureManagerProvider() : super(SimpleUser.empty()); - void setUser(SimpleUser id) { - state = id; + void setUser(SimpleUser user) { + state = user; } } diff --git a/lib/admin/providers/structure_provider.dart b/lib/admin/providers/structure_provider.dart index d678a7bc75..1cd9308633 100644 --- a/lib/admin/providers/structure_provider.dart +++ b/lib/admin/providers/structure_provider.dart @@ -7,6 +7,10 @@ class StructureNotifier extends StateNotifier { void setStructure(Structure structure) { state = structure; } + + void resetStructure() { + state = Structure.empty(); + } } final structureProvider = StateNotifierProvider( diff --git a/lib/admin/providers/user_association_membership_list_provider.dart b/lib/admin/providers/user_association_membership_list_provider.dart deleted file mode 100644 index 9a6ad7875a..0000000000 --- a/lib/admin/providers/user_association_membership_list_provider.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/class/user_association_membership.dart'; -import 'package:titan/admin/repositories/association_membership_user_repository.dart'; -import 'package:titan/tools/providers/list_notifier.dart'; -import 'package:titan/tools/token_expire_wrapper.dart'; - -class UserMembershiplistNotifier - extends ListNotifier { - final AssociationMembershipUserRepository associationMembershipUserRepository; - UserMembershiplistNotifier({ - required this.associationMembershipUserRepository, - }) : super(const AsyncValue.loading()); - - Future>> - loadPersonalAssociationMembershipsList() async { - return await loadList( - () async => associationMembershipUserRepository - .getPersonalAssociationMembershipList(), - ); - } - - Future>> - loadUserAssociationMembershipsList(String userId) async { - return await loadList( - () async => associationMembershipUserRepository - .getUserAssociationMembershipList(userId), - ); - } -} - -final userMembershipListProvider = - StateNotifierProvider< - UserMembershiplistNotifier, - AsyncValue> - >((ref) { - final associationMembershipUserRepository = ref.watch( - associationMembershipUserRepositoryProvider, - ); - return UserMembershiplistNotifier( - associationMembershipUserRepository: - associationMembershipUserRepository, - ); - }); - -final myMembershipListProvider = - StateNotifierProvider< - UserMembershiplistNotifier, - AsyncValue> - >((ref) { - final associationMembershipUserRepository = ref.watch( - associationMembershipUserRepositoryProvider, - ); - UserMembershiplistNotifier provider = UserMembershiplistNotifier( - associationMembershipUserRepository: - associationMembershipUserRepository, - ); - tokenExpireWrapperAuth(ref, () async { - await provider.loadPersonalAssociationMembershipsList(); - }); - return provider; - }); diff --git a/lib/admin/providers/user_invitation_provider.dart b/lib/admin/providers/user_invitation_provider.dart new file mode 100644 index 0000000000..d750b71d24 --- /dev/null +++ b/lib/admin/providers/user_invitation_provider.dart @@ -0,0 +1,25 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/admin/repositories/user_invitation_repository.dart'; + +class UserInvitationNotifier extends StateNotifier { + final UserInvitationRepository userInvitationRepository; + UserInvitationNotifier({required this.userInvitationRepository}) + : super(null); + + Future> createUsers( + List mailList, + String? groupId, + ) async { + return await userInvitationRepository.createUsers(mailList, groupId); + } +} + +final userInvitationProvider = + StateNotifierProvider((ref) { + final userInvitationRepository = ref.watch( + userInvitationRepositoryProvider, + ); + return UserInvitationNotifier( + userInvitationRepository: userInvitationRepository, + ); + }); diff --git a/lib/admin/repositories/association_logo_repository.dart b/lib/admin/repositories/association_logo_repository.dart new file mode 100644 index 0000000000..dd3111aa49 --- /dev/null +++ b/lib/admin/repositories/association_logo_repository.dart @@ -0,0 +1,31 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/auth/providers/openid_provider.dart'; +import 'package:titan/tools/repository/logo_repository.dart'; + +class AssociationLogoRepository extends LogoRepository { + @override + // ignore: overridden_fields + final ext = "associations/"; + + Future getAssociationLogo(String id) async { + final uint8List = await getLogo("", suffix: "$id/logo"); + if (uint8List.isEmpty) { + return Image.asset("assets/images/vache.png", fit: BoxFit.cover); + } + return Image.memory(uint8List); + } + + Future addAssociationLogo(Uint8List bytes, String id) async { + final uint8List = await addLogo(bytes, "", suffix: "$id/logo"); + return Image.memory(uint8List); + } +} + +final associationLogoRepository = Provider((ref) { + final token = ref.watch(tokenProvider); + return AssociationLogoRepository()..setToken(token); +}); diff --git a/lib/admin/repositories/association_repository.dart b/lib/admin/repositories/association_repository.dart new file mode 100644 index 0000000000..aa24f9fcca --- /dev/null +++ b/lib/admin/repositories/association_repository.dart @@ -0,0 +1,39 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/admin/class/assocation.dart'; +import 'package:titan/auth/providers/openid_provider.dart'; +import 'package:titan/tools/repository/repository.dart'; + +class AssociationRepository extends Repository { + @override + // ignore: overridden_fields + final ext = "associations/"; + + Future> getAssociationList() async { + return List.from( + (await getList()).map((x) => Association.fromJson(x)), + ); + } + + Future> getMyAssociations() async { + return List.from( + (await getList(suffix: "me")).map((x) => Association.fromJson(x)), + ); + } + + Future deleteAssociation(String associationId) async { + return await delete(associationId); + } + + Future updateAssociation(Association association) async { + return await update(association.toJson(), association.id); + } + + Future createAssociation(Association association) async { + return Association.fromJson(await create(association.toJson())); + } +} + +final associationRepositoryProvider = Provider((ref) { + final token = ref.watch(tokenProvider); + return AssociationRepository()..setToken(token); +}); diff --git a/lib/admin/repositories/notification_repository.dart b/lib/admin/repositories/notification_repository.dart new file mode 100644 index 0000000000..e831bc5c51 --- /dev/null +++ b/lib/admin/repositories/notification_repository.dart @@ -0,0 +1,26 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/auth/providers/openid_provider.dart'; +import 'package:titan/tools/repository/repository.dart'; + +class NotificationRepository extends Repository { + @override + // ignore: overridden_fields + final ext = "notification/"; + + Future sendNotification( + String groupId, + String title, + String content, + ) async { + return await create({ + "group_id": groupId, + "title": title, + "content": content, + }, suffix: "send"); + } +} + +final notificationRepositoryProvider = Provider((ref) { + final token = ref.watch(tokenProvider); + return NotificationRepository()..setToken(token); +}); diff --git a/lib/admin/repositories/user_invitation_repository.dart b/lib/admin/repositories/user_invitation_repository.dart new file mode 100644 index 0000000000..f19e0f4de4 --- /dev/null +++ b/lib/admin/repositories/user_invitation_repository.dart @@ -0,0 +1,29 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/auth/providers/openid_provider.dart'; +import 'package:titan/tools/repository/repository.dart'; + +class UserInvitationRepository extends Repository { + @override + // ignore: overridden_fields + final ext = "users/"; + + Future> createUsers( + List mailList, + String? groupId, + ) async { + final json = mailList + .map((email) => {'email': email, "default_group_id": groupId}) + .toList(); + final result = (await create(json, suffix: "batch-invitation"))["failed"]; + List failedEmails = []; + for (var entry in result.entries) { + if (entry.value != "User already invited") failedEmails.add(entry.key); + } + return failedEmails; + } +} + +final userInvitationRepositoryProvider = Provider((ref) { + final token = ref.watch(tokenProvider); + return UserInvitationRepository()..setToken(token); +}); diff --git a/lib/admin/router.dart b/lib/admin/router.dart index b98e20e3e0..95fa2cbafb 100644 --- a/lib/admin/router.dart +++ b/lib/admin/router.dart @@ -1,33 +1,30 @@ +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:titan/admin/providers/is_admin_provider.dart'; -import 'package:titan/admin/ui/pages/groups/add_group_page/add_group_page.dart' - deferred as add_group_page; -import 'package:titan/admin/ui/pages/groups/add_loaner_page/add_loaner_page.dart' - deferred as add_loaner_page; -import 'package:titan/admin/ui/pages/edit_module_visibility/edit_module_visibility.dart' - deferred as edit_module_visibility; import 'package:titan/admin/ui/pages/groups/edit_group_page/edit_group_page.dart' deferred as edit_group_page; -import 'package:titan/admin/ui/pages/groups/group_page/group_page.dart' - deferred as group_page; -import 'package:titan/admin/ui/pages/memberships/add_edit_user_membership_page/add_edit_user_membership_page.dart' - deferred as add_edit_user_membership_page; -import 'package:titan/admin/ui/pages/memberships/association_membership_detail_page/association_membership_detail_page.dart' - deferred as association_membership_detail_page; -import 'package:titan/admin/ui/pages/memberships/association_membership_page/association_membership_page.dart' - deferred as association_membership_page; -import 'package:titan/admin/ui/pages/schools/school_page/school_page.dart' - deferred as school_page; -import 'package:titan/admin/ui/pages/schools/add_school_page/add_school_page.dart' - deferred as add_school_page; -import 'package:titan/admin/ui/pages/schools/edit_school_page/edit_school_page.dart' - deferred as edit_school_page; -import 'package:titan/admin/ui/pages/structure_page/structure_page.dart' - deferred as structure_page; -import 'package:titan/admin/ui/pages/add_edit_structure_page/add_edit_structure_page.dart' - deferred as add_edit_structure_page; import 'package:titan/admin/ui/pages/main_page/main_page.dart' deferred as main_page; +import 'package:titan/admin/ui/pages/groups/groups_page/groups_page.dart' + deferred as groups_page; +import 'package:titan/admin/ui/pages/users_management_page/users_management_page.dart' + deferred as users_managmement_page; +import 'package:titan/admin/ui/pages/group_notifification_page/group_notification_page.dart' + deferred as group_notification_page; +import 'package:titan/admin/ui/pages/structure_page/add_edit_structure_page/add_edit_structure_page.dart' + deferred as add_edit_structure_page; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/class/module.dart'; +import 'package:titan/admin/ui/pages/structure_page/structure_page.dart' + deferred as structure_page; +import 'package:titan/admin/ui/pages/membership/association_membership_page/association_membership_page.dart' + deferred as association_membership_page; +import 'package:titan/admin/ui/pages/membership/association_membership_detail_page/association_membership_detail_page.dart' + deferred as association_membership_detail_page; +import 'package:titan/admin/ui/pages/membership/add_edit_user_membership_page/add_edit_user_membership_page.dart' + deferred as add_edit_user_membership_page; +import 'package:titan/admin/ui/pages/association_page/association_page.dart' + deferred as association_page; import 'package:titan/tools/middlewares/admin_middleware.dart'; import 'package:titan/tools/middlewares/authenticated_middleware.dart'; import 'package:titan/tools/middlewares/deferred_middleware.dart'; @@ -35,21 +32,26 @@ import 'package:qlevar_router/qlevar_router.dart'; class AdminRouter { final Ref ref; + static const String structures = '/structures'; + static const String addEditStructure = '/add_edit_structure'; static const String root = '/admin'; - static const String groups = '/groups'; + static const String usersManagement = '/users_management'; + static const String usersGroups = '/users_groups'; + static const String groupNotification = '/group_notification'; static const String addGroup = '/add_group'; static const String editGroup = '/edit_group'; - static const String addLoaner = '/add_loaner'; - static const String schools = '/schools'; - static const String addSchool = '/add_school'; - static const String editSchool = '/edit_school'; - static const String structures = '/structures'; - static const String addEditStructure = '/add_edit_structure'; - static const String editModuleVisibility = '/edit_module_visibility'; static const String associationMemberships = '/association_memberships'; static const String detailAssociationMembership = '/detail_association_membership'; static const String addEditMember = '/add_edit_member'; + static const String association = '/association'; + static final Module module = Module( + getName: (context) => AppLocalizations.of(context)!.moduleAdmin, + getDescription: (context) => + AppLocalizations.of(context)!.moduleAdminDescription, + root: AdminRouter.root, + ); + AdminRouter(this.ref); QRoute route() => QRoute( @@ -61,90 +63,37 @@ class AdminRouter { AdminMiddleware(ref, isAdminProvider), DeferredLoadingMiddleware(main_page.loadLibrary), ], + pageType: QCustomPage( + transitionsBuilder: (_, animation, _, child) => + FadeTransition(opacity: animation, child: child), + ), children: [ QRoute( - path: groups, - builder: () => group_page.GroupsPage(), - middleware: [DeferredLoadingMiddleware(group_page.loadLibrary)], - children: [ - QRoute( - path: addGroup, - builder: () => add_group_page.AddGroupPage(), - middleware: [DeferredLoadingMiddleware(add_group_page.loadLibrary)], - ), - QRoute( - path: editGroup, - builder: () => edit_group_page.EditGroupPage(), - middleware: [ - DeferredLoadingMiddleware(edit_group_page.loadLibrary), - ], - ), - QRoute( - path: addLoaner, - builder: () => add_loaner_page.AddLoanerPage(), - middleware: [ - DeferredLoadingMiddleware(add_loaner_page.loadLibrary), - ], - ), - ], - ), - QRoute( - path: editModuleVisibility, - builder: () => edit_module_visibility.EditModulesVisibilityPage(), + path: usersManagement, + builder: () => users_managmement_page.UsersManagementPage(), middleware: [ - DeferredLoadingMiddleware(edit_module_visibility.loadLibrary), + DeferredLoadingMiddleware(users_managmement_page.loadLibrary), ], ), QRoute( - path: schools, - builder: () => school_page.SchoolsPage(), - middleware: [DeferredLoadingMiddleware(school_page.loadLibrary)], + path: usersGroups, + builder: () => groups_page.GroupsPage(), + middleware: [DeferredLoadingMiddleware(groups_page.loadLibrary)], children: [ QRoute( - path: addSchool, - builder: () => add_school_page.AddSchoolPage(), - middleware: [ - DeferredLoadingMiddleware(add_school_page.loadLibrary), - ], - ), - QRoute( - path: editSchool, - builder: () => edit_school_page.EditSchoolPage(), + path: editGroup, + builder: () => edit_group_page.EditGroupPage(), middleware: [ - DeferredLoadingMiddleware(edit_school_page.loadLibrary), + DeferredLoadingMiddleware(edit_group_page.loadLibrary), ], ), ], ), QRoute( - path: associationMemberships, - builder: () => association_membership_page.AssociationMembershipsPage(), + path: groupNotification, + builder: () => group_notification_page.GroupNotificationPage(), middleware: [ - DeferredLoadingMiddleware(association_membership_page.loadLibrary), - ], - children: [ - QRoute( - path: detailAssociationMembership, - builder: () => - association_membership_detail_page.AssociationMembershipEditorPage(), - middleware: [ - DeferredLoadingMiddleware( - association_membership_detail_page.loadLibrary, - ), - ], - children: [ - QRoute( - path: addEditMember, - builder: () => - add_edit_user_membership_page.AddEditUserMembershipPage(), - middleware: [ - DeferredLoadingMiddleware( - add_edit_user_membership_page.loadLibrary, - ), - ], - ), - ], - ), + DeferredLoadingMiddleware(group_notification_page.loadLibrary), ], ), QRoute( @@ -192,6 +141,11 @@ class AdminRouter { ), ], ), + QRoute( + path: association, + builder: () => association_page.AssociationPage(), + middleware: [DeferredLoadingMiddleware(association_page.loadLibrary)], + ), ], ); } diff --git a/lib/admin/tools/constants.dart b/lib/admin/tools/constants.dart deleted file mode 100644 index aa9eef2746..0000000000 --- a/lib/admin/tools/constants.dart +++ /dev/null @@ -1,92 +0,0 @@ -class AdminTextConstants { - static const String accountTypes = "Types de compte"; - static const String add = "Ajouter"; - static const String addGroup = "Ajouter un groupe"; - static const String addMember = "Ajouter un membre"; - static const String addedGroup = "Groupe créé"; - static const String addedLoaner = "Préteur ajouté"; - static const String addedMember = "Membre ajouté"; - static const String addingError = "Erreur lors de l'ajout"; - static const String addingMember = "Ajout d'un membre"; - static const String addLoaningGroup = "Ajouter un groupe de prêt"; - static const String addSchool = "Ajouter une école"; - static const String addStructure = "Ajouter une structure"; - static const String addedSchool = "École créée"; - static const String addedStructure = "Structure ajoutée"; - static const String editedStructure = "Structure modifiée"; - static const String administration = "Administration"; - static const String associationMembership = "Adhésion"; - static const String associationMembershipName = "Nom de l'adhésion"; - static const String associationsMemberships = "Adhésions"; - static const String clearFilters = "Effacer les filtres"; - static const String createAssociationMembership = "Créer une adhésion"; - static const String createdAssociationMembership = "Adhésion créée"; - static const String creationError = "Erreur lors de la création"; - static const String dateError = - "La date de début doit être avant la date de fin"; - static const String delete = "Supprimer"; - static const String deleteAssociationMembership = "Supprimer l'adhésion ?"; - static const String deletedAssociationMembership = "Adhésion supprimée"; - static const String deleteGroup = "Supprimer le groupe ?"; - static const String deletedGroup = "Groupe supprimé"; - static const String deleteSchool = "Supprimer l'école ?"; - static const String deletedSchool = "École supprimée"; - static const String deleting = "Suppression"; - static const String deletingError = "Erreur lors de la suppression"; - static const String description = "Description"; - static const String eclSchool = "Centrale Lyon"; - static const String edit = "Modifier"; - static const String editStructure = "Modifier la structure"; - static const String editMembership = "Modifier l'adhésion"; - static const String emptyDate = "Date vide"; - static const String emptyFieldError = "Le nom ne peut pas être vide"; - static const String emailRegex = "Email Regex"; - static const String emptyUser = "Utilisateur vide"; - static const String endDate = "Date de fin"; - static const String endDateMaximal = "Date de fin maximale"; - static const String endDateMinimal = "Date de fin minimale"; - static const String error = "Erreur"; - static const String filters = "Filtres"; - static const String group = "Groupe"; - static const String groups = "Groupes"; - static const String loaningGroup = "Groupe de prêt"; - static const String looking = "Recherche"; - static const String manager = "Administrateur de la structure"; - static const String maximum = "Maximum"; - static const String members = "Membres"; - static const String membershipAddingError = - "Erreur lors de l'ajout (surement dû à une superposition de dates)"; - static const String memberships = "Adhésions"; - static String membershipUpdatingError = - "Erreur lors de la modification (surement dû à une superposition de dates)"; - static const String minimum = "Minimum"; - static const String modifyModuleVisibility = "Visibilité des modules"; - static const String myEclPay = "MyECLPay"; - static const String name = "Nom"; - static const String noManager = "Aucun manager n'est sélectionné"; - static const String noMember = "Aucun membre"; - static const String noMoreLoaner = "Aucun prêteur n'est disponible"; - static const String noSchool = "Sans école"; - static const String removeGroupMember = "Supprimer le membre du groupe ?"; - static const String research = "Recherche"; - static const String schools = "Écoles"; - static const String structures = "Structures"; - static const String startDate = "Date de début"; - static const String startDateMaximal = "Date de début maximale"; - static const String startDateMinimal = "Date de début minimale"; - static const String updatedAssociationMembership = "Adhésion modifiée"; - static const String updatedGroup = "Groupe modifié"; - static const String updatedMembership = "Adhésion modifiée"; - static const String updatingError = "Erreur lors de la modification"; - static const String user = "Utilisateur"; - static const String validateFilters = "Valider les filtres"; - static const String visibilities = "Visibilités"; -} - -enum SchoolIdConstant { - noSchool("dce19aa2-8863-4c93-861e-fb7be8f610ed"), - eclSchool("d9772da7-1142-4002-8b86-b694b431dfed"); - - const SchoolIdConstant(this.value); - final String value; -} diff --git a/lib/admin/tools/function.dart b/lib/admin/tools/function.dart deleted file mode 100644 index dbeff77c60..0000000000 --- a/lib/admin/tools/function.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:titan/admin/tools/constants.dart'; - -String getSchoolNameFromId(String id, String name) { - if (id == SchoolIdConstant.noSchool.value) { - return AdminTextConstants.noSchool; - } - if (id == SchoolIdConstant.eclSchool.value) { - return AdminTextConstants.eclSchool; - } - return name; -} diff --git a/lib/admin/tools/functions.dart b/lib/admin/tools/functions.dart new file mode 100644 index 0000000000..4c927c6c6d --- /dev/null +++ b/lib/admin/tools/functions.dart @@ -0,0 +1,38 @@ +List parseCsvContent(String content) { + if (content.isEmpty) return []; + + final separators = [',', ';', '\t', '|']; + + // Simple heuristic: count occurrences of each separator in the entire content + String bestSeparator = ','; + int maxOccurrences = 0; + + for (final separator in separators) { + final occurrences = separator.allMatches(content).length; + + if (occurrences > maxOccurrences) { + maxOccurrences = occurrences; + bestSeparator = separator; + } + } + + final lines = content.split('\n').where((line) => line.trim().isNotEmpty); + final result = []; + + for (final line in lines) { + final fields = line + .split(bestSeparator) + .map((field) => field.trim()) + .where((field) => field.isNotEmpty && _isValidEmail(field)); + result.addAll(fields); + } + + return result.toSet().toList(); +} + +bool _isValidEmail(String email) { + final emailRegex = RegExp( + r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', + ); + return emailRegex.hasMatch(email.trim()); +} diff --git a/lib/admin/ui/admin.dart b/lib/admin/ui/admin.dart deleted file mode 100644 index 4d40551181..0000000000 --- a/lib/admin/ui/admin.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/router.dart'; -import 'package:titan/admin/tools/constants.dart'; -import 'package:titan/tools/token_expire_wrapper.dart'; -import 'package:titan/tools/ui/widgets/top_bar.dart'; -import 'package:titan/user/providers/user_provider.dart'; - -class AdminTemplate extends HookConsumerWidget { - final Widget child; - const AdminTemplate({super.key, required this.child}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final meNotifier = ref.watch(asyncUserProvider.notifier); - return Scaffold( - body: Container( - decoration: const BoxDecoration(color: Colors.white), - child: SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - TopBar( - title: AdminTextConstants.administration, - root: AdminRouter.root, - onMenu: () { - tokenExpireWrapper(ref, () async { - await meNotifier.loadMe(); - }); - }, - ), - Expanded(child: child), - ], - ), - ), - ), - ); - } -} diff --git a/lib/admin/ui/components/user_ui.dart b/lib/admin/ui/components/user_ui.dart index d7ca60174d..681e426bb9 100644 --- a/lib/admin/ui/components/user_ui.dart +++ b/lib/admin/ui/components/user_ui.dart @@ -29,11 +29,7 @@ class UserUi extends HookConsumerWidget { child: Container( padding: const EdgeInsets.all(7), decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [ColorConstants.background2, Colors.black], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), + color: ColorConstants.onMain, boxShadow: [ BoxShadow( color: ColorConstants.background2.withValues(alpha: 0.4), diff --git a/lib/admin/ui/pages/add_edit_structure_page/add_edit_structure_page.dart b/lib/admin/ui/pages/add_edit_structure_page/add_edit_structure_page.dart deleted file mode 100644 index c9fb207b80..0000000000 --- a/lib/admin/ui/pages/add_edit_structure_page/add_edit_structure_page.dart +++ /dev/null @@ -1,195 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/class/association_membership_simple.dart'; -import 'package:titan/admin/providers/association_membership_list_provider.dart'; -import 'package:titan/admin/providers/structure_manager_provider.dart'; -import 'package:titan/admin/providers/structure_provider.dart'; -import 'package:titan/admin/tools/constants.dart'; -import 'package:titan/admin/ui/admin.dart'; -import 'package:titan/admin/ui/components/admin_button.dart'; -import 'package:titan/admin/ui/components/text_editing.dart'; -import 'package:titan/admin/ui/pages/add_edit_structure_page/search_user.dart'; -import 'package:titan/paiement/class/structure.dart'; -import 'package:titan/paiement/providers/structure_list_provider.dart'; -import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:titan/tools/token_expire_wrapper.dart'; -import 'package:titan/tools/ui/builders/async_child.dart'; -import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; -import 'package:titan/tools/ui/layouts/item_chip.dart'; -import 'package:titan/tools/ui/widgets/align_left_text.dart'; -import 'package:titan/tools/ui/builders/waiting_button.dart'; -import 'package:titan/user/class/simple_users.dart'; -import 'package:qlevar_router/qlevar_router.dart'; - -class AddEditStructurePage extends HookConsumerWidget { - const AddEditStructurePage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final key = GlobalKey(); - final structure = ref.watch(structureProvider); - final structureManager = ref.watch(structureManagerProvider); - final structureManagerNotifier = ref.watch( - structureManagerProvider.notifier, - ); - final structureListNotifier = ref.watch(structureListProvider.notifier); - final isEdit = structure.id != ''; - final name = useTextEditingController(text: isEdit ? structure.name : null); - final allAssociationMembershipList = ref.watch( - allAssociationMembershipListProvider, - ); - final currentMembership = useState( - (isEdit) - ? structure.associationMembership - : AssociationMembership.empty(), - ); - void displayToastWithContext(TypeMsg type, String msg) { - displayToast(context, type, msg); - } - - return AdminTemplate( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 30.0), - child: SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics( - parent: BouncingScrollPhysics(), - ), - child: Form( - key: key, - child: Column( - children: [ - const SizedBox(height: 20), - AlignLeftText( - isEdit - ? AdminTextConstants.editStructure - : AdminTextConstants.addStructure, - ), - const SizedBox(height: 20), - TextEditing(controller: name, label: AdminTextConstants.name), - AsyncChild( - value: allAssociationMembershipList, - builder: (context, allAssociationMembershipList) { - return HorizontalListView.builder( - height: 40, - items: [ - ...allAssociationMembershipList, - AssociationMembership.empty(), - ], - itemBuilder: (context, associationMembership, index) { - final selected = - currentMembership.value.id == - associationMembership.id; - return ItemChip( - selected: selected, - onTap: () async { - currentMembership.value = associationMembership; - }, - child: Text( - associationMembership.name.toUpperCase(), - style: TextStyle( - color: selected ? Colors.white : Colors.black, - fontWeight: FontWeight.bold, - ), - ), - ); - }, - ); - }, - ), - const SizedBox(height: 20), - isEdit - ? Column( - children: [ - Text( - AdminTextConstants.manager, - style: TextStyle( - color: ColorConstants.gradient1, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 10), - Text( - structureManager.getName(), - style: TextStyle( - color: ColorConstants.gradient1, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 10), - ], - ) - : const SearchUser(), - const SizedBox(height: 20), - WaitingButton( - onTap: () async { - if (key.currentState == null) { - return; - } - if (structureManager.id.isEmpty && !isEdit) { - displayToastWithContext( - TypeMsg.error, - AdminTextConstants.noManager, - ); - return; - } - if (key.currentState!.validate()) { - await tokenExpireWrapper(ref, () async { - final value = isEdit - ? await structureListNotifier.updateStructure( - Structure( - name: name.text, - associationMembership: - currentMembership.value, - managerUser: structureManager, - id: structure.id, - ), - ) - : await structureListNotifier.createStructure( - Structure( - name: name.text, - associationMembership: - currentMembership.value, - managerUser: structureManager, - id: '', - ), - ); - if (value) { - QR.back(); - structureManagerNotifier.setUser(SimpleUser.empty()); - displayToastWithContext( - TypeMsg.msg, - isEdit - ? AdminTextConstants.editedStructure - : AdminTextConstants.addedStructure, - ); - } else { - displayToastWithContext( - TypeMsg.error, - AdminTextConstants.addingError, - ); - } - }); - } - }, - builder: (child) => AdminButton(child: child), - child: Text( - isEdit ? AdminTextConstants.edit : AdminTextConstants.add, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/admin/ui/pages/add_edit_structure_page/results.dart b/lib/admin/ui/pages/add_edit_structure_page/results.dart deleted file mode 100644 index 351f91eea7..0000000000 --- a/lib/admin/ui/pages/add_edit_structure_page/results.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/providers/structure_manager_provider.dart'; -import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/ui/builders/async_child.dart'; -import 'package:titan/tools/ui/builders/waiting_button.dart'; -import 'package:titan/user/providers/user_list_provider.dart'; - -class MemberResults extends HookConsumerWidget { - const MemberResults({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final users = ref.watch(userList); - final usersNotifier = ref.watch(userList.notifier); - final structureManagerNotifier = ref.watch( - structureManagerProvider.notifier, - ); - - return AsyncChild( - value: users, - builder: (context, value) => Column( - children: value - .map( - (e) => Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - e.getName(), - style: const TextStyle(fontSize: 15), - overflow: TextOverflow.ellipsis, - ), - ), - Row( - children: [ - WaitingButton( - onTap: () async { - structureManagerNotifier.setUser(e); - usersNotifier.clear(); - // TODO: Confirmation dialog - }, - waitingColor: ColorConstants.gradient1, - builder: (child) => child, - child: const HeroIcon(HeroIcons.plus), - ), - ], - ), - ], - ), - ), - ) - .toList(), - ), - loaderColor: ColorConstants.gradient1, - ); - } -} diff --git a/lib/admin/ui/pages/add_edit_structure_page/search_user.dart b/lib/admin/ui/pages/add_edit_structure_page/search_user.dart deleted file mode 100644 index 688a12cd00..0000000000 --- a/lib/admin/ui/pages/add_edit_structure_page/search_user.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/providers/structure_manager_provider.dart'; -import 'package:titan/admin/tools/constants.dart'; -import 'package:titan/admin/ui/pages/add_edit_structure_page/results.dart'; -import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/ui/widgets/styled_search_bar.dart'; -import 'package:titan/user/class/simple_users.dart'; -import 'package:titan/user/providers/user_list_provider.dart'; - -class SearchUser extends HookConsumerWidget { - const SearchUser({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final usersNotifier = ref.watch(userList.notifier); - final structureManager = ref.watch(structureManagerProvider); - - return Column( - children: [ - StyledSearchBar( - label: AdminTextConstants.manager, - color: ColorConstants.gradient1, - padding: const EdgeInsets.all(0), - editingController: useTextEditingController( - text: structureManager.id == SimpleUser.empty().id - ? "" - : structureManager.getName(), - ), - onChanged: (value) async { - if (value.isNotEmpty) { - await usersNotifier.filterUsers(value); - } else { - usersNotifier.clear(); - } - }, - suffixIcon: Padding( - padding: const EdgeInsets.all(7.0), - child: Container( - padding: const EdgeInsets.all(7), - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [ColorConstants.gradient1, ColorConstants.gradient2], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - boxShadow: [ - BoxShadow( - color: ColorConstants.gradient2.withValues(alpha: 0.4), - offset: const Offset(2, 3), - blurRadius: 5, - ), - ], - borderRadius: const BorderRadius.all(Radius.circular(10)), - ), - child: HeroIcon(HeroIcons.plus, size: 20, color: Colors.white), - ), - ), - ), - const SizedBox(height: 10), - const MemberResults(), - ], - ); - } -} diff --git a/lib/admin/ui/pages/association_page/add_association_modal.dart b/lib/admin/ui/pages/association_page/add_association_modal.dart new file mode 100644 index 0000000000..1ac2b309c8 --- /dev/null +++ b/lib/admin/ui/pages/association_page/add_association_modal.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:titan/admin/class/simple_group.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; +import 'package:titan/tools/ui/styleguide/list_item.dart'; +import 'package:titan/tools/ui/styleguide/list_item_template.dart'; +import 'package:titan/tools/ui/widgets/text_entry.dart'; + +class AddAssociationModal extends HookWidget { + final List groups; + final void Function(SimpleGroup group, String name) onSubmit; + final WidgetRef ref; + + const AddAssociationModal({ + super.key, + required this.groups, + required this.onSubmit, + required this.ref, + }); + + @override + Widget build(BuildContext context) { + final nameController = useTextEditingController(); + final chosenGroup = useState(null); + + final localizeWithContext = AppLocalizations.of(context)!; + + return BottomModalTemplate( + title: localizeWithContext.adminAddAssociation, + child: SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextEntry( + label: localizeWithContext.adminAssociationName, + controller: nameController, + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: chosenGroup.value == null + ? ListItem( + title: localizeWithContext + .adminChooseAssociationManagerGroup, + onTap: () async { + FocusScope.of(context).unfocus(); + final ctx = context; + await Future.delayed(Duration(milliseconds: 150)); + if (!ctx.mounted) return; + + await showCustomBottomModal( + context: ctx, + ref: ref, + modal: BottomModalTemplate( + title: localizeWithContext.adminChooseGroup, + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 600), + child: SingleChildScrollView( + physics: BouncingScrollPhysics(), + child: Column( + children: [ + ...groups.map( + (e) => ListItemTemplate( + title: e.name, + trailing: const HeroIcon( + HeroIcons.plus, + ), + onTap: () { + chosenGroup.value = e; + Navigator.of(ctx).pop(); + }, + ), + ), + ], + ), + ), + ), + ), + ); + }, + ) + : ListItem( + title: localizeWithContext.adminManagerGroup( + chosenGroup.value!.name, + ), + onTap: () async { + FocusScope.of(context).unfocus(); + final ctx = context; + await Future.delayed(Duration(milliseconds: 150)); + if (!ctx.mounted) return; + + await showCustomBottomModal( + context: ctx, + ref: ref, + modal: BottomModalTemplate( + title: localizeWithContext.adminChooseGroup, + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 600), + child: SingleChildScrollView( + child: Column( + children: [ + ...groups.map( + (e) => ListItemTemplate( + title: e.name, + trailing: const HeroIcon( + HeroIcons.plus, + ), + onTap: () { + chosenGroup.value = e; + Navigator.of(ctx).pop(); + }, + ), + ), + ], + ), + ), + ), + ), + ); + }, + ), + ), + const SizedBox(height: 10), + Button( + text: localizeWithContext.adminAdd, + onPressed: () { + if (chosenGroup.value != null) { + onSubmit(chosenGroup.value!, nameController.text); + } + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/admin/ui/pages/association_page/association_item.dart b/lib/admin/ui/pages/association_page/association_item.dart new file mode 100644 index 0000000000..08b0a8267c --- /dev/null +++ b/lib/admin/ui/pages/association_page/association_item.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/admin/class/assocation.dart'; +import 'package:titan/admin/providers/all_groups_list_provider.dart'; +import 'package:titan/admin/providers/association_logo_provider.dart'; +import 'package:titan/admin/providers/associations_logo_map_provider.dart'; +import 'package:titan/admin/ui/pages/association_page/edit_association.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/ui/builders/auto_loader_child.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/list_item.dart'; + +class AssociationItem extends HookConsumerWidget { + const AssociationItem({super.key, required this.association}); + + final Association association; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final groups = ref.watch(allGroupList); + final group = groups.firstWhere((group) => group.id == association.groupId); + final associationLogo = ref.watch( + associationLogoMapProvider.select((value) => value[association.id]), + ); + final associationLogoMapNotifier = ref.watch( + associationLogoMapProvider.notifier, + ); + final associationLogoNotifier = ref.watch(associationLogoProvider.notifier); + + final localizeWithContext = AppLocalizations.of(context)!; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: ListItem( + title: association.name, + subtitle: localizeWithContext.adminManagerGroup(group.name), + icon: AutoLoaderChild( + group: associationLogo, + notifier: associationLogoMapNotifier, + mapKey: association.id, + loader: (associationId) => + associationLogoNotifier.getAssociationLogo(associationId), + dataBuilder: (context, data) { + return CircleAvatar( + radius: 20, + backgroundColor: Colors.white, + backgroundImage: Image(image: data.first.image).image, + ); + }, + ), + onTap: () async { + associationLogoNotifier.getAssociationLogo(association.id); + await showCustomBottomModal( + context: context, + ref: ref, + modal: BottomModalTemplate( + title: localizeWithContext.adminEditAssociation(association.name), + child: EditAssociation(association: association, group: group), + ), + ); + }, + ), + ); + } +} diff --git a/lib/admin/ui/pages/association_page/association_page.dart b/lib/admin/ui/pages/association_page/association_page.dart new file mode 100644 index 0000000000..01b83c84a7 --- /dev/null +++ b/lib/admin/ui/pages/association_page/association_page.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:titan/admin/admin.dart'; +import 'package:titan/admin/class/assocation.dart'; +import 'package:titan/admin/providers/all_groups_list_provider.dart'; +import 'package:titan/admin/providers/assocation_list_provider.dart'; +import 'package:titan/admin/ui/pages/association_page/add_association_modal.dart'; +import 'package:titan/admin/ui/pages/association_page/association_item.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; +import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/tools/ui/layouts/refresher.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/icon_button.dart'; + +class AssociationPage extends ConsumerWidget { + const AssociationPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final associationList = ref.watch(associationListProvider); + final associationNotifier = ref.watch(associationListProvider.notifier); + final groups = ref.watch(allGroupList); + void displayToastWithContext(TypeMsg type, String msg) { + displayToast(context, type, msg); + } + + void popWithContext() { + Navigator.of(context).pop(); + } + + final localizeWithContext = AppLocalizations.of(context)!; + + return AdminTemplate( + child: Refresher( + controller: ScrollController(), + onRefresh: () async { + await associationNotifier.loadAssociations(); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + const SizedBox(height: 20), + Row( + children: [ + Text( + localizeWithContext.adminAssociations, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorConstants.title, + ), + ), + const Spacer(), + CustomIconButton( + icon: HeroIcon( + HeroIcons.plus, + color: Colors.white, + size: 30, + ), + onPressed: () async { + await showCustomBottomModal( + context: context, + ref: ref, + modal: AddAssociationModal( + groups: groups, + onSubmit: (group, name) { + tokenExpireWrapper(ref, () async { + final value = await associationNotifier + .createAssociation( + Association.empty().copyWith( + groupId: group.id, + name: name, + ), + ); + if (value) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.adminAssociationCreated, + ); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext + .adminAssociationCreationError, + ); + } + popWithContext(); + }); + }, + ref: ref, + ), + ); + }, + ), + ], + ), + SizedBox(height: 10), + AsyncChild( + value: associationList, + builder: (BuildContext context, associationList) { + return Column( + children: [ + ...associationList.map( + (association) => + AssociationItem(association: association), + ), + ], + ); + }, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/admin/ui/pages/association_page/edit_association.dart b/lib/admin/ui/pages/association_page/edit_association.dart new file mode 100644 index 0000000000..228730ccc1 --- /dev/null +++ b/lib/admin/ui/pages/association_page/edit_association.dart @@ -0,0 +1,226 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:titan/admin/class/assocation.dart'; +import 'package:titan/admin/class/simple_group.dart'; +import 'package:titan/admin/providers/all_groups_list_provider.dart'; +import 'package:titan/admin/providers/assocation_list_provider.dart'; +import 'package:titan/admin/providers/association_logo_provider.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/settings/ui/pages/main_page/picture_button.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; +import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; +import 'package:titan/tools/ui/styleguide/list_item.dart'; +import 'package:titan/tools/ui/styleguide/text_entry.dart'; + +class EditAssociation extends HookConsumerWidget { + final Association association; + final SimpleGroup group; + const EditAssociation({ + super.key, + required this.association, + required this.group, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final associationListNotifier = ref.watch(associationListProvider.notifier); + final nameController = useTextEditingController(text: association.name); + final groups = ref.watch(allGroupList); + final chosenGroup = useState(group); + final associationLogo = ref.watch(associationLogoProvider); + final associationLogoNotifier = ref.watch(associationLogoProvider.notifier); + + MediaQuery.of(context).viewInsets.bottom; + + void displayToastWithContext(TypeMsg type, String msg) { + displayToast(context, type, msg); + } + + final localizeWithContext = AppLocalizations.of(context)!; + final navigatorWithContext = Navigator.of(context); + + return SingleChildScrollView( + child: Column( + children: [ + if (View.of(context).viewInsets.bottom == 0) + AsyncChild( + value: associationLogo, + builder: (context, associationLogo) { + return Center( + child: Stack( + clipBehavior: Clip.none, + children: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + spreadRadius: 5, + blurRadius: 10, + offset: const Offset(2, 3), + ), + ], + ), + child: CircleAvatar( + radius: 80, + backgroundImage: Image( + image: associationLogo.image, + ).image, + ), + ), + Positioned( + bottom: 0, + left: 0, + child: GestureDetector( + onTap: () async { + final value = await associationLogoNotifier.setLogo( + ImageSource.gallery, + association.id, + ); + if (value != null) { + if (value) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext + .adminUpdatedAssociationLogo, + ); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.adminTooHeavyLogo, + ); + } + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext + .adminFailedToUpdateAssociationLogo, + ); + } + }, + child: const PictureButton(icon: HeroIcons.photo), + ), + ), + Positioned( + bottom: 0, + right: 0, + child: GestureDetector( + onTap: () async { + final value = await associationLogoNotifier.setLogo( + ImageSource.camera, + association.id, + ); + if (value != null) { + if (value) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext + .adminUpdatedAssociationLogo, + ); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.adminTooHeavyLogo, + ); + } + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext + .adminFailedToUpdateAssociationLogo, + ); + } + }, + child: const PictureButton(icon: HeroIcons.camera), + ), + ), + ], + ), + ); + }, + ), + SizedBox(height: View.of(context).viewInsets.bottom == 0 ? 30 : 10), + TextEntry( + label: localizeWithContext.adminAssociationName, + controller: nameController, + ), + SizedBox(height: 20), + ListItem( + title: localizeWithContext.adminManagerGroup( + chosenGroup.value!.name, + ), + onTap: () async { + FocusScope.of(context).unfocus(); + final ctx = context; + await Future.delayed(Duration(milliseconds: 150)); + if (!ctx.mounted) return; + + await showCustomBottomModal( + context: ctx, + ref: ref, + modal: BottomModalTemplate( + title: localizeWithContext.adminChooseGroup, + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: 600), + child: SingleChildScrollView( + child: Column( + children: [ + ...groups.map( + (e) => ListItem( + title: e.name, + onTap: () { + chosenGroup.value = e; + Navigator.of(ctx).pop(); + }, + ), + ), + ], + ), + ), + ), + ), + ); + }, + ), + SizedBox(height: 20), + Button( + text: localizeWithContext.globalConfirm, + disabled: + !(nameController.value.text != association.name || + chosenGroup.value!.id != association.groupId), + onPressed: () async { + await tokenExpireWrapper(ref, () async { + final newAssociation = association.copyWith( + name: nameController.value.text, + groupId: chosenGroup.value!.id, + ); + final value = await associationListNotifier.updateAssociation( + newAssociation, + ); + if (value) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.adminAssociationUpdated, + ); + navigatorWithContext.pop(); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.adminAssociationUpdateError, + ); + } + }); + }, + ), + ], + ), + ); + } +} diff --git a/lib/admin/ui/pages/group_notifification_page/group_notification_page.dart b/lib/admin/ui/pages/group_notifification_page/group_notification_page.dart new file mode 100644 index 0000000000..d1fb25598f --- /dev/null +++ b/lib/admin/ui/pages/group_notifification_page/group_notification_page.dart @@ -0,0 +1,155 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/admin/admin.dart'; +import 'package:titan/admin/providers/group_list_provider.dart'; +import 'package:titan/admin/repositories/notification_repository.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/tools/ui/layouts/refresher.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; +import 'package:titan/tools/ui/styleguide/list_item.dart'; +import 'package:titan/tools/ui/styleguide/text_entry.dart'; + +class GroupNotificationPage extends HookConsumerWidget { + const GroupNotificationPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final groups = ref.watch(allGroupListProvider); + final groupsNotifier = ref.watch(allGroupListProvider.notifier); + final notificationRepository = ref.watch(notificationRepositoryProvider); + final titleController = useTextEditingController(); + final contentController = useTextEditingController(); + void displayToastWithContext(TypeMsg type, String msg) { + displayToast(context, type, msg); + } + + final localizeWithContext = AppLocalizations.of(context)!; + return AdminTemplate( + child: Refresher( + controller: ScrollController(), + onRefresh: () async { + await groupsNotifier.loadGroups(); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Column( + children: [ + const SizedBox(height: 20), + Align( + alignment: Alignment.centerLeft, + child: Text( + localizeWithContext.adminGroupNotification, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorConstants.title, + ), + ), + ), + const SizedBox(height: 10), + AsyncChild( + value: groups, + builder: (context, g) { + g.sort( + (a, b) => + a.name.toLowerCase().compareTo(b.name.toLowerCase()), + ); + return Column( + children: [ + Column( + children: [ + ...g.map( + (group) => Padding( + padding: const EdgeInsets.symmetric( + vertical: 5.0, + ), + child: ListItem( + title: group.name, + subtitle: group.description, + onTap: () async { + await showCustomBottomModal( + context: context, + ref: ref, + modal: BottomModalTemplate( + title: localizeWithContext + .adminNotifyGroup(group.name), + child: Column( + children: [ + TextEntry( + label: + localizeWithContext.adminTitle, + controller: titleController, + keyboardType: + TextInputType.multiline, + ), + const SizedBox(height: 20), + TextEntry( + label: localizeWithContext + .adminContent, + controller: contentController, + keyboardType: + TextInputType.multiline, + ), + const SizedBox(height: 20), + Button( + text: localizeWithContext.adminSend, + onPressed: () { + notificationRepository + .sendNotification( + group.id, + titleController.text, + contentController.text, + ) + .then((value) { + if (value) { + QR.back(); + displayToastWithContext( + TypeMsg.msg, + localizeWithContext + .adminNotificationSent, + ); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext + .adminFailedToSendNotification, + ); + } + }) + .catchError((error) { + displayToastWithContext( + TypeMsg.error, + error.toString(), + ); + }); + }, + ), + ], + ), + ), + ); + }, + ), + ), + ), + const SizedBox(height: 20), + ], + ), + ], + ); + }, + loaderColor: ColorConstants.main, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/admin/ui/pages/groups/add_group_page/add_group_page.dart b/lib/admin/ui/pages/groups/add_group_page/add_group_page.dart deleted file mode 100644 index a42957311a..0000000000 --- a/lib/admin/ui/pages/groups/add_group_page/add_group_page.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/class/simple_group.dart'; -import 'package:titan/admin/providers/group_list_provider.dart'; -import 'package:titan/admin/tools/constants.dart'; -import 'package:titan/admin/ui/admin.dart'; -import 'package:titan/admin/ui/components/admin_button.dart'; -import 'package:titan/admin/ui/components/text_editing.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:titan/tools/token_expire_wrapper.dart'; -import 'package:titan/tools/ui/widgets/align_left_text.dart'; -import 'package:titan/tools/ui/builders/waiting_button.dart'; -import 'package:qlevar_router/qlevar_router.dart'; - -class AddGroupPage extends HookConsumerWidget { - const AddGroupPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final key = GlobalKey(); - final name = useTextEditingController(); - final description = useTextEditingController(); - final groupListNotifier = ref.watch(allGroupListProvider.notifier); - void displayToastWithContext(TypeMsg type, String msg) { - displayToast(context, type, msg); - } - - return AdminTemplate( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 30.0), - child: SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics( - parent: BouncingScrollPhysics(), - ), - child: Form( - key: key, - child: Column( - children: [ - const AlignLeftText(AdminTextConstants.addGroup), - const SizedBox(height: 30), - TextEditing(controller: name, label: AdminTextConstants.name), - TextEditing( - controller: description, - label: AdminTextConstants.description, - ), - WaitingButton( - onTap: () async { - await tokenExpireWrapper(ref, () async { - final value = await groupListNotifier.createGroup( - SimpleGroup( - name: name.text, - description: description.text, - id: '', - ), - ); - if (value) { - QR.back(); - displayToastWithContext( - TypeMsg.msg, - AdminTextConstants.addedGroup, - ); - } else { - displayToastWithContext( - TypeMsg.error, - AdminTextConstants.addingError, - ); - } - }); - }, - builder: (child) => AdminButton(child: child), - child: const Text( - AdminTextConstants.add, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/admin/ui/pages/groups/add_loaner_page/add_loaner_page.dart b/lib/admin/ui/pages/groups/add_loaner_page/add_loaner_page.dart deleted file mode 100644 index 058a9e6216..0000000000 --- a/lib/admin/ui/pages/groups/add_loaner_page/add_loaner_page.dart +++ /dev/null @@ -1,119 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/providers/group_list_provider.dart'; -import 'package:titan/admin/tools/constants.dart'; -import 'package:titan/admin/ui/admin.dart'; -import 'package:titan/loan/class/loaner.dart'; -import 'package:titan/loan/providers/all_loaner_list_provider.dart'; -import 'package:titan/loan/providers/loaner_list_provider.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:titan/tools/token_expire_wrapper.dart'; -import 'package:titan/tools/ui/widgets/align_left_text.dart'; -import 'package:titan/tools/ui/builders/async_child.dart'; -import 'package:qlevar_router/qlevar_router.dart'; - -class AddLoanerPage extends HookConsumerWidget { - const AddLoanerPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final loanerListNotifier = ref.watch(loanerListProvider.notifier); - final loaners = ref.watch(allLoanerList); - final associations = ref.watch(allGroupListProvider); - final loanersId = loaners.map((x) => x.groupManagerId).toList(); - void displayToastWithContext(TypeMsg type, String msg) { - displayToast(context, type, msg); - } - - return AdminTemplate( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 30.0), - child: SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics( - parent: BouncingScrollPhysics(), - ), - child: Column( - children: [ - SizedBox( - child: Column( - children: [ - const AlignLeftText(AdminTextConstants.addLoaningGroup), - const SizedBox(height: 30), - AsyncChild( - value: associations, - builder: (context, associationList) { - final canAdd = associationList - .where((x) => !loanersId.contains(x.id)) - .toList(); - return canAdd.isNotEmpty - ? Column( - children: canAdd - .map( - (e) => GestureDetector( - onTap: () { - Loaner newLoaner = Loaner( - groupManagerId: e.id, - id: '', - name: e.name, - ); - tokenExpireWrapper(ref, () async { - final value = - await loanerListNotifier - .addLoaner(newLoaner); - if (value) { - QR.back(); - displayToastWithContext( - TypeMsg.msg, - AdminTextConstants.addedLoaner, - ); - } else { - displayToastWithContext( - TypeMsg.error, - AdminTextConstants.addingError, - ); - } - }); - }, - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 20, - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - e.name, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - ), - ), - const HeroIcon( - HeroIcons.plus, - size: 25, - color: Colors.black, - ), - ], - ), - ), - ), - ) - .toList(), - ) - : const Center( - child: Text(AdminTextConstants.noMoreLoaner), - ); - }, - ), - ], - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/admin/ui/pages/groups/edit_group_page/edit_group_page.dart b/lib/admin/ui/pages/groups/edit_group_page/edit_group_page.dart index a906c41534..76b16c7fc9 100644 --- a/lib/admin/ui/pages/groups/edit_group_page/edit_group_page.dart +++ b/lib/admin/ui/pages/groups/edit_group_page/edit_group_page.dart @@ -1,145 +1,45 @@ import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/class/group.dart'; +import 'package:titan/admin/admin.dart'; +import 'package:titan/admin/providers/group_from_simple_group_provider.dart'; import 'package:titan/admin/providers/group_id_provider.dart'; -import 'package:titan/admin/providers/group_list_provider.dart'; import 'package:titan/admin/providers/group_provider.dart'; -import 'package:titan/admin/providers/simple_groups_groups_provider.dart'; -import 'package:titan/admin/tools/constants.dart'; -import 'package:titan/admin/ui/admin.dart'; -import 'package:titan/admin/ui/components/admin_button.dart'; import 'package:titan/admin/ui/pages/groups/edit_group_page/search_user.dart'; -import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:titan/tools/token_expire_wrapper.dart'; -import 'package:titan/tools/ui/builders/auto_loader_child.dart'; -import 'package:titan/tools/ui/widgets/align_left_text.dart'; -import 'package:titan/tools/ui/builders/waiting_button.dart'; -import 'package:titan/tools/ui/widgets/text_entry.dart'; -import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/tools/ui/builders/single_auto_loader_child.dart'; +import 'package:titan/tools/ui/layouts/refresher.dart'; -class EditGroupPage extends HookConsumerWidget { +class EditGroupPage extends ConsumerWidget { const EditGroupPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final groupId = ref.watch(groupIdProvider); - final groupNotifier = ref.watch(groupProvider.notifier); - final groupListNotifier = ref.watch(allGroupListProvider.notifier); - final key = GlobalKey(); - final name = useTextEditingController(); - final description = useTextEditingController(); - final simpleGroupsGroupsNotifier = ref.watch( - simpleGroupsGroupsProvider.notifier, + final group = ref.watch( + groupFromSimpleGroupProvider.select((map) => map[groupId]), ); - final simpleGroupsGroups = ref.watch( - simpleGroupsGroupsProvider.select((value) => value[groupId]), + final groupNotifier = ref.watch(groupProvider.notifier); + final groupFromSimpleGroupNotifier = ref.watch( + groupFromSimpleGroupProvider.notifier, ); - - void displayToastWithContext(TypeMsg type, String msg) { - displayToast(context, type, msg); - } - return AdminTemplate( - child: SingleChildScrollView( - physics: const BouncingScrollPhysics(), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 30.0), - child: AutoLoaderChild( - group: simpleGroupsGroups, - notifier: simpleGroupsGroupsNotifier, - mapKey: groupId, - loader: (groupId) async => (await groupNotifier.loadGroup( - groupId, - )).maybeWhen(data: (groups) => groups, orElse: () => Group.empty()), - dataBuilder: (context, groups) { - final group = groups.first; - name.text = group.name; - description.text = group.description; - return Column( - children: [ - const AlignLeftText( - AdminTextConstants.edit, - fontSize: 20, - color: ColorConstants.gradient1, - ), - const SizedBox(height: 20), - Form( - key: key, - child: Column( - children: [ - Container( - margin: const EdgeInsets.symmetric(vertical: 10), - alignment: Alignment.centerLeft, - child: TextEntry( - controller: name, - color: ColorConstants.gradient1, - label: AdminTextConstants.name, - suffixIcon: const HeroIcon(HeroIcons.pencil), - enabledColor: Colors.transparent, - ), - ), - Container( - margin: const EdgeInsets.symmetric(vertical: 10), - alignment: Alignment.centerLeft, - child: TextEntry( - controller: description, - color: ColorConstants.gradient1, - label: AdminTextConstants.description, - suffixIcon: const HeroIcon(HeroIcons.pencil), - enabledColor: Colors.transparent, - ), - ), - const SizedBox(height: 20), - WaitingButton( - onTap: () async { - if (!key.currentState!.validate()) { - return; - } - await tokenExpireWrapper(ref, () async { - Group newGroup = group.copyWith( - name: name.text, - description: description.text, - ); - groupNotifier.setGroup(newGroup); - final value = await groupListNotifier.updateGroup( - newGroup.toSimpleGroup(), - ); - if (value) { - QR.back(); - displayToastWithContext( - TypeMsg.msg, - AdminTextConstants.updatedGroup, - ); - } else { - displayToastWithContext( - TypeMsg.msg, - AdminTextConstants.updatingError, - ); - } - }); - }, - builder: (child) => AdminButton(child: child), - child: const Text( - AdminTextConstants.edit, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - ), - const SizedBox(height: 20), - const SearchUser(), - ], - ), - ), - ], - ); - }, - loaderColor: Colors.white, + child: Refresher( + controller: ScrollController(), + onRefresh: () async { + await groupNotifier.loadGroup(groupId); + }, + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: SingleAutoLoaderChild( + item: group, + notifier: groupFromSimpleGroupNotifier, + mapKey: groupId, + loader: groupNotifier.loadGroup, + dataBuilder: (BuildContext context, value) { + return SearchUser(); + }, + ), ), ), ), diff --git a/lib/admin/ui/pages/groups/edit_group_page/results.dart b/lib/admin/ui/pages/groups/edit_group_page/results.dart index 9c3709453c..ea73f29adf 100644 --- a/lib/admin/ui/pages/groups/edit_group_page/results.dart +++ b/lib/admin/ui/pages/groups/edit_group_page/results.dart @@ -4,13 +4,13 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/admin/class/group.dart'; import 'package:titan/admin/providers/group_provider.dart'; import 'package:titan/admin/providers/simple_groups_groups_provider.dart'; -import 'package:titan/admin/tools/constants.dart'; import 'package:titan/tools/constants.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; -import 'package:titan/tools/ui/builders/waiting_button.dart'; +import 'package:titan/tools/ui/styleguide/list_item_template.dart'; import 'package:titan/user/providers/user_list_provider.dart'; +import 'package:titan/l10n/app_localizations.dart'; class MemberResults extends HookConsumerWidget { const MemberResults({super.key}); @@ -35,60 +35,48 @@ class MemberResults extends HookConsumerWidget { .map( (e) => Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - e.getName(), - style: const TextStyle(fontSize: 15), - overflow: TextOverflow.ellipsis, - ), - ), - Row( - children: [ - WaitingButton( - onTap: () async { - if (!group.value!.members.contains(e)) { - Group newGroup = group.value!.copyWith( - members: group.value!.members + [e], - ); - await tokenExpireWrapper(ref, () async { - groupNotifier.addMember(newGroup, e).then(( - value, - ) { - if (value) { - simpleGroupGroupsNotifier.setTData( - newGroup.id, - AsyncData([newGroup]), - ); - displayToastWithContext( - TypeMsg.msg, - AdminTextConstants.addedMember, - ); - } else { - displayToastWithContext( - TypeMsg.error, - AdminTextConstants.addingError, - ); - } - }); - }); - } - }, - waitingColor: ColorConstants.gradient1, - builder: (child) => child, - child: const HeroIcon(HeroIcons.plus), - ), - ], - ), - ], + child: ListItemTemplate( + title: e.getName(), + onTap: () async { + if (!group.value!.members.contains(e)) { + Group newGroup = group.value!.copyWith( + members: group.value!.members + [e], + ); + final addedMemberMsg = AppLocalizations.of( + context, + )!.adminAddedMember; + final addingErrorMsg = AppLocalizations.of( + context, + )!.adminAddingError; + await tokenExpireWrapper(ref, () async { + groupNotifier.addMember(newGroup, e).then((result) { + if (result) { + simpleGroupGroupsNotifier.setTData( + newGroup.id, + AsyncData([newGroup]), + ); + value.remove(e); + displayToastWithContext( + TypeMsg.msg, + addedMemberMsg, + ); + } else { + displayToastWithContext( + TypeMsg.error, + addingErrorMsg, + ); + } + }); + }); + } + }, + trailing: const HeroIcon(HeroIcons.plus), ), ), ) .toList(), ), - loaderColor: ColorConstants.gradient1, + loaderColor: ColorConstants.main, ); } } diff --git a/lib/admin/ui/pages/groups/edit_group_page/search_user.dart b/lib/admin/ui/pages/groups/edit_group_page/search_user.dart index bc5f1723f4..8fcc0629a0 100644 --- a/lib/admin/ui/pages/groups/edit_group_page/search_user.dart +++ b/lib/admin/ui/pages/groups/edit_group_page/search_user.dart @@ -1,150 +1,153 @@ import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/admin/class/group.dart'; -import 'package:titan/admin/providers/group_id_provider.dart'; +import 'package:titan/admin/providers/group_from_simple_group_provider.dart'; import 'package:titan/admin/providers/group_provider.dart'; -import 'package:titan/admin/providers/simple_groups_groups_provider.dart'; -import 'package:titan/admin/tools/constants.dart'; -import 'package:titan/admin/ui/pages/groups/edit_group_page/results.dart'; import 'package:titan/admin/ui/components/user_ui.dart'; -import 'package:titan/tools/constants.dart'; +import 'package:titan/admin/ui/pages/groups/edit_group_page/results.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/icon_button.dart'; +import 'package:titan/tools/ui/styleguide/searchbar.dart'; import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; -import 'package:titan/tools/ui/widgets/loader.dart'; -import 'package:titan/tools/ui/widgets/styled_search_bar.dart'; import 'package:titan/user/providers/user_list_provider.dart'; +import 'package:titan/l10n/app_localizations.dart'; class SearchUser extends HookConsumerWidget { const SearchUser({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final group = ref.watch(groupProvider); final usersNotifier = ref.watch(userList.notifier); - final groupId = ref.watch(groupIdProvider); final groupNotifier = ref.watch(groupProvider.notifier); - final simpleGroupsGroups = ref.watch(simpleGroupsGroupsProvider); - final simpleGroupGroupsNotifier = ref.watch( - simpleGroupsGroupsProvider.notifier, + final group = ref.watch(groupProvider); + final groupFromSimpleGroupNotifier = ref.watch( + groupFromSimpleGroupProvider.notifier, ); - final add = useState(false); void displayToastWithContext(TypeMsg type, String msg) { displayToast(context, type, msg); } - final simpleGroup = simpleGroupsGroups[groupId]; - if (simpleGroup == null) { - return const Loader(); - } + final localizeWithContext = AppLocalizations.of(context)!; + return AsyncChild( - value: simpleGroup, + value: group, builder: (context, g) { return Column( children: [ - StyledSearchBar( - label: AdminTextConstants.members, - color: ColorConstants.gradient1, - padding: const EdgeInsets.all(0), - onChanged: (value) async { - if (value.isNotEmpty) { - await usersNotifier.filterUsers( - value, - excludeGroup: [group.value!.toSimpleGroup()], - ); - } else { - usersNotifier.clear(); - } - }, - onSuffixIconTap: (focusNode, editingController) { - add.value = !add.value; - if (!add.value) { - editingController.clear(); - usersNotifier.clear(); - focusNode.unfocus(); - } else { - focusNode.requestFocus(); - } - }, - suffixIcon: Padding( - padding: const EdgeInsets.all(7.0), - child: Container( - padding: const EdgeInsets.all(7), - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [ - ColorConstants.gradient1, - ColorConstants.gradient2, - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - boxShadow: [ - BoxShadow( - color: ColorConstants.gradient2.withValues(alpha: 0.4), - offset: const Offset(2, 3), - blurRadius: 5, - ), - ], - borderRadius: const BorderRadius.all(Radius.circular(10)), - ), - child: HeroIcon( - !add.value ? HeroIcons.plus : HeroIcons.xMark, - size: 20, - color: Colors.white, + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: CustomSearchBar( + onSearch: (value) async { + if (value.isNotEmpty) { + await usersNotifier.filterUsers( + value, + includeGroup: [g.toSimpleGroup()], + ); + } else { + usersNotifier.clear(); + } + }, ), ), - ), - ), - if (add.value) const SizedBox(height: 10), - if (add.value) const MemberResults(), - if (!add.value) - ...g[0].members.map( - (x) => UserUi( - user: x, - onDelete: () { - showDialog( + const SizedBox(width: 10), + CustomIconButton( + icon: HeroIcon(HeroIcons.plus, size: 30, color: Colors.white), + onPressed: () async { + await showCustomBottomModal( context: context, - builder: (BuildContext context) => CustomDialogBox( - descriptions: AdminTextConstants.removeGroupMember, - title: AdminTextConstants.deleting, - onYes: () async { - await tokenExpireWrapper(ref, () async { - Group newGroup = g[0].copyWith( - members: g[0].members - .where((element) => element.id != x.id) - .toList(), - ); - final value = await groupNotifier.deleteMember( - newGroup, - x, - ); - if (value) { - simpleGroupGroupsNotifier.setTData( - newGroup.id, - AsyncData([newGroup]), - ); - displayToastWithContext( - TypeMsg.msg, - AdminTextConstants.updatedGroup, - ); - } else { - displayToastWithContext( - TypeMsg.msg, - AdminTextConstants.updatingError, - ); - } - }); - }, + ref: ref, + modal: BottomModalTemplate( + title: localizeWithContext.adminAddMember, + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 350), + child: Column( + children: [ + CustomSearchBar( + onSearch: (value) async { + if (value.isNotEmpty) { + await usersNotifier.filterUsers( + value, + excludeGroup: [g.toSimpleGroup()], + ); + } else { + usersNotifier.clear(); + } + }, + ), + const SizedBox(height: 10), + Expanded( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: const MemberResults(), + ), + ), + ], + ), + ), ), ); }, ), + ], + ), + const SizedBox(height: 10), + ...g.members.map( + (x) => UserUi( + user: x, + onDelete: () { + showDialog( + context: context, + builder: (BuildContext context) => CustomDialogBox( + descriptions: AppLocalizations.of( + context, + )!.adminRemoveGroupMember, + title: AppLocalizations.of(context)!.adminDeleting, + onYes: () async { + final updatedGroupMsg = AppLocalizations.of( + context, + )!.adminUpdatedGroup; + final updatingErrorMsg = AppLocalizations.of( + context, + )!.adminUpdatingError; + await tokenExpireWrapper(ref, () async { + Group newGroup = g.copyWith( + members: g.members + .where((element) => element.id != x.id) + .toList(), + ); + final value = await groupNotifier.deleteMember( + newGroup, + x, + ); + if (value) { + groupFromSimpleGroupNotifier.setTData( + newGroup.id, + AsyncData(newGroup), + ); + displayToastWithContext( + TypeMsg.msg, + updatedGroupMsg, + ); + } else { + displayToastWithContext( + TypeMsg.msg, + updatingErrorMsg, + ); + } + }); + }, + ), + ); + }, ), + ), ], ); }, diff --git a/lib/admin/ui/pages/groups/group_page/group_button.dart b/lib/admin/ui/pages/groups/group_page/group_button.dart deleted file mode 100644 index 9c29cae36c..0000000000 --- a/lib/admin/ui/pages/groups/group_page/group_button.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; - -class GroupButton extends StatelessWidget { - final Widget child; - final Color gradient1; - final Color gradient2; - const GroupButton({ - super.key, - required this.child, - required this.gradient1, - required this.gradient2, - }); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [gradient1, gradient2], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - boxShadow: [ - BoxShadow( - color: gradient2.withValues(alpha: 0.3), - spreadRadius: 2, - blurRadius: 4, - offset: const Offset(2, 3), - ), - ], - borderRadius: BorderRadius.circular(10), - ), - child: Center(child: child), - ); - } -} diff --git a/lib/admin/ui/pages/groups/group_page/group_page.dart b/lib/admin/ui/pages/groups/group_page/group_page.dart deleted file mode 100644 index d563677205..0000000000 --- a/lib/admin/ui/pages/groups/group_page/group_page.dart +++ /dev/null @@ -1,186 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/providers/group_id_provider.dart'; -import 'package:titan/admin/providers/group_list_provider.dart'; -import 'package:titan/admin/router.dart'; -import 'package:titan/admin/ui/admin.dart'; -import 'package:titan/admin/ui/components/item_card_ui.dart'; -import 'package:titan/admin/ui/pages/groups/group_page/group_ui.dart'; -import 'package:titan/loan/providers/loaner_list_provider.dart'; -import 'package:titan/admin/tools/constants.dart'; -import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/ui/builders/async_child.dart'; -import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:titan/tools/ui/layouts/refresher.dart'; -import 'package:titan/tools/token_expire_wrapper.dart'; -import 'package:titan/user/providers/user_list_provider.dart'; -import 'package:qlevar_router/qlevar_router.dart'; - -class GroupsPage extends HookConsumerWidget { - const GroupsPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final groups = ref.watch(allGroupListProvider); - final groupsNotifier = ref.watch(allGroupListProvider.notifier); - final groupIdNotifier = ref.watch(groupIdProvider.notifier); - final loans = ref.watch(loanerListProvider); - final loanListNotifier = ref.watch(loanerListProvider.notifier); - ref.watch(userList); - void displayToastWithContext(TypeMsg type, String msg) { - displayToast(context, type, msg); - } - - final List loanersId = loans.maybeWhen( - data: (value) => value.map((e) => e.groupManagerId).toList(), - orElse: () => [], - ); - - return AdminTemplate( - child: Refresher( - onRefresh: () async { - await groupsNotifier.loadGroups(); - await loanListNotifier.loadLoanerList(); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 30.0), - child: Column( - children: [ - const SizedBox(height: 20), - const Align( - alignment: Alignment.centerLeft, - child: Text( - AdminTextConstants.groups, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w700, - color: ColorConstants.gradient1, - ), - ), - ), - const SizedBox(height: 30), - AsyncChild( - value: groups, - builder: (context, g) { - g.sort( - (a, b) => - a.name.toLowerCase().compareTo(b.name.toLowerCase()), - ); - return Column( - children: [ - Column( - children: [ - GestureDetector( - onTap: () { - QR.to( - AdminRouter.root + - AdminRouter.groups + - AdminRouter.addGroup, - ); - }, - child: ItemCardUi( - children: [ - const Spacer(), - HeroIcon( - HeroIcons.plus, - color: Colors.grey.shade700, - size: 40, - ), - const Spacer(), - ], - ), - ), - GestureDetector( - onTap: () { - QR.to( - AdminRouter.root + - AdminRouter.groups + - AdminRouter.addLoaner, - ); - }, - child: ItemCardUi( - children: [ - const Spacer(), - Stack( - clipBehavior: Clip.none, - children: [ - HeroIcon( - HeroIcons.buildingLibrary, - color: Colors.grey.shade700, - size: 40, - ), - Positioned( - right: -2, - top: -2, - child: HeroIcon( - HeroIcons.plus, - size: 15, - color: Colors.grey.shade700, - ), - ), - ], - ), - const Spacer(), - ], - ), - ), - ...g.map( - (group) => GroupUi( - group: group, - isLoaner: loanersId.contains(group.id), - onEdit: () { - groupIdNotifier.setId(group.id); - QR.to( - AdminRouter.root + - AdminRouter.groups + - AdminRouter.editGroup, - ); - }, - onDelete: () async { - await showDialog( - context: context, - builder: (context) { - return CustomDialogBox( - title: AdminTextConstants.deleting, - descriptions: - AdminTextConstants.deleteGroup, - onYes: () async { - tokenExpireWrapper(ref, () async { - final value = await groupsNotifier - .deleteGroup(group); - if (value) { - displayToastWithContext( - TypeMsg.msg, - AdminTextConstants.deletedGroup, - ); - } else { - displayToastWithContext( - TypeMsg.error, - AdminTextConstants.deletingError, - ); - } - }); - }, - ); - }, - ); - }, - ), - ), - const SizedBox(height: 20), - ], - ), - ], - ); - }, - loaderColor: ColorConstants.gradient1, - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/admin/ui/pages/groups/group_page/group_ui.dart b/lib/admin/ui/pages/groups/group_page/group_ui.dart deleted file mode 100644 index faad00dce6..0000000000 --- a/lib/admin/ui/pages/groups/group_page/group_ui.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/class/simple_group.dart'; -import 'package:titan/admin/ui/components/item_card_ui.dart'; -import 'package:titan/admin/ui/pages/groups/group_page/group_button.dart'; -import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/ui/builders/waiting_button.dart'; - -class GroupUi extends HookConsumerWidget { - final SimpleGroup group; - final void Function() onEdit; - final Future Function() onDelete; - final bool isLoaner; - const GroupUi({ - super.key, - required this.group, - required this.onEdit, - required this.onDelete, - required this.isLoaner, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return ItemCardUi( - children: [ - const SizedBox(width: 10), - if (isLoaner) - Row( - children: [ - HeroIcon(HeroIcons.buildingLibrary, color: Colors.grey.shade700), - const SizedBox(width: 15), - ], - ), - Expanded( - child: Text( - group.name, - style: const TextStyle( - color: Colors.black, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(width: 10), - Row( - children: [ - GestureDetector( - onTap: onEdit, - child: GroupButton( - gradient1: Colors.grey.shade800, - gradient2: Colors.grey.shade900, - child: const HeroIcon(HeroIcons.eye, color: Colors.white), - ), - ), - const SizedBox(width: 10), - WaitingButton( - onTap: onDelete, - builder: (child) => GroupButton( - gradient1: ColorConstants.gradient1, - gradient2: ColorConstants.gradient2, - child: child, - ), - child: const HeroIcon(HeroIcons.xMark, color: Colors.white), - ), - ], - ), - ], - ); - } -} diff --git a/lib/admin/ui/pages/groups/groups_page/groups_page.dart b/lib/admin/ui/pages/groups/groups_page/groups_page.dart new file mode 100644 index 0000000000..d61438321a --- /dev/null +++ b/lib/admin/ui/pages/groups/groups_page/groups_page.dart @@ -0,0 +1,315 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/admin/admin.dart'; +import 'package:titan/admin/class/simple_group.dart'; +import 'package:titan/admin/providers/group_id_provider.dart'; +import 'package:titan/admin/providers/group_list_provider.dart'; +import 'package:titan/admin/router.dart'; +import 'package:titan/navigation/ui/scroll_to_hide_navbar.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; +import 'package:titan/tools/ui/styleguide/icon_button.dart'; +import 'package:titan/tools/ui/styleguide/list_item.dart'; +import 'package:titan/tools/ui/styleguide/text_entry.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/ui/layouts/refresher.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; +import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; +import 'package:titan/user/providers/user_list_provider.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; + +class GroupsPage extends HookConsumerWidget { + const GroupsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final groups = ref.watch(allGroupListProvider); + final groupsNotifier = ref.watch(allGroupListProvider.notifier); + final nameController = useTextEditingController(); + final descController = useTextEditingController(); + final groupIdNotifier = ref.watch(groupIdProvider.notifier); + final groupListNotifier = ref.watch(allGroupListProvider.notifier); + final scrollController = useScrollController(); + ref.watch(userList); + void displayToastWithContext(TypeMsg type, String msg) { + displayToast(context, type, msg); + } + + final localizeWithContext = AppLocalizations.of(context)!; + final navigatorWithContext = Navigator.of(context); + + return AdminTemplate( + child: ScrollToHideNavbar( + controller: scrollController, + child: Refresher( + controller: scrollController, + onRefresh: () async { + await groupsNotifier.loadGroups(); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Column( + children: [ + const SizedBox(height: 20), + Row( + children: [ + Text( + localizeWithContext.adminGroupsManagement, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorConstants.title, + ), + ), + Spacer(), + CustomIconButton( + icon: const HeroIcon( + HeroIcons.plus, + size: 30, + color: Colors.white, + ), + onPressed: () async { + nameController.text = ''; + descController.text = ''; + await showCustomBottomModal( + context: context, + ref: ref, + modal: BottomModalTemplate( + title: localizeWithContext.adminAddGroup, + child: Column( + children: [ + TextEntry( + label: localizeWithContext.adminName, + controller: nameController, + ), + const SizedBox(height: 20), + TextEntry( + label: localizeWithContext.adminDescription, + controller: descController, + ), + const SizedBox(height: 20), + Button( + text: localizeWithContext.adminAdd, + onPressed: () async { + final addedGroupMsg = AppLocalizations.of( + context, + )!.adminAddedGroup; + final addingErrorMsg = AppLocalizations.of( + context, + )!.adminAddingError; + await tokenExpireWrapper(ref, () async { + final value = await groupListNotifier + .createGroup( + SimpleGroup( + name: nameController.text, + description: descController.text, + id: '', + ), + ); + if (value) { + QR.back(); + displayToastWithContext( + TypeMsg.msg, + addedGroupMsg, + ); + } else { + displayToastWithContext( + TypeMsg.error, + addingErrorMsg, + ); + } + }); + }, + ), + ], + ), + ), + ); + }, + ), + ], + ), + const SizedBox(height: 10), + AsyncChild( + value: groups, + builder: (context, g) { + g.sort( + (a, b) => + a.name.toLowerCase().compareTo(b.name.toLowerCase()), + ); + return Column( + children: [ + ...g.map( + (group) => Padding( + padding: const EdgeInsets.symmetric(vertical: 5.0), + child: ListItem( + title: group.name, + subtitle: group.description, + onTap: () async { + await showCustomBottomModal( + context: context, + ref: ref, + modal: BottomModalTemplate( + title: group.name, + child: Column( + children: [ + Button( + text: localizeWithContext.adminEdit, + onPressed: () async { + nameController.text = group.name; + descController.text = + group.description; + Navigator.pop(context); + await showCustomBottomModal( + context: context, + ref: ref, + modal: BottomModalTemplate( + title: localizeWithContext + .adminEditGroup, + child: Column( + children: [ + TextEntry( + label: localizeWithContext + .adminName, + controller: + nameController, + ), + const SizedBox(height: 20), + TextEntry( + label: localizeWithContext + .adminDescription, + controller: + descController, + ), + const SizedBox(height: 20), + Button( + text: localizeWithContext + .adminEdit, + onPressed: () async { + final addedGroupMsg = + AppLocalizations.of( + context, + )!.adminAddedGroup; + final addingErrorMsg = + AppLocalizations.of( + context, + )!.adminAddingError; + await tokenExpireWrapper( + ref, + () async { + final value = await groupListNotifier + .updateGroup( + SimpleGroup( + name: nameController + .text, + description: + descController + .text, + id: group + .id, + ), + ); + if (value) { + QR.back(); + displayToastWithContext( + TypeMsg.msg, + addedGroupMsg, + ); + } else { + displayToastWithContext( + TypeMsg.error, + addingErrorMsg, + ); + } + }, + ); + }, + ), + ], + ), + ), + ); + }, + ), + const SizedBox(height: 20), + Button( + text: localizeWithContext + .adminManageMembers, + onPressed: () { + Navigator.pop(context); + groupIdNotifier.setId(group.id); + QR.to( + AdminRouter.root + + AdminRouter.usersGroups + + AdminRouter.editGroup, + ); + }, + ), + const SizedBox(height: 20), + Button( + text: localizeWithContext + .adminDeleteGroup, + type: ButtonType.danger, + onPressed: () async { + await showDialog( + context: context, + builder: (context) { + return CustomDialogBox( + title: localizeWithContext + .adminDelete, + descriptions: localizeWithContext + .adminDeleteGroupConfirmation, + onYes: () async { + tokenExpireWrapper(ref, () async { + final value = + await groupsNotifier + .deleteGroup( + group, + ); + if (value) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext + .adminDeletedGroup, + ); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext + .adminFailedToDeleteGroup, + ); + } + }); + }, + ); + }, + ); + navigatorWithContext.pop(); + }, + ), + ], + ), + ), + ); + }, + ), + ), + ), + const SizedBox(height: 20), + ], + ); + }, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/admin/ui/pages/main_page/main_page.dart b/lib/admin/ui/pages/main_page/main_page.dart index a458200eb1..e59220c545 100644 --- a/lib/admin/ui/pages/main_page/main_page.dart +++ b/lib/admin/ui/pages/main_page/main_page.dart @@ -1,13 +1,17 @@ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/navigation/ui/scroll_to_hide_navbar.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/admin/admin.dart'; import 'package:titan/admin/router.dart'; -import 'package:titan/admin/tools/constants.dart'; -import 'package:titan/admin/ui/admin.dart'; -import 'package:titan/admin/ui/pages/main_page/menu_card_ui.dart'; +import 'package:titan/admin/ui/pages/users_management_page/users_management_page.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/list_item.dart'; + import 'package:titan/user/providers/user_list_provider.dart'; -import 'package:qlevar_router/qlevar_router.dart'; class AdminMainPage extends HookConsumerWidget { const AdminMainPage({super.key}); @@ -16,61 +20,102 @@ class AdminMainPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { ref.watch(userList); - final controller = ScrollController(); + final localizeWithContext = AppLocalizations.of(context)!; return AdminTemplate( child: Padding( - padding: const EdgeInsets.all(40), - child: GridView( - controller: controller, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - mainAxisSpacing: 20, - crossAxisSpacing: 20, - childAspectRatio: - MediaQuery.of(context).size.width < - MediaQuery.of(context).size.height - ? 0.75 - : 1.5, - ), - children: [ - GestureDetector( - onTap: () { - QR.to(AdminRouter.root + AdminRouter.editModuleVisibility); - }, - child: const MenuCardUi( - text: AdminTextConstants.visibilities, - icon: HeroIcons.eye, - ), + padding: const EdgeInsets.all(20), + child: ScrollToHideNavbar( + controller: ScrollController(), + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + localizeWithContext.adminAdministration, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorConstants.title, + ), + ), + const SizedBox(height: 20), + Text( + localizeWithContext.adminUsersAndGroups, + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 10), + ListItem( + title: localizeWithContext.adminUsersManagement, + subtitle: localizeWithContext.adminUsersManagementDescription, + onTap: () async { + await showCustomBottomModal( + context: context, + ref: ref, + modal: BottomModalTemplate( + title: localizeWithContext.adminUsersManagement, + child: UsersManagementPage(), + ), + ); + }, + ), + const SizedBox(height: 10), + ListItem( + title: localizeWithContext.adminGroupsManagement, + subtitle: localizeWithContext.adminManageUserGroups, + onTap: () => + QR.to(AdminRouter.root + AdminRouter.usersGroups), + ), + const SizedBox(height: 10), + ListItem( + title: localizeWithContext.adminGroupNotification, + subtitle: localizeWithContext.adminSendNotificationToGroup, + onTap: () => + QR.to(AdminRouter.root + AdminRouter.groupNotification), + ), + const SizedBox(height: 20), + Text( + localizeWithContext.adminPaiementModule, + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 10), + ListItem( + title: getPaymentName(), + subtitle: localizeWithContext.adminManagePaiementStructures, + onTap: () => QR.to(AdminRouter.root + AdminRouter.structures), + ), + const SizedBox(height: 20), + Text( + localizeWithContext.adminAssociationMembership, + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 10), + ListItem( + title: localizeWithContext.adminAssociationMembership, + subtitle: localizeWithContext + .adminManageUsersAssociationMemberships, + onTap: () => QR.to( + AdminRouter.root + AdminRouter.associationMemberships, + ), + ), + const SizedBox(height: 20), + Text( + localizeWithContext.adminAssociations, + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 10), + ListItem( + title: localizeWithContext.adminAssociations, + subtitle: localizeWithContext.adminManageAssociations, + onTap: () { + QR.to(AdminRouter.root + AdminRouter.association); + }, + ), + const SizedBox(height: 20), + ], ), - GestureDetector( - onTap: () { - QR.to(AdminRouter.root + AdminRouter.groups); - }, - child: const MenuCardUi( - text: AdminTextConstants.groups, - icon: HeroIcons.users, - ), - ), - GestureDetector( - onTap: () { - QR.to(AdminRouter.root + AdminRouter.schools); - }, - child: const MenuCardUi( - text: AdminTextConstants.schools, - icon: HeroIcons.academicCap, - ), - ), - GestureDetector( - onTap: () { - QR.to(AdminRouter.root + AdminRouter.structures); - }, - child: const MenuCardUi( - text: AdminTextConstants.myEclPay, - icon: HeroIcons.creditCard, - ), - ), - ], + ), ), ), ); diff --git a/lib/admin/ui/pages/memberships/add_edit_user_membership_page/add_edit_user_membership_page.dart b/lib/admin/ui/pages/membership/add_edit_user_membership_page/add_edit_user_membership_page.dart similarity index 57% rename from lib/admin/ui/pages/memberships/add_edit_user_membership_page/add_edit_user_membership_page.dart rename to lib/admin/ui/pages/membership/add_edit_user_membership_page/add_edit_user_membership_page.dart index 3ec8fa38b3..854f2bd756 100644 --- a/lib/admin/ui/pages/memberships/add_edit_user_membership_page/add_edit_user_membership_page.dart +++ b/lib/admin/ui/pages/membership/add_edit_user_membership_page/add_edit_user_membership_page.dart @@ -1,41 +1,40 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:titan/admin/admin.dart'; import 'package:titan/admin/class/user_association_membership.dart'; import 'package:titan/admin/class/user_association_membership_base.dart'; import 'package:titan/admin/providers/association_membership_members_list_provider.dart'; import 'package:titan/admin/providers/user_association_membership_provider.dart'; -import 'package:titan/admin/tools/constants.dart'; -import 'package:titan/admin/ui/admin.dart'; -import 'package:titan/admin/ui/pages/memberships/add_edit_user_membership_page/search_result.dart'; +import 'package:titan/admin/ui/pages/membership/add_edit_user_membership_page/user_search_modal.dart'; import 'package:titan/tools/constants.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; -import 'package:titan/tools/ui/layouts/add_edit_button_layout.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/list_item.dart'; import 'package:titan/tools/ui/widgets/align_left_text.dart'; import 'package:titan/tools/ui/widgets/date_entry.dart'; -import 'package:titan/tools/ui/widgets/styled_search_bar.dart'; -import 'package:titan/user/providers/user_list_provider.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AddEditUserMembershipPage extends HookConsumerWidget { const AddEditUserMembershipPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final locale = Localizations.localeOf(context); final associationMembershipMembersNotifier = ref.watch( associationMembershipMembersProvider.notifier, ); - final queryController = useTextEditingController(text: ''); - final usersNotifier = ref.watch(userList.notifier); final membership = ref.watch(userAssociationMembershipProvider); final isEdit = membership.id != UserAssociationMembership.empty().id; final start = useTextEditingController( - text: isEdit ? processDate(membership.startDate) : "", + text: isEdit ? DateFormat.yMd(locale).format(membership.startDate) : "", ); final end = useTextEditingController( - text: isEdit ? processDate(membership.endDate) : "", + text: isEdit ? DateFormat.yMd(locale).format(membership.endDate) : "", ); void displayToastWithContext(TypeMsg type, String msg) { @@ -44,32 +43,32 @@ class AddEditUserMembershipPage extends HookConsumerWidget { return AdminTemplate( child: Padding( - padding: const EdgeInsets.all(30.0), + padding: const EdgeInsets.all(20.0), child: SingleChildScrollView( child: Column( children: [ AlignLeftText( isEdit - ? AdminTextConstants.editMembership - : AdminTextConstants.addMember, + ? AppLocalizations.of(context)!.adminEditMembership + : AppLocalizations.of(context)!.adminAddMember, + fontWeight: FontWeight.w900, + color: ColorConstants.title, + fontSize: 24, ), const SizedBox(height: 20), if (!isEdit) ...[ - StyledSearchBar( - padding: EdgeInsets.zero, - label: AdminTextConstants.user, - editingController: queryController, - onChanged: (value) async { - tokenExpireWrapper(ref, () async { - if (value.isNotEmpty) { - await usersNotifier.filterUsers(value); - } else { - usersNotifier.clear(); - } - }); + ListItem( + title: membership.user.id.isNotEmpty + ? membership.user.getName() + : AppLocalizations.of(context)!.adminUser, + onTap: () async { + await showCustomBottomModal( + context: context, + ref: ref, + modal: UserSearchModal(), + ); }, ), - SearchResult(queryController: queryController), ] else Text( membership.user.getName(), @@ -80,7 +79,7 @@ class AddEditUserMembershipPage extends HookConsumerWidget { ), const SizedBox(height: 10), DateEntry( - label: AdminTextConstants.startDate, + label: AppLocalizations.of(context)!.adminStartDate, controller: start, onTap: () => getOnlyDayDate( context, @@ -89,9 +88,9 @@ class AddEditUserMembershipPage extends HookConsumerWidget { lastDate: DateTime(2100), ), ), - const SizedBox(height: 50), + const SizedBox(height: 10), DateEntry( - label: AdminTextConstants.endDate, + label: AppLocalizations.of(context)!.adminEndDate, controller: end, onTap: () => getOnlyDayDate( context, @@ -100,17 +99,25 @@ class AddEditUserMembershipPage extends HookConsumerWidget { lastDate: DateTime(2100), ), ), - const SizedBox(height: 50), + const SizedBox(height: 20), WaitingButton( - builder: (child) => AddEditButtonLayout( - colors: const [ - ColorConstants.gradient1, - ColorConstants.gradient2, - ], - child: child, + builder: (child) => Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 10, + ), + decoration: BoxDecoration( + color: ColorConstants.tertiary, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: ColorConstants.onTertiary), + ), + child: Center(child: child), ), child: Text( - !isEdit ? AdminTextConstants.add : AdminTextConstants.edit, + !isEdit + ? AppLocalizations.of(context)!.adminAdd + : AppLocalizations.of(context)!.adminEdit, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w600, @@ -121,50 +128,60 @@ class AddEditUserMembershipPage extends HookConsumerWidget { if (membership.user.id == "") { displayToastWithContext( TypeMsg.msg, - AdminTextConstants.emptyUser, + AppLocalizations.of(context)!.adminEmptyUser, ); return; } if (start.text.isEmpty || end.text.isEmpty) { displayToastWithContext( TypeMsg.msg, - AdminTextConstants.emptyDate, + AppLocalizations.of(context)!.adminEmptyDate, ); return; } tokenExpireWrapper(ref, () async { if (DateTime.parse( - processDateBack(start.text), - ).isAfter(DateTime.parse(processDateBack(end.text)))) { + processDateBack(start.text, locale.toString()), + ).isAfter( + DateTime.parse( + processDateBack(end.text, locale.toString()), + ), + )) { displayToastWithContext( TypeMsg.error, - AdminTextConstants.dateError, + AppLocalizations.of(context)!.adminDateError, ); return; } if (isEdit) { + final updatedMembershipMsg = AppLocalizations.of( + context, + )!.adminUpdatedMembership; + final updatingErrorMsg = AppLocalizations.of( + context, + )!.adminMembershipUpdatingError; final value = await associationMembershipMembersNotifier .updateMember( membership.copyWith( startDate: DateTime.parse( - processDateBack(start.text), + processDateBack(start.text, locale.toString()), ), endDate: DateTime.parse( - processDateBack(end.text), + processDateBack(end.text, locale.toString()), ), ), ); if (value) { displayToastWithContext( TypeMsg.msg, - AdminTextConstants.updatedMembership, + updatedMembershipMsg, ); QR.back(); } else { displayToastWithContext( TypeMsg.error, - AdminTextConstants.membershipUpdatingError, + updatingErrorMsg, ); } } else { @@ -174,22 +191,26 @@ class AddEditUserMembershipPage extends HookConsumerWidget { associationMembershipId: membership.associationMembershipId, userId: membership.user.id, - startDate: DateTime.parse(processDateBack(start.text)), - endDate: DateTime.parse(processDateBack(end.text)), + startDate: DateTime.parse( + processDateBack(start.text, locale.toString()), + ), + endDate: DateTime.parse( + processDateBack(end.text, locale.toString()), + ), ); + final addedMemberMsg = AppLocalizations.of( + context, + )!.adminAddedMember; + final addingErrorMsg = AppLocalizations.of( + context, + )!.adminMembershipAddingError; final value = await associationMembershipMembersNotifier .addMember(membershipAdd, membership.user); if (value) { - displayToastWithContext( - TypeMsg.msg, - AdminTextConstants.addedMember, - ); + displayToastWithContext(TypeMsg.msg, addedMemberMsg); QR.back(); } else { - displayToastWithContext( - TypeMsg.error, - AdminTextConstants.membershipAddingError, - ); + displayToastWithContext(TypeMsg.error, addingErrorMsg); } } }); diff --git a/lib/admin/ui/pages/membership/add_edit_user_membership_page/search_result.dart b/lib/admin/ui/pages/membership/add_edit_user_membership_page/search_result.dart new file mode 100644 index 0000000000..281d4e5743 --- /dev/null +++ b/lib/admin/ui/pages/membership/add_edit_user_membership_page/search_result.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/admin/providers/user_association_membership_provider.dart'; +import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/user/providers/user_list_provider.dart'; + +class SearchResult extends HookConsumerWidget { + final TextEditingController queryController; + const SearchResult({super.key, required this.queryController}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final users = ref.watch(userList); + final usersNotifier = ref.watch(userList.notifier); + final membershipNotifier = ref.watch( + userAssociationMembershipProvider.notifier, + ); + final membership = ref.watch(userAssociationMembershipProvider); + + return AsyncChild( + value: users, + builder: (context, usersData) { + return Column( + children: usersData + .map( + (user) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + user.getName(), + style: const TextStyle(fontSize: 15), + overflow: TextOverflow.ellipsis, + ), + ), + GestureDetector( + onTap: () { + membershipNotifier.setUserAssociationMembership( + membership.copyWith(user: user, userId: user.id), + ); + usersNotifier.clear(); + Navigator.of(context).pop(); + }, + child: const HeroIcon(HeroIcons.plus), + ), + ], + ), + ), + ) + .toList(), + ); + }, + ); + } +} diff --git a/lib/admin/ui/pages/membership/add_edit_user_membership_page/user_search_modal.dart b/lib/admin/ui/pages/membership/add_edit_user_membership_page/user_search_modal.dart new file mode 100644 index 0000000000..20b3195f97 --- /dev/null +++ b/lib/admin/ui/pages/membership/add_edit_user_membership_page/user_search_modal.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/admin/ui/pages/membership/add_edit_user_membership_page/search_result.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/searchbar.dart'; +import 'package:titan/user/providers/user_list_provider.dart'; + +class UserSearchModal extends HookConsumerWidget { + const UserSearchModal({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final usersNotifier = ref.watch(userList.notifier); + final textController = useTextEditingController(); + + final localizeWithContext = AppLocalizations.of(context)!; + + return BottomModalTemplate( + title: localizeWithContext.adminSelectManager, + type: BottomModalType.main, + child: Column( + children: [ + CustomSearchBar( + autofocus: true, + onSearch: (value) => tokenExpireWrapper(ref, () async { + if (value.isNotEmpty) { + await usersNotifier.filterUsers(value); + textController.text = value; + } else { + usersNotifier.clear(); + textController.clear(); + } + }), + ), + const SizedBox(height: 10), + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 280), + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: SearchResult(queryController: textController), + ), + ), + ], + ), + ); + } +} diff --git a/lib/admin/ui/pages/memberships/association_membership_detail_page/association_membership_detail_page.dart b/lib/admin/ui/pages/membership/association_membership_detail_page/association_membership_detail_page.dart similarity index 61% rename from lib/admin/ui/pages/memberships/association_membership_detail_page/association_membership_detail_page.dart rename to lib/admin/ui/pages/membership/association_membership_detail_page/association_membership_detail_page.dart index 87b343fff3..53349aa6b6 100644 --- a/lib/admin/ui/pages/memberships/association_membership_detail_page/association_membership_detail_page.dart +++ b/lib/admin/ui/pages/membership/association_membership_detail_page/association_membership_detail_page.dart @@ -1,22 +1,25 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/admin/admin.dart'; +import 'package:titan/admin/providers/research_filter_provider.dart'; +import 'package:titan/admin/router.dart'; import 'package:titan/admin/class/user_association_membership.dart'; import 'package:titan/admin/providers/association_membership_filtered_members_provider.dart'; import 'package:titan/admin/providers/association_membership_members_list_provider.dart'; import 'package:titan/admin/providers/association_membership_provider.dart'; import 'package:titan/admin/providers/user_association_membership_provider.dart'; -import 'package:titan/admin/router.dart'; -import 'package:titan/admin/tools/constants.dart'; -import 'package:titan/admin/ui/admin.dart'; -import 'package:titan/admin/ui/pages/memberships/association_membership_detail_page/association_membership_information_editor.dart'; -import 'package:titan/admin/ui/pages/memberships/association_membership_detail_page/association_membership_member_editable_card.dart'; -import 'package:titan/admin/ui/pages/memberships/association_membership_detail_page/research_bar.dart'; -import 'package:titan/admin/ui/pages/memberships/association_membership_detail_page/search_filters.dart'; +import 'package:titan/admin/ui/pages/membership/association_membership_detail_page/association_membership_information_editor.dart'; +import 'package:titan/admin/ui/pages/membership/association_membership_detail_page/association_membership_member_editable_card.dart'; +import 'package:titan/admin/ui/pages/membership/association_membership_detail_page/search_filters.dart'; import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/ui/builders/waiting_button.dart'; import 'package:titan/tools/ui/layouts/refresher.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/icon_button.dart'; +import 'package:titan/tools/ui/styleguide/list_item.dart'; +import 'package:titan/tools/ui/styleguide/searchbar.dart'; class AssociationMembershipEditorPage extends HookConsumerWidget { final scrollKey = GlobalKey(); @@ -24,6 +27,7 @@ class AssociationMembershipEditorPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final filterNotifier = ref.watch(filterProvider.notifier); final associationMembership = ref.watch(associationMembershipProvider); final associationMembershipMemberListNotifier = ref.watch( associationMembershipMembersProvider.notifier, @@ -37,23 +41,24 @@ class AssociationMembershipEditorPage extends HookConsumerWidget { return AdminTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await associationMembershipMemberListNotifier .loadAssociationMembershipMembers(associationMembership.id); }, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 30), + padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( children: [ const SizedBox(height: 20), Container( alignment: Alignment.centerLeft, child: Text( - "${AdminTextConstants.associationMembership} ${associationMembership.name}", + "${AppLocalizations.of(context)!.adminAssociationMembership} ${associationMembership.name}", style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w700, - color: ColorConstants.gradient1, + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorConstants.title, ), ), ), @@ -61,35 +66,17 @@ class AssociationMembershipEditorPage extends HookConsumerWidget { const SizedBox(height: 30), Row( children: [ - const Text( - AdminTextConstants.members, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w700, - color: ColorConstants.gradient1, - ), - ), - const SizedBox(width: 10), Text( - "(${associationMembershipFilteredList.length} ${AdminTextConstants.members})", - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.w700, - color: ColorConstants.gradient1, + "${AppLocalizations.of(context)!.adminMembers} (${associationMembershipFilteredList.length})", + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorConstants.title, ), ), const Spacer(), - WaitingButton( - builder: (child) => Container( - width: 40, - height: 40, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: ColorConstants.gradient1, - ), - child: child, - ), - onTap: () async { + CustomIconButton( + onPressed: () async { userAssociationMembershipNotifier .setUserAssociationMembership( UserAssociationMembership.empty().copyWith( @@ -103,7 +90,7 @@ class AssociationMembershipEditorPage extends HookConsumerWidget { AdminRouter.addEditMember, ); }, - child: const HeroIcon( + icon: const HeroIcon( HeroIcons.plus, size: 30, color: Colors.white, @@ -112,15 +99,36 @@ class AssociationMembershipEditorPage extends HookConsumerWidget { ], ), const SizedBox(height: 10), - ExpansionTile( - title: const Text(AdminTextConstants.filters), - children: const [SearchFilters()], + ListItem( + title: AppLocalizations.of(context)!.adminFilters, + onTap: () async { + FocusScope.of(context).unfocus(); + final ctx = context; + await Future.delayed(Duration(milliseconds: 150)); + if (!ctx.mounted) return; + + await showCustomBottomModal( + context: ctx, + ref: ref, + modal: BottomModalTemplate( + title: AppLocalizations.of(context)!.adminFilters, + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: SearchFilters(), + ), + ), + ); + }, ), const SizedBox(height: 20), - ResearchBar(), + CustomSearchBar( + onSearch: (query) { + filterNotifier.setFilter(query); + }, + ), const SizedBox(height: 10), associationMembershipFilteredList.isEmpty - ? const Text(AdminTextConstants.noMember) + ? Text(AppLocalizations.of(context)!.adminNoMember) : SizedBox( height: 400, child: ListView.builder( diff --git a/lib/admin/ui/pages/membership/association_membership_detail_page/association_membership_information_editor.dart b/lib/admin/ui/pages/membership/association_membership_detail_page/association_membership_information_editor.dart new file mode 100644 index 0000000000..eef898f45f --- /dev/null +++ b/lib/admin/ui/pages/membership/association_membership_detail_page/association_membership_information_editor.dart @@ -0,0 +1,209 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/admin/providers/all_groups_list_provider.dart'; +import 'package:titan/admin/providers/association_membership_list_provider.dart'; +import 'package:titan/admin/providers/association_membership_provider.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; +import 'package:titan/tools/ui/builders/waiting_button.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/list_item.dart'; + +class AssociationMembershipInformationEditor extends HookConsumerWidget { + final scrollKey = GlobalKey(); + AssociationMembershipInformationEditor({super.key}); + + @override + Widget build(context, ref) { + void displayToastWithContext(TypeMsg type, String msg) { + displayToast(context, type, msg); + } + + final associationMembership = ref.watch(associationMembershipProvider); + final associationMembershipNotifier = ref.watch( + associationMembershipProvider.notifier, + ); + final name = useTextEditingController(text: associationMembership.name); + final groups = ref.watch(allGroupList); + final groupIdController = useTextEditingController( + text: associationMembership.managerGroupId, + ); + final associationMembershipListNotifier = ref.watch( + allAssociationMembershipListProvider.notifier, + ); + final key = GlobalKey(); + final localizeWithContext = AppLocalizations.of(context)!; + + groups.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); + + return Form( + key: key, + child: Column( + children: [ + Container( + margin: const EdgeInsets.symmetric(vertical: 10), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + child: TextFormField( + controller: name, + cursorColor: ColorConstants.tertiary, + decoration: InputDecoration( + labelText: AppLocalizations.of(context)!.adminName, + labelStyle: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + suffixIcon: Container( + margin: const EdgeInsets.only(left: 20), + child: const HeroIcon(HeroIcons.pencil), + ), + enabledBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), + ), + focusedBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: ColorConstants.tertiary), + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return AppLocalizations.of( + context, + )!.adminEmptyFieldError; + } + return null; + }, + ), + ), + ], + ), + ), + Align( + alignment: Alignment.centerLeft, + child: Text( + AppLocalizations.of(context)!.adminGroup, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + ListItem( + title: groups + .firstWhere((group) => group.id == groupIdController.text) + .name, + onTap: () async { + FocusScope.of(context).unfocus(); + final ctx = context; + await Future.delayed(Duration(milliseconds: 150)); + if (!ctx.mounted) return; + + await showCustomBottomModal( + context: ctx, + ref: ref, + modal: BottomModalTemplate( + title: localizeWithContext.adminChooseGroupManager, + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 280), + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + children: [ + ...groups.map( + (e) => Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + e.name, + style: const TextStyle(fontSize: 15), + overflow: TextOverflow.ellipsis, + ), + ), + GestureDetector( + onTap: () { + groupIdController.text = e.id; + Navigator.of(ctx).pop(); + }, + child: const HeroIcon(HeroIcons.plus), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + }, + ), + const SizedBox(height: 20), + WaitingButton( + builder: (child) => Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: ColorConstants.tertiary, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: ColorConstants.onTertiary), + ), + child: Center(child: child), + ), + onTap: () async { + if (!key.currentState!.validate()) { + return; + } + + await tokenExpireWrapper(ref, () async { + final updatedAssociationMembershipMsg = AppLocalizations.of( + context, + )!.adminUpdatedAssociationMembership; + final updatingAssociationMembershipErrorMsg = + AppLocalizations.of(context)!.adminUpdatingError; + final value = await associationMembershipListNotifier + .updateAssociationMembership( + associationMembership.copyWith(name: name.text), + ); + if (value) { + associationMembershipNotifier.setAssociationMembership( + associationMembership.copyWith( + name: name.text, + managerGroupId: groupIdController.text, + ), + ); + displayToastWithContext( + TypeMsg.msg, + updatedAssociationMembershipMsg, + ); + } else { + displayToastWithContext( + TypeMsg.msg, + updatingAssociationMembershipErrorMsg, + ); + } + }); + }, + child: Text( + AppLocalizations.of(context)!.adminEdit, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Color.fromARGB(255, 255, 255, 255), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/admin/ui/pages/membership/association_membership_detail_page/association_membership_member_editable_card.dart b/lib/admin/ui/pages/membership/association_membership_detail_page/association_membership_member_editable_card.dart new file mode 100644 index 0000000000..b4c7e3360f --- /dev/null +++ b/lib/admin/ui/pages/membership/association_membership_detail_page/association_membership_member_editable_card.dart @@ -0,0 +1,144 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:titan/admin/class/user_association_membership.dart'; +import 'package:titan/admin/providers/association_membership_members_list_provider.dart'; +import 'package:titan/admin/providers/user_association_membership_provider.dart'; +import 'package:titan/admin/router.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/providers/locale_notifier.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/ui/styleguide/icon_button.dart'; +import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; + +class MemberEditableCard extends HookConsumerWidget { + const MemberEditableCard({super.key, required this.associationMembership}); + + final UserAssociationMembership associationMembership; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final locale = ref.watch(localeProvider); + final associationMembershipMemberListNotifier = ref.watch( + associationMembershipMembersProvider.notifier, + ); + final userAssociationMembershipNotifier = ref.watch( + userAssociationMembershipProvider.notifier, + ); + + final localization = AppLocalizations.of(context)!; + + void displayToastWithContext(TypeMsg type, String msg) { + displayToast(context, type, msg); + } + + return Container( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AutoSizeText( + (associationMembership.user.nickname ?? + "${associationMembership.user.firstname} ${associationMembership.user.name}"), + style: const TextStyle(fontWeight: FontWeight.bold), + minFontSize: 10, + maxFontSize: 15, + overflow: TextOverflow.ellipsis, + ), + associationMembership.user.nickname != null + ? AutoSizeText( + "${associationMembership.user.firstname} ${associationMembership.user.name}", + minFontSize: 10, + maxFontSize: 15, + overflow: TextOverflow.ellipsis, + ) + : const SizedBox(), + ], + ), + ), + Expanded( + child: Column( + children: [ + Text( + DateFormat.yMd( + locale.toString(), + ).format(associationMembership.startDate), + style: const TextStyle(fontSize: 12), + ), + Text( + DateFormat.yMd( + locale.toString(), + ).format(associationMembership.endDate), + style: const TextStyle(fontSize: 12), + ), + ], + ), + ), + CustomIconButton.secondary( + icon: const HeroIcon( + HeroIcons.pencil, + color: ColorConstants.tertiary, + ), + onPressed: () async { + userAssociationMembershipNotifier.setUserAssociationMembership( + associationMembership, + ); + QR.to( + AdminRouter.root + + AdminRouter.associationMemberships + + AdminRouter.detailAssociationMembership + + AdminRouter.addEditMember, + ); + }, + ), + const SizedBox(width: 10), + CustomIconButton.danger( + icon: HeroIcon(HeroIcons.trash, color: Colors.white), + onPressed: () async { + await showDialog( + context: context, + builder: (context) { + return CustomDialogBox( + title: localization.adminDeleteAssociationMember, + descriptions: + localization.adminDeleteAssociationMemberConfirmation, + onYes: () async { + final deletedMemberMsg = + localization.phonebookDeletedMember; + final deleteMemberErrorMsg = + localization.phonebookDeletingError; + await tokenExpireWrapper(ref, () async { + final result = + await associationMembershipMemberListNotifier + .deleteMember(associationMembership); + if (result) { + displayToastWithContext( + TypeMsg.msg, + deletedMemberMsg, + ); + } else { + displayToastWithContext( + TypeMsg.error, + deleteMemberErrorMsg, + ); + } + }); + }, + ); + }, + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/admin/ui/pages/membership/association_membership_detail_page/search_filters.dart b/lib/admin/ui/pages/membership/association_membership_detail_page/search_filters.dart new file mode 100644 index 0000000000..09c08d5e5c --- /dev/null +++ b/lib/admin/ui/pages/membership/association_membership_detail_page/search_filters.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:titan/admin/providers/association_membership_members_list_provider.dart'; +import 'package:titan/admin/providers/association_membership_provider.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; +import 'package:titan/tools/ui/builders/waiting_button.dart'; +import 'package:titan/tools/ui/layouts/add_edit_button_layout.dart'; +import 'package:titan/tools/ui/widgets/date_entry.dart'; +import 'package:titan/l10n/app_localizations.dart'; + +class SearchFilters extends HookConsumerWidget { + const SearchFilters({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final locale = Localizations.localeOf(context); + final associationMembershipMemberListNotifier = ref.watch( + associationMembershipMembersProvider.notifier, + ); + final associationMembership = ref.watch(associationMembershipProvider); + final startMinimal = useTextEditingController(text: ""); + final startMaximal = useTextEditingController( + text: DateFormat.yMd(locale).format(DateTime.now()), + ); + final endMinimal = useTextEditingController( + text: DateFormat.yMd(locale).format(DateTime.now()), + ); + final endMaximal = useTextEditingController(text: ""); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context)!.adminStartDate, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900), + ), + const SizedBox(height: 10), + DateEntry( + label: AppLocalizations.of(context)!.adminStartDateMinimal, + controller: startMinimal, + onTap: () => getOnlyDayDate( + context, + startMinimal, + firstDate: DateTime(2019), + lastDate: DateTime(DateTime.now().year + 7), + ), + ), + const SizedBox(height: 10), + DateEntry( + label: AppLocalizations.of(context)!.adminStartDateMaximal, + controller: startMaximal, + onTap: () => getOnlyDayDate( + context, + startMaximal, + firstDate: DateTime(2019), + lastDate: DateTime(DateTime.now().year + 7), + ), + ), + const SizedBox(height: 20), + Text( + AppLocalizations.of(context)!.adminEndDate, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900), + ), + const SizedBox(height: 10), + DateEntry( + label: AppLocalizations.of(context)!.adminEndDateMinimal, + controller: endMinimal, + onTap: () => getOnlyDayDate( + context, + endMinimal, + firstDate: DateTime(2019), + lastDate: DateTime(DateTime.now().year + 7), + ), + ), + const SizedBox(height: 10), + DateEntry( + label: AppLocalizations.of(context)!.adminEndDateMaximal, + controller: endMaximal, + onTap: () => getOnlyDayDate( + context, + endMaximal, + firstDate: DateTime(2019), + lastDate: DateTime(DateTime.now().year + 7), + ), + ), + const SizedBox(height: 30), + WaitingButton( + onTap: () async { + await tokenExpireWrapper(ref, () async { + await associationMembershipMemberListNotifier + .loadAssociationMembershipMembers( + associationMembership.id, + minimalStartDate: startMinimal.text.isNotEmpty + ? DateTime.parse( + processDateBack( + startMinimal.text, + locale.toString(), + ), + ) + : null, + minimalEndDate: endMinimal.text.isNotEmpty + ? DateTime.parse( + processDateBack(endMinimal.text, locale.toString()), + ) + : null, + maximalStartDate: startMaximal.text.isNotEmpty + ? DateTime.parse( + processDateBack( + startMaximal.text, + locale.toString(), + ), + ) + : null, + maximalEndDate: endMaximal.text.isNotEmpty + ? DateTime.parse( + processDateBack(endMaximal.text, locale.toString()), + ) + : null, + ); + }); + }, + builder: (child) => Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: ColorConstants.tertiary, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: ColorConstants.onTertiary), + ), + child: Center(child: child), + ), + child: Text( + AppLocalizations.of(context)!.adminValidateFilters, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Color.fromARGB(255, 255, 255, 255), + ), + textAlign: TextAlign.center, + ), + ), + SizedBox(height: 20), + WaitingButton( + onTap: () async { + startMaximal.clear(); + startMinimal.clear(); + endMaximal.clear(); + endMinimal.clear(); + await tokenExpireWrapper(ref, () async { + await associationMembershipMemberListNotifier + .loadAssociationMembershipMembers(associationMembership.id); + }); + }, + builder: (child) => AddEditButtonLayout( + colors: const [ColorConstants.main, ColorConstants.onMain], + child: child, + ), + child: Text( + AppLocalizations.of(context)!.adminClearFilters, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Color.fromARGB(255, 255, 255, 255), + ), + textAlign: TextAlign.center, + ), + ), + SizedBox(height: 10), + ], + ); + } +} diff --git a/lib/admin/ui/pages/membership/association_membership_page/add_membership_modal.dart b/lib/admin/ui/pages/membership/association_membership_page/add_membership_modal.dart new file mode 100644 index 0000000000..b126b4ea80 --- /dev/null +++ b/lib/admin/ui/pages/membership/association_membership_page/add_membership_modal.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:titan/admin/class/simple_group.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; +import 'package:titan/tools/ui/styleguide/list_item.dart'; +import 'package:titan/tools/ui/widgets/text_entry.dart'; + +class AddMembershipModal extends HookWidget { + final List groups; + final void Function(SimpleGroup group, String name) onSubmit; + final WidgetRef ref; + + const AddMembershipModal({ + super.key, + required this.groups, + required this.onSubmit, + required this.ref, + }); + + @override + Widget build(BuildContext context) { + final nameController = useTextEditingController(); + final chosenGroup = useState(null); + + final localizeWithContext = AppLocalizations.of(context)!; + + return BottomModalTemplate( + title: localizeWithContext.adminAssociationMembershipsManagement, + child: SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextEntry( + label: localizeWithContext.adminName, + controller: nameController, + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: ListItem( + title: chosenGroup.value == null + ? localizeWithContext.adminChooseGroupManager + : chosenGroup.value!.name, + onTap: () async { + FocusScope.of(context).unfocus(); + final ctx = context; + await Future.delayed(Duration(milliseconds: 150)); + if (!ctx.mounted) return; + + await showCustomBottomModal( + context: ctx, + ref: ref, + modal: BottomModalTemplate( + title: localizeWithContext.adminChooseGroupManager, + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 280), + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + children: [ + ...groups.map( + (e) => Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + e.name, + style: const TextStyle(fontSize: 15), + overflow: TextOverflow.ellipsis, + ), + ), + GestureDetector( + onTap: () { + chosenGroup.value = e; + Navigator.of(ctx).pop(); + }, + child: const HeroIcon(HeroIcons.plus), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + }, + ), + ), + const SizedBox(height: 10), + Button( + text: localizeWithContext.adminAdd, + onPressed: () { + if (chosenGroup.value != null) { + onSubmit(chosenGroup.value!, nameController.text); + } + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/admin/ui/pages/membership/association_membership_page/association_membership_page.dart b/lib/admin/ui/pages/membership/association_membership_page/association_membership_page.dart new file mode 100644 index 0000000000..79ab71b5f9 --- /dev/null +++ b/lib/admin/ui/pages/membership/association_membership_page/association_membership_page.dart @@ -0,0 +1,238 @@ +import 'package:flutter/material.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/admin/admin.dart'; +import 'package:titan/admin/class/association_membership_simple.dart'; +import 'package:titan/admin/providers/all_groups_list_provider.dart'; +import 'package:titan/admin/providers/association_membership_list_provider.dart'; +import 'package:titan/admin/router.dart'; +import 'package:titan/admin/ui/pages/membership/association_membership_page/add_membership_modal.dart'; +import 'package:titan/admin/providers/association_membership_members_list_provider.dart'; +import 'package:titan/admin/providers/association_membership_provider.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; +import 'package:titan/tools/ui/styleguide/icon_button.dart'; +import 'package:titan/tools/ui/styleguide/list_item.dart'; +import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/ui/layouts/refresher.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; + +class AssociationMembershipsPage extends HookConsumerWidget { + const AssociationMembershipsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final associationsMemberships = ref.watch( + allAssociationMembershipListProvider, + ); + final associationMembershipsNotifier = ref.watch( + allAssociationMembershipListProvider.notifier, + ); + final associationMembershipNotifier = ref.watch( + associationMembershipProvider.notifier, + ); + final associationMembershipMembersNotifier = ref.watch( + associationMembershipMembersProvider.notifier, + ); + final groups = ref.watch(allGroupList); + + void displayToastWithContext(TypeMsg type, String msg) { + displayToast(context, type, msg); + } + + void popWithContext() { + Navigator.of(context).pop(); + } + + final localizeWithContext = AppLocalizations.of(context)!; + + return AdminTemplate( + child: Refresher( + controller: ScrollController(), + onRefresh: () async { + await associationMembershipsNotifier.loadAssociationMemberships(); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Column( + children: [ + const SizedBox(height: 20), + Row( + children: [ + Text( + AppLocalizations.of(context)!.adminAssociationMembership, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorConstants.title, + ), + ), + const Spacer(), + CustomIconButton( + icon: HeroIcon( + HeroIcons.plus, + color: Colors.white, + size: 30, + ), + onPressed: () async { + await showCustomBottomModal( + context: context, + ref: ref, + modal: AddMembershipModal( + ref: ref, + groups: groups, + onSubmit: (group, name) { + tokenExpireWrapper(ref, () async { + final value = await associationMembershipsNotifier + .createAssociationMembership( + AssociationMembership.empty().copyWith( + managerGroupId: group.id, + name: name, + ), + ); + if (value) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext + .adminCreatedAssociationMembership, + ); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.adminCreationError, + ); + } + popWithContext(); + }); + }, + ), + ); + }, + ), + ], + ), + SizedBox(height: 10), + AsyncChild( + value: associationsMemberships, + builder: (context, g) { + g.sort( + (a, b) => + a.name.toLowerCase().compareTo(b.name.toLowerCase()), + ); + return Column( + children: [ + Column( + children: [ + ...g.map( + (associationMembership) => Padding( + padding: const EdgeInsets.symmetric( + vertical: 5.0, + ), + child: ListItem( + title: associationMembership.name, + onTap: () async { + await showCustomBottomModal( + context: context, + ref: ref, + modal: BottomModalTemplate( + title: associationMembership.name, + child: Column( + children: [ + Button( + text: localizeWithContext.adminEdit, + onPressed: () { + associationMembershipMembersNotifier + .loadAssociationMembershipMembers( + associationMembership.id, + ); + associationMembershipNotifier + .setAssociationMembership( + associationMembership, + ); + QR.to( + AdminRouter.root + + AdminRouter + .associationMemberships + + AdminRouter + .detailAssociationMembership, + ); + }, + ), + const SizedBox(height: 20), + Button( + text: + localizeWithContext.adminDelete, + type: ButtonType.danger, + onPressed: () async { + await showDialog( + context: context, + builder: (context) { + return CustomDialogBox( + title: AppLocalizations.of( + context, + )!.adminDeleting, + descriptions: + AppLocalizations.of( + context, + )!.adminDeleteAssociationMembership, + onYes: () async { + tokenExpireWrapper(ref, () async { + final deletedAssociationMembershipMsg = + AppLocalizations.of( + context, + )!.adminDeletedAssociationMembership; + final deletingErrorMsg = + AppLocalizations.of( + context, + )!.adminDeletingError; + final value = + await associationMembershipsNotifier + .deleteAssociationMembership( + associationMembership, + ); + if (value) { + displayToastWithContext( + TypeMsg.msg, + deletedAssociationMembershipMsg, + ); + } else { + displayToastWithContext( + TypeMsg.error, + deletingErrorMsg, + ); + } + }); + }, + ); + }, + ); + }, + ), + ], + ), + ), + ); + }, + ), + ), + ), + const SizedBox(height: 20), + ], + ), + ], + ); + }, + loaderColor: ColorConstants.main, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/admin/ui/pages/memberships/add_edit_user_membership_page/search_result.dart b/lib/admin/ui/pages/memberships/add_edit_user_membership_page/search_result.dart deleted file mode 100644 index e9d0994a6b..0000000000 --- a/lib/admin/ui/pages/memberships/add_edit_user_membership_page/search_result.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/providers/user_association_membership_provider.dart'; -import 'package:titan/user/providers/user_list_provider.dart'; - -class SearchResult extends HookConsumerWidget { - final TextEditingController queryController; - const SearchResult({super.key, required this.queryController}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final users = ref.watch(userList); - final usersNotifier = ref.watch(userList.notifier); - final membershipNotifier = ref.watch( - userAssociationMembershipProvider.notifier, - ); - final membership = ref.watch(userAssociationMembershipProvider); - - return users.when( - data: (usersData) { - return Column( - children: usersData - .map( - (user) => GestureDetector( - behavior: HitTestBehavior.opaque, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.1), - offset: const Offset(0, 1), - blurRadius: 4, - spreadRadius: 2, - ), - ], - ), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 14), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container(width: 20), - Expanded( - child: Text( - user.getName(), - style: const TextStyle(fontSize: 13), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - ), - ), - onTap: () { - membershipNotifier.setUserAssociationMembership( - membership.copyWith(user: user, userId: user.id), - ); - queryController.text = user.getName(); - usersNotifier.clear(); - }, - ), - ) - .toList(), - ); - }, - loading: () => const Center(child: CircularProgressIndicator()), - error: (e, s) => Text(e.toString()), - ); - } -} diff --git a/lib/admin/ui/pages/memberships/association_membership_detail_page/association_membership_information_editor.dart b/lib/admin/ui/pages/memberships/association_membership_detail_page/association_membership_information_editor.dart deleted file mode 100644 index 1b19086843..0000000000 --- a/lib/admin/ui/pages/memberships/association_membership_detail_page/association_membership_information_editor.dart +++ /dev/null @@ -1,164 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/providers/all_groups_list_provider.dart'; -import 'package:titan/admin/providers/association_membership_list_provider.dart'; -import 'package:titan/admin/providers/association_membership_provider.dart'; -import 'package:titan/admin/tools/constants.dart'; -import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:titan/tools/token_expire_wrapper.dart'; -import 'package:titan/tools/ui/builders/waiting_button.dart'; -import 'package:titan/tools/ui/layouts/add_edit_button_layout.dart'; - -class AssociationMembershipInformationEditor extends HookConsumerWidget { - final scrollKey = GlobalKey(); - AssociationMembershipInformationEditor({super.key}); - - @override - Widget build(context, ref) { - void displayToastWithContext(TypeMsg type, String msg) { - displayToast(context, type, msg); - } - - final associationMembership = ref.watch(associationMembershipProvider); - final associationMembershipNotifier = ref.watch( - associationMembershipProvider.notifier, - ); - final name = useTextEditingController(text: associationMembership.name); - final groups = ref.watch(allGroupList); - final groupIdController = useTextEditingController( - text: associationMembership.managerGroupId, - ); - final associationMembershipListNotifier = ref.watch( - allAssociationMembershipListProvider.notifier, - ); - final key = GlobalKey(); - - groups.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); - - return Column( - children: [ - Form( - key: key, - child: Column( - children: [ - Container( - margin: const EdgeInsets.symmetric(vertical: 10), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - child: TextFormField( - controller: name, - cursorColor: ColorConstants.gradient1, - decoration: InputDecoration( - labelText: AdminTextConstants.name, - labelStyle: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - suffixIcon: Container( - padding: const EdgeInsets.all(10), - child: const HeroIcon(HeroIcons.pencil), - ), - enabledBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: Colors.transparent), - ), - focusedBorder: const UnderlineInputBorder( - borderSide: BorderSide( - color: ColorConstants.gradient1, - ), - ), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return AdminTextConstants.emptyFieldError; - } - return null; - }, - ), - ), - ], - ), - ), - const Align( - alignment: Alignment.centerLeft, - child: Text( - AdminTextConstants.group, - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - DropdownButtonFormField( - value: groupIdController.text, - onChanged: (String? newValue) { - groupIdController.text = newValue!; - }, - items: groups - .map( - (group) => DropdownMenuItem( - value: group.id, - child: Text(group.name), - ), - ) - .toList(), - decoration: const InputDecoration( - hintText: AdminTextConstants.group, - ), - ), - const SizedBox(height: 20), - WaitingButton( - builder: (child) => AddEditButtonLayout( - colors: const [ - ColorConstants.gradient1, - ColorConstants.gradient2, - ], - child: child, - ), - onTap: () async { - if (!key.currentState!.validate()) { - return; - } - - await tokenExpireWrapper(ref, () async { - final value = await associationMembershipListNotifier - .updateAssociationMembership( - associationMembership.copyWith(name: name.text), - ); - if (value) { - associationMembershipNotifier.setAssociationMembership( - associationMembership.copyWith( - name: name.text, - managerGroupId: groupIdController.text, - ), - ); - displayToastWithContext( - TypeMsg.msg, - AdminTextConstants.updatedAssociationMembership, - ); - } else { - displayToastWithContext( - TypeMsg.msg, - AdminTextConstants.updatingError, - ); - } - }); - }, - child: const Text( - AdminTextConstants.edit, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Color.fromARGB(255, 255, 255, 255), - ), - ), - ), - ], - ), - ), - ], - ); - } -} diff --git a/lib/admin/ui/pages/memberships/association_membership_detail_page/association_membership_member_editable_card.dart b/lib/admin/ui/pages/memberships/association_membership_detail_page/association_membership_member_editable_card.dart deleted file mode 100644 index 1552f951e6..0000000000 --- a/lib/admin/ui/pages/memberships/association_membership_detail_page/association_membership_member_editable_card.dart +++ /dev/null @@ -1,112 +0,0 @@ -import 'package:auto_size_text/auto_size_text.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/class/user_association_membership.dart'; -import 'package:titan/admin/providers/association_membership_members_list_provider.dart'; -import 'package:titan/admin/providers/user_association_membership_provider.dart'; -import 'package:titan/admin/router.dart'; -import 'package:titan/phonebook/ui/pages/admin_page/delete_button.dart'; -import 'package:titan/phonebook/ui/pages/admin_page/edition_button.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:titan/phonebook/tools/constants.dart'; -import 'package:titan/tools/token_expire_wrapper.dart'; -import 'package:qlevar_router/qlevar_router.dart'; - -class MemberEditableCard extends HookConsumerWidget { - const MemberEditableCard({super.key, required this.associationMembership}); - - final UserAssociationMembership associationMembership; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final associationMembershipMemberListNotifier = ref.watch( - associationMembershipMembersProvider.notifier, - ); - final userAssociationMembershipNotifier = ref.watch( - userAssociationMembershipProvider.notifier, - ); - - void displayToastWithContext(TypeMsg type, String msg) { - displayToast(context, type, msg); - } - - return Container( - padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), - margin: const EdgeInsets.symmetric(vertical: 5), - decoration: BoxDecoration( - border: Border.all(), - borderRadius: const BorderRadius.all(Radius.circular(20)), - ), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AutoSizeText( - (associationMembership.user.nickname ?? - "${associationMembership.user.firstname} ${associationMembership.user.name}"), - style: const TextStyle(fontWeight: FontWeight.bold), - minFontSize: 10, - maxFontSize: 15, - ), - const SizedBox(height: 3), - associationMembership.user.nickname != null - ? AutoSizeText( - "${associationMembership.user.firstname} ${associationMembership.user.name}", - minFontSize: 10, - maxFontSize: 15, - ) - : const SizedBox(), - ], - ), - ), - Expanded( - child: Column( - children: [ - Text(associationMembership.startDate.toString().split(" ")[0]), - Text(associationMembership.endDate.toString().split(" ")[0]), - ], - ), - ), - EditionButton( - deactivated: false, - onEdition: () async { - userAssociationMembershipNotifier.setUserAssociationMembership( - associationMembership, - ); - QR.to( - AdminRouter.root + - AdminRouter.associationMemberships + - AdminRouter.detailAssociationMembership + - AdminRouter.addEditMember, - ); - }, - ), - const SizedBox(width: 10), - DeleteButton( - deactivated: false, - deletion: true, - onDelete: () async { - await tokenExpireWrapper(ref, () async { - final result = await associationMembershipMemberListNotifier - .deleteMember(associationMembership); - if (result) { - displayToastWithContext( - TypeMsg.msg, - PhonebookTextConstants.deletedMember, - ); - } else { - displayToastWithContext( - TypeMsg.error, - PhonebookTextConstants.deletingError, - ); - } - }); - }, - ), - ], - ), - ); - } -} diff --git a/lib/admin/ui/pages/memberships/association_membership_detail_page/research_bar.dart b/lib/admin/ui/pages/memberships/association_membership_detail_page/research_bar.dart deleted file mode 100644 index 783cb553fb..0000000000 --- a/lib/admin/ui/pages/memberships/association_membership_detail_page/research_bar.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/providers/research_filter_provider.dart'; -import 'package:titan/admin/tools/constants.dart'; -import 'package:titan/tools/constants.dart'; - -class ResearchBar extends HookConsumerWidget { - const ResearchBar({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final focusNode = useFocusNode(); - final editingController = useTextEditingController(); - final filterNotifier = ref.watch(filterProvider.notifier); - - return TextField( - onChanged: (value) { - filterNotifier.setFilter(value); - }, - focusNode: focusNode, - controller: editingController, - cursorColor: Color(0xFF1D1D1D), - decoration: const InputDecoration( - isDense: true, - suffixIcon: Icon(Icons.search, color: Color(0xFF1D1D1D), size: 30), - label: Text( - AdminTextConstants.research, - style: TextStyle(color: Color(0xFF1D1D1D)), - ), - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide(color: ColorConstants.gradient1), - ), - ), - ); - } -} diff --git a/lib/admin/ui/pages/memberships/association_membership_detail_page/search_filters.dart b/lib/admin/ui/pages/memberships/association_membership_detail_page/search_filters.dart deleted file mode 100644 index 694d366265..0000000000 --- a/lib/admin/ui/pages/memberships/association_membership_detail_page/search_filters.dart +++ /dev/null @@ -1,193 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/providers/association_membership_members_list_provider.dart'; -import 'package:titan/admin/providers/association_membership_provider.dart'; -import 'package:titan/admin/tools/constants.dart'; -import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:titan/tools/token_expire_wrapper.dart'; -import 'package:titan/tools/ui/builders/waiting_button.dart'; -import 'package:titan/tools/ui/layouts/add_edit_button_layout.dart'; -import 'package:titan/tools/ui/widgets/date_entry.dart'; - -class SearchFilters extends HookConsumerWidget { - const SearchFilters({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final associationMembershipMemberListNotifier = ref.watch( - associationMembershipMembersProvider.notifier, - ); - final associationMembership = ref.watch(associationMembershipProvider); - final startMinimal = useTextEditingController(text: ""); - final startMaximal = useTextEditingController( - text: processDate(DateTime.now()), - ); - final endMinimal = useTextEditingController( - text: processDate(DateTime.now()), - ); - final endMaximal = useTextEditingController(text: ""); - - return Column( - children: [ - Row( - children: [ - SizedBox( - width: MediaQuery.of(context).size.width * 0.4, - child: Column( - children: [ - Text( - AdminTextConstants.startDate, - style: const TextStyle(fontSize: 18), - ), - DateEntry( - label: AdminTextConstants.startDateMinimal, - controller: startMinimal, - onTap: () => getOnlyDayDate( - context, - startMinimal, - firstDate: DateTime(2019), - lastDate: DateTime(DateTime.now().year + 7), - ), - ), - const SizedBox(height: 20), - DateEntry( - label: AdminTextConstants.startDateMaximal, - controller: startMaximal, - onTap: () => getOnlyDayDate( - context, - startMaximal, - firstDate: DateTime(2019), - lastDate: DateTime(DateTime.now().year + 7), - ), - ), - ], - ), - ), - const Spacer(), - SizedBox( - width: MediaQuery.of(context).size.width * 0.4, - child: Column( - children: [ - Text( - AdminTextConstants.endDate, - style: const TextStyle(fontSize: 18), - ), - DateEntry( - label: AdminTextConstants.endDateMinimal, - controller: endMinimal, - onTap: () => getOnlyDayDate( - context, - endMinimal, - firstDate: DateTime(2019), - lastDate: DateTime(DateTime.now().year + 7), - ), - ), - const SizedBox(height: 20), - DateEntry( - label: AdminTextConstants.endDateMaximal, - controller: endMaximal, - onTap: () => getOnlyDayDate( - context, - endMaximal, - firstDate: DateTime(2019), - lastDate: DateTime(DateTime.now().year + 7), - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 20), - Row( - children: [ - const Spacer(flex: 2), - SizedBox( - width: MediaQuery.of(context).size.width * 0.3, - child: WaitingButton( - onTap: () async { - await tokenExpireWrapper(ref, () async { - await associationMembershipMemberListNotifier - .loadAssociationMembershipMembers( - associationMembership.id, - minimalStartDate: startMinimal.text.isNotEmpty - ? DateTime.parse( - processDateBack(startMinimal.text), - ) - : null, - minimalEndDate: endMinimal.text.isNotEmpty - ? DateTime.parse(processDateBack(endMinimal.text)) - : null, - maximalStartDate: startMaximal.text.isNotEmpty - ? DateTime.parse( - processDateBack(startMaximal.text), - ) - : null, - maximalEndDate: endMaximal.text.isNotEmpty - ? DateTime.parse(processDateBack(endMaximal.text)) - : null, - ); - }); - }, - builder: (child) => AddEditButtonLayout( - colors: const [ - ColorConstants.gradient1, - ColorConstants.gradient2, - ], - child: child, - ), - child: Text( - AdminTextConstants.validateFilters, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Color.fromARGB(255, 255, 255, 255), - ), - textAlign: TextAlign.center, - ), - ), - ), - const Spacer(), - SizedBox( - width: MediaQuery.of(context).size.width * 0.3, - child: WaitingButton( - onTap: () async { - startMaximal.clear(); - startMinimal.clear(); - endMaximal.clear(); - endMinimal.clear(); - await tokenExpireWrapper(ref, () async { - await associationMembershipMemberListNotifier - .loadAssociationMembershipMembers( - associationMembership.id, - ); - }); - }, - builder: (child) => AddEditButtonLayout( - colors: const [ - ColorConstants.gradient1, - ColorConstants.gradient2, - ], - child: child, - ), - child: Text( - AdminTextConstants.clearFilters, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Color.fromARGB(255, 255, 255, 255), - ), - textAlign: TextAlign.center, - ), - ), - ), - const Spacer(flex: 2), - ], - ), - SizedBox(height: 10), - ], - ); - } -} diff --git a/lib/admin/ui/pages/memberships/association_membership_page/association_membership_button.dart b/lib/admin/ui/pages/memberships/association_membership_page/association_membership_button.dart deleted file mode 100644 index 9fd58fa128..0000000000 --- a/lib/admin/ui/pages/memberships/association_membership_page/association_membership_button.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; - -class AssociationMembershipButton extends StatelessWidget { - final Widget child; - final Color gradient1; - final Color gradient2; - const AssociationMembershipButton({ - super.key, - required this.child, - required this.gradient1, - required this.gradient2, - }); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [gradient1, gradient2], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - boxShadow: [ - BoxShadow( - color: gradient2.withValues(alpha: 0.3), - spreadRadius: 2, - blurRadius: 4, - offset: const Offset(2, 3), - ), - ], - borderRadius: BorderRadius.circular(10), - ), - child: Center(child: child), - ); - } -} diff --git a/lib/admin/ui/pages/memberships/association_membership_page/association_membership_creation_dialog.dart b/lib/admin/ui/pages/memberships/association_membership_page/association_membership_creation_dialog.dart deleted file mode 100644 index 9e0559b996..0000000000 --- a/lib/admin/ui/pages/memberships/association_membership_page/association_membership_creation_dialog.dart +++ /dev/null @@ -1,149 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:titan/admin/class/simple_group.dart'; -import 'package:titan/admin/tools/constants.dart'; -import 'package:titan/tools/constants.dart'; - -class MembershipCreationDialogBox extends StatelessWidget { - static const Color titleColor = ColorConstants.gradient1; - static const Color descriptionColor = Colors.black; - static const Color yesColor = ColorConstants.gradient2; - static const Color noColor = ColorConstants.background2; - - final Function() onYes; - final Function()? onNo; - final TextEditingController nameController; - final TextEditingController groupIdController; - final List groups; - - static const double _padding = 20; - static const double _avatarRadius = 45; - - static const Color background = Color(0xfffafafa); - const MembershipCreationDialogBox({ - super.key, - required this.nameController, - required this.groupIdController, - required this.onYes, - required this.groups, - this.onNo, - }); - - @override - Widget build(BuildContext context) { - groups.sort( - (SimpleGroup a, SimpleGroup b) => - a.name.toLowerCase().compareTo(b.name.toLowerCase()), - ); - groupIdController.text = groups.first.id; - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - MembershipCreationDialogBox._padding, - ), - ), - elevation: 0, - backgroundColor: Colors.transparent, - child: Stack( - children: [ - Container( - padding: const EdgeInsets.all(MembershipCreationDialogBox._padding), - margin: const EdgeInsets.only( - top: MembershipCreationDialogBox._avatarRadius, - ), - decoration: BoxDecoration( - shape: BoxShape.rectangle, - color: MembershipCreationDialogBox.background, - borderRadius: BorderRadius.circular( - MembershipCreationDialogBox._padding, - ), - boxShadow: [ - BoxShadow( - color: Colors.grey.shade700, - offset: const Offset(0, 5), - blurRadius: 5, - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - AdminTextConstants.createAssociationMembership, - style: const TextStyle( - fontSize: 25, - fontWeight: FontWeight.w800, - color: titleColor, - ), - ), - const SizedBox(height: 15), - TextField( - controller: nameController, - decoration: const InputDecoration( - hintText: AdminTextConstants.associationMembershipName, - ), - ), - const SizedBox(height: 20), - DropdownButtonFormField( - value: groupIdController.text, - onChanged: (String? newValue) { - groupIdController.text = newValue!; - }, - items: groups - .map( - (SimpleGroup group) => DropdownMenuItem( - value: group.id, - child: Text(group.name), - ), - ) - .toList(), - decoration: const InputDecoration( - hintText: AdminTextConstants.group, - ), - ), - const SizedBox(height: 20), - Align( - alignment: Alignment.bottomCenter, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - TextButton( - onPressed: () async { - Navigator.of(context).pop(); - await onYes(); - }, - child: const Text( - "Créer", - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: noColor, - ), - ), - ), - TextButton( - onPressed: () async { - if (onNo == null) { - Navigator.of(context).pop(); - } - onNo?.call(); - }, - child: const Text( - "Annuler", - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: yesColor, - ), - ), - ), - ], - ), - ), - ], - ), - ), - ], - ), - ); - } -} diff --git a/lib/admin/ui/pages/memberships/association_membership_page/association_membership_page.dart b/lib/admin/ui/pages/memberships/association_membership_page/association_membership_page.dart deleted file mode 100644 index 8ae53d5400..0000000000 --- a/lib/admin/ui/pages/memberships/association_membership_page/association_membership_page.dart +++ /dev/null @@ -1,201 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/class/association_membership_simple.dart'; -import 'package:titan/admin/providers/all_groups_list_provider.dart'; -import 'package:titan/admin/providers/association_membership_list_provider.dart'; -import 'package:titan/admin/providers/association_membership_members_list_provider.dart'; -import 'package:titan/admin/providers/association_membership_provider.dart'; -import 'package:titan/admin/router.dart'; -import 'package:titan/admin/ui/admin.dart'; -import 'package:titan/admin/ui/components/item_card_ui.dart'; -import 'package:titan/admin/tools/constants.dart'; -import 'package:titan/admin/ui/pages/memberships/association_membership_page/association_membership_creation_dialog.dart'; -import 'package:titan/admin/ui/pages/memberships/association_membership_page/association_membership_ui.dart'; -import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/ui/builders/async_child.dart'; -import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:titan/tools/ui/layouts/refresher.dart'; -import 'package:titan/tools/token_expire_wrapper.dart'; -import 'package:qlevar_router/qlevar_router.dart'; - -class AssociationMembershipsPage extends HookConsumerWidget { - const AssociationMembershipsPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final associationsMemberships = ref.watch( - allAssociationMembershipListProvider, - ); - final associationMembershipsNotifier = ref.watch( - allAssociationMembershipListProvider.notifier, - ); - final associationMembershipNotifier = ref.watch( - associationMembershipProvider.notifier, - ); - final associationMembershipMembersNotifier = ref.watch( - associationMembershipMembersProvider.notifier, - ); - final groups = ref.watch(allGroupList); - - final nameController = TextEditingController(); - final groupIdController = TextEditingController(); - void displayToastWithContext(TypeMsg type, String msg) { - displayToast(context, type, msg); - } - - return AdminTemplate( - child: Refresher( - onRefresh: () async { - await associationMembershipsNotifier.loadAssociationMemberships(); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 30.0), - child: Column( - children: [ - const SizedBox(height: 20), - const Align( - alignment: Alignment.centerLeft, - child: Text( - AdminTextConstants.associationsMemberships, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w700, - color: ColorConstants.gradient1, - ), - ), - ), - const SizedBox(height: 30), - AsyncChild( - value: associationsMemberships, - builder: (context, g) { - g.sort( - (a, b) => - a.name.toLowerCase().compareTo(b.name.toLowerCase()), - ); - return Column( - children: [ - Column( - children: [ - GestureDetector( - onTap: () async { - await showDialog( - context: context, - builder: (context) { - return MembershipCreationDialogBox( - groups: groups, - nameController: nameController, - groupIdController: groupIdController, - onYes: () async { - tokenExpireWrapper(ref, () async { - final value = - await associationMembershipsNotifier - .createAssociationMembership( - AssociationMembership.empty() - .copyWith( - managerGroupId: - groupIdController - .text, - name: - nameController.text, - ), - ); - if (value) { - displayToastWithContext( - TypeMsg.msg, - AdminTextConstants - .createdAssociationMembership, - ); - } else { - displayToastWithContext( - TypeMsg.error, - AdminTextConstants.creationError, - ); - } - }); - }, - ); - }, - ); - }, - child: ItemCardUi( - children: [ - const Spacer(), - HeroIcon( - HeroIcons.plus, - color: Colors.grey.shade700, - size: 40, - ), - const Spacer(), - ], - ), - ), - ...g.map( - (associationMembership) => AssociationMembershipUi( - associationMembership: associationMembership, - onEdit: () { - associationMembershipMembersNotifier - .loadAssociationMembershipMembers( - associationMembership.id, - ); - associationMembershipNotifier - .setAssociationMembership( - associationMembership, - ); - QR.to( - AdminRouter.root + - AdminRouter.associationMemberships + - AdminRouter.detailAssociationMembership, - ); - }, - onDelete: () async { - await showDialog( - context: context, - builder: (context) { - return CustomDialogBox( - title: AdminTextConstants.deleting, - descriptions: AdminTextConstants - .deleteAssociationMembership, - onYes: () async { - tokenExpireWrapper(ref, () async { - final value = - await associationMembershipsNotifier - .deleteAssociationMembership( - associationMembership, - ); - if (value) { - displayToastWithContext( - TypeMsg.msg, - AdminTextConstants - .deletedAssociationMembership, - ); - } else { - displayToastWithContext( - TypeMsg.error, - AdminTextConstants.deletingError, - ); - } - }); - }, - ); - }, - ); - }, - ), - ), - const SizedBox(height: 20), - ], - ), - ], - ); - }, - loaderColor: ColorConstants.gradient1, - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/admin/ui/pages/memberships/association_membership_page/association_membership_ui.dart b/lib/admin/ui/pages/memberships/association_membership_page/association_membership_ui.dart deleted file mode 100644 index 694a576747..0000000000 --- a/lib/admin/ui/pages/memberships/association_membership_page/association_membership_ui.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/class/association_membership_simple.dart'; -import 'package:titan/admin/ui/components/item_card_ui.dart'; -import 'package:titan/admin/ui/pages/memberships/association_membership_page/association_membership_button.dart'; -import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/ui/builders/waiting_button.dart'; - -class AssociationMembershipUi extends HookConsumerWidget { - final AssociationMembership associationMembership; - final void Function() onEdit; - final Future Function() onDelete; - const AssociationMembershipUi({ - super.key, - required this.associationMembership, - required this.onEdit, - required this.onDelete, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return ItemCardUi( - children: [ - const SizedBox(width: 10), - Expanded( - child: Text( - associationMembership.name, - style: const TextStyle( - color: Colors.black, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(width: 10), - Row( - children: [ - GestureDetector( - onTap: onEdit, - child: AssociationMembershipButton( - gradient1: Colors.grey.shade800, - gradient2: Colors.grey.shade900, - child: const HeroIcon(HeroIcons.eye, color: Colors.white), - ), - ), - const SizedBox(width: 10), - WaitingButton( - onTap: onDelete, - builder: (child) => AssociationMembershipButton( - gradient1: ColorConstants.gradient1, - gradient2: ColorConstants.gradient2, - child: child, - ), - child: const HeroIcon(HeroIcons.xMark, color: Colors.white), - ), - ], - ), - ], - ); - } -} diff --git a/lib/admin/ui/pages/structure_page/add_edit_structure_page/add_edit_structure_page.dart b/lib/admin/ui/pages/structure_page/add_edit_structure_page/add_edit_structure_page.dart new file mode 100644 index 0000000000..5ec47ef0e1 --- /dev/null +++ b/lib/admin/ui/pages/structure_page/add_edit_structure_page/add_edit_structure_page.dart @@ -0,0 +1,330 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/admin/admin.dart'; +import 'package:titan/admin/ui/pages/structure_page/add_edit_structure_page/user_search_modal.dart'; +import 'package:titan/admin/class/association_membership_simple.dart'; +import 'package:titan/admin/providers/association_membership_list_provider.dart'; +import 'package:titan/admin/providers/structure_manager_provider.dart'; +import 'package:titan/admin/providers/structure_provider.dart'; +import 'package:titan/paiement/class/structure.dart'; +import 'package:titan/paiement/providers/structure_list_provider.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; +import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; +import 'package:titan/tools/ui/layouts/item_chip.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; +import 'package:titan/tools/ui/styleguide/list_item.dart'; +import 'package:titan/tools/ui/styleguide/text_entry.dart'; +import 'package:titan/user/class/simple_users.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; + +class AddEditStructurePage extends HookConsumerWidget { + const AddEditStructurePage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final key = GlobalKey(); + final structure = ref.watch(structureProvider); + final structureManager = ref.watch(structureManagerProvider); + final structureManagerNotifier = ref.watch( + structureManagerProvider.notifier, + ); + final structureListNotifier = ref.watch(structureListProvider.notifier); + final isEdit = structure.id != ''; + final name = useTextEditingController(text: isEdit ? structure.name : null); + final shortId = useTextEditingController( + text: isEdit ? structure.shortId : null, + ); + final siegeAddressStreet = useTextEditingController( + text: isEdit ? structure.siegeAddressStreet : null, + ); + final siegeAddressCity = useTextEditingController( + text: isEdit ? structure.siegeAddressCity : null, + ); + final siegeAddressZipcode = useTextEditingController( + text: isEdit ? structure.siegeAddressZipcode : null, + ); + final siegeAddressCountry = useTextEditingController( + text: isEdit ? structure.siegeAddressCountry : null, + ); + final siret = useTextEditingController( + text: isEdit ? structure.siret : null, + ); + final iban = useTextEditingController(text: isEdit ? structure.iban : null); + final bic = useTextEditingController(text: isEdit ? structure.bic : null); + final allAssociationMembershipList = ref.watch( + allAssociationMembershipListProvider, + ); + final currentMembership = useState( + isEdit ? structure.associationMembership : AssociationMembership.empty(), + ); + void displayToastWithContext(TypeMsg type, String msg) { + displayToast(context, type, msg); + } + + final localizeWithContext = AppLocalizations.of(context)!; + + return AdminTemplate( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics( + parent: BouncingScrollPhysics(), + ), + child: Form( + key: key, + child: Column( + children: [ + Text( + isEdit + ? localizeWithContext.adminEditStructure + : localizeWithContext.adminAddStructure, + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 20), + TextEntry( + controller: name, + label: localizeWithContext.adminName, + ), + const SizedBox(height: 20), + TextEntry( + controller: shortId, + label: localizeWithContext.adminShortId, + validator: (value) { + if (value.isNotEmpty && value.length != 3) { + return localizeWithContext.adminShortIdError; + } + return null; + }, + ), + const SizedBox(height: 30), + Text( + localizeWithContext.adminSiegeAddress, + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + TextEntry( + controller: siegeAddressStreet, + label: localizeWithContext.adminStreet, + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + flex: 2, + child: TextEntry( + controller: siegeAddressCity, + label: localizeWithContext.adminCity, + ), + ), + const SizedBox(width: 20), + Expanded( + child: TextEntry( + controller: siegeAddressZipcode, + label: localizeWithContext.adminZipcode, + ), + ), + ], + ), + const SizedBox(height: 20), + TextEntry( + controller: siegeAddressCountry, + label: localizeWithContext.adminCountry, + ), + const SizedBox(height: 20), + TextEntry( + controller: siret, + label: localizeWithContext.adminSiret, + validator: (value) { + if (value.isNotEmpty && + value.replaceAll(" ", "").length != 14) { + return localizeWithContext.adminSiretError; + } + return null; + }, + canBeEmpty: true, + ), + const SizedBox(height: 20), + Text( + localizeWithContext.adminBankDetails, + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 20), + TextEntry( + controller: iban, + label: localizeWithContext.adminIban, + validator: (value) { + if (value.isNotEmpty && + value.replaceAll(" ", "").length != 27) { + return localizeWithContext.adminIbanError; + } + return null; + }, + ), + const SizedBox(height: 20), + TextEntry( + controller: bic, + label: localizeWithContext.adminBic, + validator: (value) { + if (value.isNotEmpty && + value.replaceAll(" ", "").length != 11) { + return localizeWithContext.adminBicError; + } + return null; + }, + ), + const SizedBox(height: 20), + AsyncChild( + value: allAssociationMembershipList, + builder: (context, allAssociationMembershipList) { + return HorizontalListView.builder( + height: 40, + items: [ + ...allAssociationMembershipList, + AssociationMembership.empty(), + ], + itemBuilder: (context, associationMembership, index) { + final selected = + currentMembership.value.id == + associationMembership.id; + return ItemChip( + selected: selected, + onTap: () async { + currentMembership.value = associationMembership; + }, + child: Text( + associationMembership.name.toUpperCase(), + style: TextStyle( + color: selected ? Colors.white : Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ); + }, + ); + }, + ), + const SizedBox(height: 20), + isEdit + ? Column( + children: [ + Text( + localizeWithContext.adminManager, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 10), + Text( + structureManager.getName(), + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 10), + ], + ) + : ListItem( + title: structureManager.id.isNotEmpty + ? structureManager.getName() + : localizeWithContext.adminSelectManager, + subtitle: structureManager.getName(), + onTap: () async { + await showCustomBottomModal( + context: context, + ref: ref, + modal: UserSearchModal(), + ); + }, + ), + const SizedBox(height: 20), + Button( + onPressed: () async { + if (key.currentState == null) { + return; + } + if (structureManager.id.isEmpty && !isEdit) { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.adminNoManager, + ); + return; + } + if (key.currentState!.validate()) { + await tokenExpireWrapper(ref, () async { + final editedStructureMsg = isEdit + ? localizeWithContext.adminEditedStructure + : localizeWithContext.adminAddedStructure; + final addedStructureErrorMsg = AppLocalizations.of( + context, + )!.adminAddingError; + final value = isEdit + ? await structureListNotifier.updateStructure( + Structure( + id: structure.id, + shortId: shortId.text, + name: name.text, + siegeAddressStreet: siegeAddressStreet.text, + siegeAddressCity: siegeAddressCity.text, + siegeAddressZipcode: siegeAddressZipcode.text, + siegeAddressCountry: siegeAddressCountry.text, + siret: siret.text, + iban: iban.text, + bic: bic.text, + associationMembership: + currentMembership.value, + managerUser: structureManager, + ), + ) + : await structureListNotifier.createStructure( + Structure( + id: '', + shortId: shortId.text, + name: name.text, + siegeAddressStreet: siegeAddressStreet.text, + siegeAddressCity: siegeAddressCity.text, + siegeAddressZipcode: siegeAddressZipcode.text, + siegeAddressCountry: siegeAddressCountry.text, + siret: siret.text, + iban: iban.text, + bic: bic.text, + associationMembership: + currentMembership.value, + managerUser: structureManager, + ), + ); + if (value) { + QR.back(); + structureManagerNotifier.setUser(SimpleUser.empty()); + displayToastWithContext( + TypeMsg.msg, + editedStructureMsg, + ); + } else { + displayToastWithContext( + TypeMsg.error, + addedStructureErrorMsg, + ); + } + }); + } + }, + text: isEdit + ? localizeWithContext.adminEdit + : localizeWithContext.adminAdd, + ), + SizedBox(height: 80), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/admin/ui/pages/structure_page/add_edit_structure_page/search_result.dart b/lib/admin/ui/pages/structure_page/add_edit_structure_page/search_result.dart new file mode 100644 index 0000000000..9d30ca97f6 --- /dev/null +++ b/lib/admin/ui/pages/structure_page/add_edit_structure_page/search_result.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/admin/providers/structure_manager_provider.dart'; +import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/user/providers/user_list_provider.dart'; + +class SearchResult extends HookConsumerWidget { + final TextEditingController queryController; + const SearchResult({super.key, required this.queryController}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final users = ref.watch(userList); + final usersNotifier = ref.watch(userList.notifier); + final structureManagerNotifier = ref.watch( + structureManagerProvider.notifier, + ); + + return AsyncChild( + value: users, + builder: (context, usersData) { + return Column( + children: usersData + .map( + (user) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + user.getName(), + style: const TextStyle(fontSize: 15), + overflow: TextOverflow.ellipsis, + ), + ), + GestureDetector( + onTap: () { + structureManagerNotifier.setUser(user); + usersNotifier.clear(); + Navigator.of(context).pop(); + }, + child: const HeroIcon(HeroIcons.plus), + ), + ], + ), + ), + ) + .toList(), + ); + }, + ); + } +} diff --git a/lib/admin/ui/pages/structure_page/add_edit_structure_page/user_search_modal.dart b/lib/admin/ui/pages/structure_page/add_edit_structure_page/user_search_modal.dart new file mode 100644 index 0000000000..b6c35f61c5 --- /dev/null +++ b/lib/admin/ui/pages/structure_page/add_edit_structure_page/user_search_modal.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/admin/ui/pages/structure_page/add_edit_structure_page/search_result.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/searchbar.dart'; +import 'package:titan/user/providers/user_list_provider.dart'; + +class UserSearchModal extends HookConsumerWidget { + const UserSearchModal({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final usersNotifier = ref.watch(userList.notifier); + final textController = useTextEditingController(); + + final localizeWithContext = AppLocalizations.of(context)!; + + return BottomModalTemplate( + title: localizeWithContext.adminSelectManager, + type: BottomModalType.main, + child: Column( + children: [ + CustomSearchBar( + autofocus: true, + onSearch: (value) => tokenExpireWrapper(ref, () async { + if (value.isNotEmpty) { + await usersNotifier.filterUsers(value); + textController.text = value; + } else { + usersNotifier.clear(); + textController.clear(); + } + }), + ), + const SizedBox(height: 10), + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 280), + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: SearchResult(queryController: textController), + ), + ), + ], + ), + ); + } +} diff --git a/lib/admin/ui/pages/structure_page/structure_button.dart b/lib/admin/ui/pages/structure_page/structure_button.dart deleted file mode 100644 index 9c29cae36c..0000000000 --- a/lib/admin/ui/pages/structure_page/structure_button.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; - -class GroupButton extends StatelessWidget { - final Widget child; - final Color gradient1; - final Color gradient2; - const GroupButton({ - super.key, - required this.child, - required this.gradient1, - required this.gradient2, - }); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [gradient1, gradient2], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - boxShadow: [ - BoxShadow( - color: gradient2.withValues(alpha: 0.3), - spreadRadius: 2, - blurRadius: 4, - offset: const Offset(2, 3), - ), - ], - borderRadius: BorderRadius.circular(10), - ), - child: Center(child: child), - ); - } -} diff --git a/lib/admin/ui/pages/structure_page/structure_page.dart b/lib/admin/ui/pages/structure_page/structure_page.dart index 82b20900f7..3f302e0a6e 100644 --- a/lib/admin/ui/pages/structure_page/structure_page.dart +++ b/lib/admin/ui/pages/structure_page/structure_page.dart @@ -1,17 +1,19 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/admin/admin.dart'; +import 'package:titan/admin/router.dart'; import 'package:titan/admin/providers/structure_manager_provider.dart'; import 'package:titan/admin/providers/structure_provider.dart'; -import 'package:titan/admin/router.dart'; -import 'package:titan/admin/ui/admin.dart'; -import 'package:titan/admin/ui/components/item_card_ui.dart'; -import 'package:titan/admin/ui/pages/structure_page/structure_ui.dart'; -import 'package:titan/admin/tools/constants.dart'; import 'package:titan/paiement/class/structure.dart'; +import 'package:titan/paiement/providers/bank_account_holder_provider.dart'; import 'package:titan/paiement/providers/structure_list_provider.dart'; import 'package:titan/tools/constants.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; +import 'package:titan/tools/ui/styleguide/icon_button.dart'; +import 'package:titan/tools/ui/styleguide/list_item.dart'; import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/ui/layouts/refresher.dart'; @@ -19,12 +21,18 @@ import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/user/class/simple_users.dart'; import 'package:titan/user/providers/user_list_provider.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:tuple/tuple.dart'; class StructurePage extends HookConsumerWidget { const StructurePage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final bankAccountHolder = ref.watch(bankAccountHolderProvider); + final bankAccountHolderNotifier = ref.watch( + bankAccountHolderProvider.notifier, + ); final structures = ref.watch(structureListProvider); final structuresNotifier = ref.watch(structureListProvider.notifier); final structureNotifier = ref.watch(structureProvider.notifier); @@ -37,31 +45,53 @@ class StructurePage extends HookConsumerWidget { displayToast(context, type, msg); } + final localizeWithContext = AppLocalizations.of(context)!; + return AdminTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await structuresNotifier.getStructures(); }, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 30.0), + padding: const EdgeInsets.symmetric(horizontal: 20.0), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 20), - const Align( - alignment: Alignment.centerLeft, - child: Text( - AdminTextConstants.structures, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w700, - color: ColorConstants.gradient1, + Row( + children: [ + Text( + localizeWithContext.adminStructures, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorConstants.title, + ), + ), + const Spacer(), + CustomIconButton( + icon: HeroIcon( + HeroIcons.plus, + color: Colors.white, + size: 30, + ), + onPressed: () { + structureNotifier.setStructure(Structure.empty()); + structureManagerNotifier.setUser(SimpleUser.empty()); + QR.to( + AdminRouter.root + + AdminRouter.structures + + AdminRouter.addEditStructure, + ); + }, ), - ), + ], ), - const SizedBox(height: 30), - AsyncChild( - value: structures, - builder: (context, structures) { + const SizedBox(height: 10), + Async2Children( + values: Tuple2(structures, bankAccountHolder), + builder: (context, structures, bankAccountHolder) { structures.sort( (a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()), @@ -70,73 +100,136 @@ class StructurePage extends HookConsumerWidget { children: [ Column( children: [ - GestureDetector( - onTap: () { - structureNotifier.setStructure(Structure.empty()); - structureManagerNotifier.setUser( - SimpleUser.empty(), - ); - QR.to( - AdminRouter.root + - AdminRouter.structures + - AdminRouter.addEditStructure, - ); - }, - child: ItemCardUi( - children: [ - const Spacer(), - HeroIcon( - HeroIcons.plus, - color: Colors.grey.shade700, - size: 40, - ), - const Spacer(), - ], + Text( + bankAccountHolder.id == "" + ? localizeWithContext + .adminUndefinedBankAccountHolder + : localizeWithContext.adminBankAccountHolder( + bankAccountHolder.name, + ), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, ), ), + const SizedBox(height: 10), ...structures.map( - (structure) => StructureUi( - group: structure, - onEdit: () { - structureNotifier.setStructure(structure); - structureManagerNotifier.setUser( - structure.managerUser, - ); - QR.to( - AdminRouter.root + - AdminRouter.structures + - AdminRouter.addEditStructure, - ); - }, - onDelete: () async { - await showDialog( - context: context, - builder: (context) { - return CustomDialogBox( - title: AdminTextConstants.deleting, - descriptions: - AdminTextConstants.deleteGroup, - onYes: () async { - tokenExpireWrapper(ref, () async { - final value = await structuresNotifier - .deleteStructure(structure); - if (value) { - displayToastWithContext( - TypeMsg.msg, - AdminTextConstants.deletedGroup, - ); - } else { - displayToastWithContext( - TypeMsg.error, - AdminTextConstants.deletingError, - ); - } - }); - }, - ); - }, - ); - }, + (structure) => Padding( + padding: const EdgeInsets.symmetric( + vertical: 5.0, + ), + child: ListItem( + title: structure.name, + subtitle: structure.managerUser.getName(), + onTap: () async { + await showCustomBottomModal( + context: context, + ref: ref, + modal: BottomModalTemplate( + title: structure.name, + child: Column( + children: [ + Button( + text: localizeWithContext.adminEdit, + onPressed: () { + structureNotifier.setStructure( + structure, + ); + structureManagerNotifier.setUser( + structure.managerUser, + ); + + QR.to( + AdminRouter.root + + AdminRouter.structures + + AdminRouter + .addEditStructure, + ); + Navigator.of(context).pop(); + }, + ), + const SizedBox(height: 10), + Button( + text: localizeWithContext + .adminDefineAsBankAccountHolder, + onPressed: () async { + final value = + await bankAccountHolderNotifier + .updateBankAccountHolder( + structure, + ); + if (value) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext + .adminBankAccountHolderModified, + ); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext + .adminError, + ); + } + }, + ), + const SizedBox(height: 10), + Button( + type: ButtonType.danger, + text: + localizeWithContext.adminDelete, + onPressed: () async { + await showDialog( + context: context, + builder: (context) { + return CustomDialogBox( + title: AppLocalizations.of( + context, + )!.adminDeleting, + descriptions: + AppLocalizations.of( + context, + )!.adminDeleteGroup, + onYes: () async { + final deletedGroupMsg = + localizeWithContext + .adminDeletedGroup; + final deletingErrorMsg = + localizeWithContext + .adminDeletingError; + tokenExpireWrapper(ref, () async { + final value = + await structuresNotifier + .deleteStructure( + structure, + ); + if (value) { + displayToastWithContext( + TypeMsg.msg, + deletedGroupMsg, + ); + } else { + displayToastWithContext( + TypeMsg.error, + deletingErrorMsg, + ); + } + }); + Navigator.of( + context, + ).pop(); + }, + ); + }, + ); + }, + ), + ], + ), + ), + ); + }, + ), ), ), const SizedBox(height: 20), @@ -145,7 +238,6 @@ class StructurePage extends HookConsumerWidget { ], ); }, - loaderColor: ColorConstants.gradient1, ), ], ), diff --git a/lib/admin/ui/pages/structure_page/structure_ui.dart b/lib/admin/ui/pages/structure_page/structure_ui.dart deleted file mode 100644 index cd75918502..0000000000 --- a/lib/admin/ui/pages/structure_page/structure_ui.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/ui/components/item_card_ui.dart'; -import 'package:titan/admin/ui/pages/structure_page/structure_button.dart'; -import 'package:titan/paiement/class/structure.dart'; -import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/ui/builders/waiting_button.dart'; - -class StructureUi extends HookConsumerWidget { - final Structure group; - final void Function() onEdit; - final Future Function() onDelete; - const StructureUi({ - super.key, - required this.group, - required this.onEdit, - required this.onDelete, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return ItemCardUi( - children: [ - const SizedBox(width: 10), - Expanded( - child: Text( - group.name, - style: const TextStyle( - color: Colors.black, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(width: 10), - Row( - children: [ - GestureDetector( - onTap: onEdit, - child: GroupButton( - gradient1: Colors.grey.shade800, - gradient2: Colors.grey.shade900, - child: const HeroIcon(HeroIcons.eye, color: Colors.white), - ), - ), - const SizedBox(width: 10), - WaitingButton( - onTap: onDelete, - builder: (child) => GroupButton( - gradient1: ColorConstants.gradient1, - gradient2: ColorConstants.gradient2, - child: child, - ), - child: const HeroIcon(HeroIcons.xMark, color: Colors.white), - ), - ], - ), - ], - ); - } -} diff --git a/lib/admin/ui/pages/users_management_page/add_user_modal.dart b/lib/admin/ui/pages/users_management_page/add_user_modal.dart new file mode 100644 index 0000000000..7f6173bce3 --- /dev/null +++ b/lib/admin/ui/pages/users_management_page/add_user_modal.dart @@ -0,0 +1,176 @@ +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/admin/class/simple_group.dart'; +import 'package:titan/admin/providers/all_groups_list_provider.dart'; +import 'package:titan/admin/providers/user_invitation_provider.dart'; +import 'package:titan/admin/tools/functions.dart' as admin_utils; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/paiement/ui/pages/main_page/account_card/device_dialog_box.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; +import 'package:titan/tools/ui/builders/waiting_button.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; +import 'package:titan/tools/ui/styleguide/list_item.dart'; + +class AddUsersModalContent extends HookConsumerWidget { + const AddUsersModalContent({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final groups = ref.watch(allGroupList); + final selectedFileName = useState(null); + final mailList = useState>([]); + final chosenGroup = useState(null); + final userInvitationNotifier = ref.watch(userInvitationProvider.notifier); + + void displayToastWithContext(TypeMsg type, String msg) { + displayToast(context, type, msg); + } + + final localizeWithContext = AppLocalizations.of(context)!; + + final navigatorWithContext = Navigator.of(context); + + return BottomModalTemplate( + title: localizeWithContext.adminInviteUsers, + child: Column( + children: [ + Text( + localizeWithContext.adminImportUsersDescription, + style: TextStyle(fontSize: 16), + ), + const SizedBox(height: 20), + Button.secondary( + text: selectedFileName.value ?? localizeWithContext.adminImportList, + onPressed: () async { + final result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['csv'], + ); + if (result != null && result.files.isNotEmpty) { + final file = result.files.first; + + selectedFileName.value = file.name; + + if (file.path != null) { + String fileContent = ''; + if (kIsWeb) { + fileContent = file.bytes != null + ? String.fromCharCodes(file.bytes!) + : ''; + } else { + fileContent = await File(file.path!).readAsString(); + } + mailList.value = admin_utils.parseCsvContent(fileContent); + } + } + }, + ), + const SizedBox(height: 20), + Text( + localizeWithContext.adminInviteUsersCounter(mailList.value.length), + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 30), + ListItem( + title: chosenGroup.value?.name ?? localizeWithContext.adminNoGroup, + onTap: () async { + FocusScope.of(context).unfocus(); + final ctx = context; + await Future.delayed(Duration(milliseconds: 150)); + if (!ctx.mounted) return; + + await showCustomBottomModal( + context: ctx, + ref: ref, + modal: BottomModalTemplate( + title: localizeWithContext.adminChooseGroup, + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: 600), + child: SingleChildScrollView( + child: Column( + children: [ + ...groups.map( + (e) => ListItem( + title: e.name, + onTap: () { + chosenGroup.value = e; + Navigator.of(ctx).pop(); + }, + ), + ), + ], + ), + ), + ), + ), + ); + }, + ), + const SizedBox(height: 30), + WaitingButton( + builder: (child) => Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: ColorConstants.tertiary, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: ColorConstants.onTertiary), + ), + child: Center(child: child), + ), + onTap: () async { + if (selectedFileName.value == null) return; + await tokenExpireWrapper(ref, () async { + final value = await userInvitationNotifier.createUsers( + mailList.value, + chosenGroup.value?.id, + ); + if (value.isEmpty) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.adminInvitedUsers, + ); + navigatorWithContext.pop(); + } else { + if (!context.mounted) return; + await showDialog( + context: context, + builder: (BuildContext context) => DeviceDialogBox( + descriptions: value.map((e) => '- $e').join('\n'), + title: AppLocalizations.of(context)!.adminEmailFailed, + buttonText: "Compris", + onClick: () async { + Navigator.of(context).pop(); + }, + ), + ); + } + }); + }, + child: Text( + localizeWithContext.adminInvite, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Color.fromARGB( + 255, + 255, + 255, + 255, + ).withAlpha(selectedFileName.value == null ? 100 : 255), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/admin/ui/pages/users_management_page/users_management_page.dart b/lib/admin/ui/pages/users_management_page/users_management_page.dart new file mode 100644 index 0000000000..f02a7ee3cc --- /dev/null +++ b/lib/admin/ui/pages/users_management_page/users_management_page.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/admin/ui/pages/users_management_page/add_user_modal.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; + +class UsersManagementPage extends HookConsumerWidget { + const UsersManagementPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final localizeWithContext = AppLocalizations.of(context)!; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Button( + text: localizeWithContext.adminAdd, + onPressed: () async { + Navigator.pop(context); + await showCustomBottomModal( + context: context, + ref: ref, + modal: AddUsersModalContent(), + ); + }, + ), + const SizedBox(height: 20), + Button( + text: localizeWithContext.adminDelete, + onPressed: () async { + Navigator.pop(context); + await showCustomBottomModal( + context: context, + ref: ref, + modal: BottomModalTemplate( + type: BottomModalType.danger, + title: localizeWithContext.adminDeleteUsers, + child: Column( + children: [ + Button( + text: localizeWithContext.adminImportList, + onPressed: () {}, + type: ButtonType.onDanger, + ), + const SizedBox(height: 20), + Button( + text: localizeWithContext.adminDelete, + onPressed: () {}, + disabled: true, + ), + ], + ), + ), + ); + }, + type: ButtonType.danger, + ), + ], + ); + } +} diff --git a/lib/advert/class/advert.dart b/lib/advert/class/advert.dart index 1fe15b5355..b8f781ef3e 100644 --- a/lib/advert/class/advert.dart +++ b/lib/advert/class/advert.dart @@ -1,4 +1,3 @@ -import 'package:titan/advert/class/announcer.dart'; import 'package:titan/tools/functions.dart'; class Advert { @@ -6,16 +5,18 @@ class Advert { late final String title; late final String content; late final DateTime date; - late final Announcer announcer; - late final List tags; + late final String associationId; + late final bool postToFeed; + late final bool notification; Advert({ required this.id, required this.title, required this.content, required this.date, - required this.announcer, - required this.tags, + required this.associationId, + required this.postToFeed, + required this.notification, }); Advert.fromJson(Map json) { @@ -23,8 +24,9 @@ class Advert { title = json["title"]; content = json["content"]; date = processDateFromAPI(json["date"]); - announcer = Announcer.fromJson(json["advertiser"]); - tags = json["tags"].split(', '); + associationId = json["advertiser_id"]; + postToFeed = json["post_to_feed"] ?? false; + notification = json["notification"] ?? true; } Map toJson() { @@ -33,8 +35,9 @@ class Advert { data["title"] = title; data["content"] = content; data["date"] = processDateToAPI(date); - data["advertiser_id"] = announcer.id; - data["tags"] = tags.join(', '); + data["advertiser_id"] = associationId; + data["post_to_feed"] = postToFeed; + data["notification"] = notification; return data; } @@ -43,16 +46,18 @@ class Advert { String? title, String? content, DateTime? date, - Announcer? announcer, - List? tags, + String? associationId, + bool? postToFeed, + bool? notification, }) { return Advert( id: id ?? this.id, title: title ?? this.title, content: content ?? this.content, date: date ?? this.date, - announcer: announcer ?? this.announcer, - tags: tags ?? this.tags, + associationId: associationId ?? this.associationId, + postToFeed: postToFeed ?? this.postToFeed, + notification: notification ?? this.notification, ); } @@ -62,13 +67,14 @@ class Advert { title: "", content: "", date: DateTime.now(), - announcer: Announcer.empty(), - tags: [], + associationId: "", + postToFeed: false, + notification: true, ); } @override String toString() { - return 'Advert{id: $id, title: $title, content: $content, date: $date, announcer: $announcer, tags: $tags}'; + return 'Advert{id: $id, title: $title, content: $content, date: $date, association_id: $associationId, postToFeed: $postToFeed, notification: $notification}'; } } diff --git a/lib/advert/class/announcer.dart b/lib/advert/class/announcer.dart deleted file mode 100644 index e76d7c7c5e..0000000000 --- a/lib/advert/class/announcer.dart +++ /dev/null @@ -1,43 +0,0 @@ -class Announcer { - Announcer({ - required this.name, - required this.groupManagerId, - required this.id, - }); - late final String name; - late final String groupManagerId; - late final String id; - - Announcer.fromJson(Map json) { - name = json['name']; - groupManagerId = json['group_manager_id']; - id = json['id']; - } - - Map toJson() { - final data = {}; - data['name'] = name; - data['group_manager_id'] = groupManagerId; - data['id'] = id; - return data; - } - - Announcer copyWith({String? name, String? groupManagerId, String? id}) { - return Announcer( - name: name ?? this.name, - groupManagerId: groupManagerId ?? this.groupManagerId, - id: id ?? this.id, - ); - } - - Announcer.empty() { - name = ""; - groupManagerId = ""; - id = ""; - } - - @override - String toString() { - return 'Announcer(name: $name, groupManagerId: $groupManagerId, id: $id)'; - } -} diff --git a/lib/advert/class/tag.dart b/lib/advert/class/tag.dart deleted file mode 100644 index 6b19c33d12..0000000000 --- a/lib/advert/class/tag.dart +++ /dev/null @@ -1,31 +0,0 @@ -class Tag { - late final String id; - late final String name; - - Tag({required this.id, required this.name}); - - Tag.fromJson(Map json) { - id = json["id"]; - name = json["name"]; - } - - Map toJson() { - final data = {}; - data["id"] = id; - data["name"] = name; - return data; - } - - Tag copyWith({String? id, String? name}) { - return Tag(id: id ?? this.id, name: name ?? this.name); - } - - static Tag empty() { - return Tag(id: "", name: ""); - } - - @override - String toString() { - return 'Tag{id: $id, name: $name}'; - } -} diff --git a/lib/advert/providers/all_announcer_list_provider.dart b/lib/advert/providers/all_announcer_list_provider.dart deleted file mode 100644 index 95d0fb63a6..0000000000 --- a/lib/advert/providers/all_announcer_list_provider.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:titan/advert/class/announcer.dart'; -import 'package:titan/advert/providers/announcer_list_provider.dart'; - -final allAnnouncerList = Provider>((ref) { - final announcersProvider = ref.watch(announcerListProvider); - return announcersProvider.maybeWhen( - data: (announcers) => announcers, - orElse: () => [], - ); -}); diff --git a/lib/advert/providers/announcer_list_provider.dart b/lib/advert/providers/announcer_list_provider.dart deleted file mode 100644 index 52a223c9df..0000000000 --- a/lib/advert/providers/announcer_list_provider.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/auth/providers/openid_provider.dart'; -import 'package:titan/advert/class/announcer.dart'; -import 'package:titan/advert/repositories/announcer_repository.dart'; -import 'package:titan/tools/providers/list_notifier.dart'; -import 'package:titan/tools/token_expire_wrapper.dart'; - -class AnnouncerListNotifier extends ListNotifier { - final AnnouncerRepository _announcerRepository = AnnouncerRepository(); - AnnouncerListNotifier({required String token}) - : super(const AsyncValue.loading()) { - _announcerRepository.setToken(token); - } - - Future>> loadAllAnnouncerList() async { - return await loadList(_announcerRepository.getAllAnnouncer); - } - - Future>> loadMyAnnouncerList() async { - return await loadList(_announcerRepository.getMyAnnouncer); - } - - Future addAnnouncer(Announcer announcer) async { - return await add(_announcerRepository.createAnnouncer, announcer); - } - - Future updateAnnouncer(Announcer announcer) async { - return await update( - _announcerRepository.updateAnnouncer, - (announcers, announcer) => - announcers - ..[announcers.indexWhere((i) => i.id == announcer.id)] = announcer, - announcer, - ); - } - - Future deleteAnnouncer(Announcer announcer) async { - return await delete( - _announcerRepository.deleteAnnouncer, - (adverts, advert) => adverts..removeWhere((i) => i.id == advert.id), - announcer.id, - announcer, - ); - } -} - -final announcerListProvider = - StateNotifierProvider>>(( - ref, - ) { - final token = ref.watch(tokenProvider); - AnnouncerListNotifier announcerListNotifier = AnnouncerListNotifier( - token: token, - ); - tokenExpireWrapperAuth(ref, () async { - await announcerListNotifier.loadAllAnnouncerList(); - }); - return announcerListNotifier; - }); - -final userAnnouncerListProvider = - StateNotifierProvider>>(( - ref, - ) { - final token = ref.watch(tokenProvider); - AnnouncerListNotifier announcerListNotifier = AnnouncerListNotifier( - token: token, - ); - tokenExpireWrapperAuth(ref, () async { - await announcerListNotifier.loadMyAnnouncerList(); - }); - return announcerListNotifier; - }); diff --git a/lib/advert/providers/announcer_provider.dart b/lib/advert/providers/announcer_provider.dart deleted file mode 100644 index df895c9ca5..0000000000 --- a/lib/advert/providers/announcer_provider.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:titan/advert/class/announcer.dart'; - -final announcerProvider = - StateNotifierProvider>((ref) { - return AnnouncerNotifier(); - }); - -class AnnouncerNotifier extends StateNotifier> { - AnnouncerNotifier() : super([]); - - void addAnnouncer(Announcer i) { - state.add(i); - state = state.sublist(0); - } - - void removeAnnouncer(Announcer i) { - state = state.where((element) => element.id != i.id).toList(); - } - - void clearAnnouncer() { - state = []; - } -} diff --git a/lib/advert/providers/is_advert_admin_provider.dart b/lib/advert/providers/is_advert_admin_provider.dart deleted file mode 100644 index 3dec527d3a..0000000000 --- a/lib/advert/providers/is_advert_admin_provider.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/advert/providers/announcer_list_provider.dart'; - -final isAdvertAdminProvider = StateProvider((ref) { - final me = ref.watch(userAnnouncerListProvider); - return me.maybeWhen(data: (data) => data.isNotEmpty, orElse: () => false); -}); diff --git a/lib/advert/providers/selected_association_provider.dart b/lib/advert/providers/selected_association_provider.dart new file mode 100644 index 0000000000..5a84bf07ad --- /dev/null +++ b/lib/advert/providers/selected_association_provider.dart @@ -0,0 +1,24 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/admin/class/assocation.dart'; + +final selectedAssociationProvider = + StateNotifierProvider>((ref) { + return AssociationNotifier(); + }); + +class AssociationNotifier extends StateNotifier> { + AssociationNotifier() : super([]); + + void addAssociation(Association i) { + state.add(i); + state = state.sublist(0); + } + + void removeAssociation(Association i) { + state = state.where((element) => element.id != i.id).toList(); + } + + void clearAssociation() { + state = []; + } +} diff --git a/lib/advert/providers/user_association_list_provider.dart b/lib/advert/providers/user_association_list_provider.dart new file mode 100644 index 0000000000..a078544d04 --- /dev/null +++ b/lib/advert/providers/user_association_list_provider.dart @@ -0,0 +1,28 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/auth/providers/openid_provider.dart'; +import 'package:titan/advert/class/advert.dart'; +import 'package:titan/advert/repositories/advert_repository.dart'; +import 'package:titan/tools/providers/list_notifier.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; + +class AdvertListNotifier extends ListNotifier { + AdvertRepository repository = AdvertRepository(); + AdvertListNotifier({required String token}) + : super(const AsyncValue.loading()) { + repository.setToken(token); + } + + Future>> loadUserAssmicationList() async { + return await loadList(repository.getAllAdvert); + } +} + +final advertListProvider = + StateNotifierProvider>>((ref) { + final token = ref.watch(tokenProvider); + AdvertListNotifier notifier = AdvertListNotifier(token: token); + tokenExpireWrapperAuth(ref, () async { + await notifier.loadUserAssmicationList(); + }); + return notifier; + }); diff --git a/lib/advert/repositories/advert_repository.dart b/lib/advert/repositories/advert_repository.dart index befbc7da72..26b452d9e9 100644 --- a/lib/advert/repositories/advert_repository.dart +++ b/lib/advert/repositories/advert_repository.dart @@ -12,6 +12,12 @@ class AdvertRepository extends Repository { )).map((e) => Advert.fromJson(e)).toList(); } + Future> getAllAdminAdvert() async { + return (await getList( + suffix: 'adverts/admin', + )).map((e) => Advert.fromJson(e)).toList(); + } + Future getAdvert(String id) async { return Advert.fromJson(await getOne(id)); } diff --git a/lib/advert/repositories/announcer_repository.dart b/lib/advert/repositories/announcer_repository.dart deleted file mode 100644 index 40b611a357..0000000000 --- a/lib/advert/repositories/announcer_repository.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:titan/advert/class/announcer.dart'; -import 'package:titan/tools/repository/repository.dart'; - -class AnnouncerRepository extends Repository { - @override - // ignore: overridden_fields - final ext = "advert/"; - - Future> getAllAnnouncer() async { - return List.from( - (await getList(suffix: "advertisers")).map((x) => Announcer.fromJson(x)), - ); - } - - Future> getMyAnnouncer() async { - return List.from( - (await getList( - suffix: "me/advertisers", - )).map((x) => Announcer.fromJson(x)), - ); - } - - Future getAnnouncer(String id) async { - return Announcer.fromJson(await getOne("advertisers/$id")); - } - - Future createAnnouncer(Announcer announcer) async { - return Announcer.fromJson( - await create(announcer.toJson(), suffix: "advertisers"), - ); - } - - Future updateAnnouncer(Announcer announcer) async { - return await update(announcer.toJson(), "advertisers/${announcer.id}"); - } - - Future deleteAnnouncer(String announcerId) async { - return await delete("advertisers/$announcerId"); - } -} diff --git a/lib/advert/repositories/tag_repository.dart b/lib/advert/repositories/tag_repository.dart deleted file mode 100644 index 01f25b0d3e..0000000000 --- a/lib/advert/repositories/tag_repository.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:titan/advert/class/tag.dart'; -import 'package:titan/tools/repository/repository.dart'; - -class TagRepository extends Repository { - @override - // ignore: overridden_fields - final ext = "advert/tag/"; - - Future> getAllTag() async { - return (await getList()).map((e) => Tag.fromJson(e)).toList(); - } - - Future getTag(String id) async { - return Tag.fromJson(await getOne(id)); - } - - Future addTag(Tag tag) async { - return Tag.fromJson(await create(tag.toJson())); - } - - Future deleteTag(String id) async { - return await delete("/$id"); - } -} diff --git a/lib/advert/router.dart b/lib/advert/router.dart index 769f6ee5ee..48b95dde3b 100644 --- a/lib/advert/router.dart +++ b/lib/advert/router.dart @@ -1,19 +1,15 @@ -import 'package:either_dart/either.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:titan/admin/providers/is_admin_provider.dart'; -import 'package:titan/advert/providers/is_advert_admin_provider.dart'; + import 'package:titan/advert/ui/pages/admin_page/admin_page.dart' deferred as admin_page; -import 'package:titan/advert/ui/pages/detail_page/detail.dart' - deferred as detail_page; import 'package:titan/advert/ui/pages/form_page/add_edit_advert_page.dart' deferred as add_edit_advert_page; -import 'package:titan/advert/ui/pages/form_page/add_rem_announcer_page.dart' - deferred as add_rem_announcer_page; import 'package:titan/advert/ui/pages/main_page/main_page.dart' deferred as main_page; -import 'package:titan/drawer/class/module.dart'; +import 'package:titan/feed/providers/is_user_a_member_of_an_association.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/class/module.dart'; import 'package:titan/tools/middlewares/admin_middleware.dart'; import 'package:titan/tools/middlewares/authenticated_middleware.dart'; import 'package:titan/tools/middlewares/deferred_middleware.dart'; @@ -24,13 +20,11 @@ class AdvertRouter { static const String root = '/advert'; static const String admin = '/admin'; static const String addEditAdvert = '/add_edit_advert'; - static const String addRemAnnouncer = '/add_remove_announcer'; - static const String detail = '/detail'; static final Module module = Module( - name: "Annonce", - icon: const Left(HeroIcons.megaphone), + getName: (context) => AppLocalizations.of(context)!.moduleAdvert, + getDescription: (context) => + AppLocalizations.of(context)!.moduleAdvertDescription, root: AdvertRouter.root, - selected: false, ); AdvertRouter(this.ref); @@ -42,12 +36,16 @@ class AdvertRouter { AuthenticatedMiddleware(ref), DeferredLoadingMiddleware(main_page.loadLibrary), ], + pageType: QCustomPage( + transitionsBuilder: (_, animation, _, child) => + FadeTransition(opacity: animation, child: child), + ), children: [ QRoute( path: admin, builder: () => admin_page.AdvertAdminPage(), middleware: [ - AdminMiddleware(ref, isAdvertAdminProvider), + AdminMiddleware(ref, isUserAMemberOfAnAssociationProvider), DeferredLoadingMiddleware(admin_page.loadLibrary), ], children: [ @@ -60,19 +58,6 @@ class AdvertRouter { ), ], ), - QRoute( - path: detail, - builder: () => detail_page.AdvertDetailPage(), - middleware: [DeferredLoadingMiddleware(detail_page.loadLibrary)], - ), - QRoute( - path: addRemAnnouncer, - builder: () => add_rem_announcer_page.AddRemAnnouncerPage(), - middleware: [ - AdminMiddleware(ref, isAdminProvider), - DeferredLoadingMiddleware(add_rem_announcer_page.loadLibrary), - ], - ), ], ); } diff --git a/lib/advert/tools/constants.dart b/lib/advert/tools/constants.dart index d1f8eb8100..43a97337d8 100644 --- a/lib/advert/tools/constants.dart +++ b/lib/advert/tools/constants.dart @@ -1,51 +1,5 @@ import 'dart:ui'; -class AdvertTextConstants { - static const String add = 'Ajouter'; - static const String addedAdvert = 'Annonce publiée'; - static const String addedAnnouncer = 'Annonceur ajouté'; - static const String addingError = 'Erreur lors de l\'ajout'; - static const String admin = 'Admin'; - static const String advert = 'Annonce'; - static const String choosingAnnouncer = 'Veuillez choisir un annonceur'; - static const String choosingPoster = 'Veuillez choisir une image'; - static const String content = 'Contenu'; - static const String deleteAdvert = 'Supprimer l\'annonce ?'; - static const String deleteAnnouncer = 'Supprimer l\'annonceur ?'; - static const String deleting = 'Suppression'; - static const String edit = 'Modifier'; - static const String editedAdvert = 'Annonce modifiée'; - static const String editingError = 'Erreur lors de la modification'; - static const String groupAdvert = 'Groupe'; - static const String incorrectOrMissingFields = - 'Champs incorrects ou manquants'; - static const String invalidNumber = 'Veuillez entrer un nombre'; - static const String management = 'Gestion'; - static const String modifyAnnouncingGroup = 'Modifier un groupe d\'annonce'; - static const String noMoreAnnouncer = 'Aucun annonceur n\'est disponible'; - static const String noValue = 'Veuillez entrer une valeur'; - static const String positiveNumber = 'Veuillez entrer un nombre positif'; - static const String removedAnnouncer = 'Annonceur supprimé'; - static const String removingError = 'Erreur lors de la suppression'; - static const String tags = 'Tags'; - static const String title = 'Titre'; - - static const List months = [ - 'Janv.', - 'Févr.', - 'Mars', - 'Avr.', - 'Mai', - 'Juin', - 'Juill.', - 'Août', - 'Sept.', - 'Oct.', - 'Nov.', - 'Déc.', - ]; -} - class AdvertColorConstants { static const Color redGradient1 = Color(0xFF9E131F); static const Color redGradient2 = Color(0xFF590512); diff --git a/lib/advert/tools/functions.dart b/lib/advert/tools/functions.dart index fd26da6d2e..e07b1d9741 100644 --- a/lib/advert/tools/functions.dart +++ b/lib/advert/tools/functions.dart @@ -1,4 +1,5 @@ -import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:titan/l10n/app_localizations.dart'; Color invert(Color color) { return Color.from( @@ -18,3 +19,21 @@ Color generateColor(String uuid) { double luminance = color.computeLuminance(); return luminance < 0.5 ? color : invert(color); } + +List getLocalizedMonths(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return [ + l10n.advertMonthJan, + l10n.advertMonthFeb, + l10n.advertMonthMar, + l10n.advertMonthApr, + l10n.advertMonthMay, + l10n.advertMonthJun, + l10n.advertMonthJul, + l10n.advertMonthAug, + l10n.advertMonthSep, + l10n.advertMonthOct, + l10n.advertMonthNov, + l10n.advertMonthDec, + ]; +} diff --git a/lib/advert/ui/components/advert_card.dart b/lib/advert/ui/components/advert_card.dart deleted file mode 100644 index dfbce33dfc..0000000000 --- a/lib/advert/ui/components/advert_card.dart +++ /dev/null @@ -1,318 +0,0 @@ -import 'package:auto_size_text/auto_size_text.dart'; -import 'package:flutter/material.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:intl/intl.dart'; -import 'package:titan/advert/class/advert.dart'; -import 'package:titan/advert/providers/advert_poster_provider.dart'; -import 'package:titan/advert/providers/advert_posters_provider.dart'; -import 'package:titan/advert/tools/constants.dart'; -import 'package:titan/cinema/tools/functions.dart'; -import 'package:titan/drawer/providers/is_web_format_provider.dart'; -import 'package:titan/tools/ui/builders/auto_loader_child.dart'; -import 'package:titan/tools/ui/widgets/text_with_hyper_link.dart'; - -class AdvertCard extends HookConsumerWidget { - final VoidCallback onTap; - final Advert advert; - - const AdvertCard({super.key, required this.onTap, required this.advert}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - double width = 300; - double height = 300; - double imageHeight = 175; - double maxHeight = MediaQuery.of(context).size.height - 344; - final posters = ref.watch( - advertPostersProvider.select((advertPosters) => advertPosters[advert.id]), - ); - final advertPostersNotifier = ref.watch(advertPostersProvider.notifier); - final posterNotifier = ref.watch(advertPosterProvider.notifier); - final isWebFormat = ref.watch(isWebFormatProvider); - return GestureDetector( - onTap: () { - if (!isWebFormat) { - onTap(); - } - }, - child: Container( - margin: const EdgeInsets.all(10), - padding: EdgeInsets.all(isWebFormat ? 50 : 0), - child: isWebFormat - ? Container( - height: maxHeight, - width: double.infinity, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(30), - child: AspectRatio( - aspectRatio: 2 / 3, - child: AutoLoaderChild( - group: posters, - notifier: advertPostersNotifier, - mapKey: advert.id, - loader: (advertId) => - posterNotifier.getAdvertPoster(advertId), - loadingBuilder: (context) => - HeroIcon(HeroIcons.photo, size: width), - dataBuilder: (context, value) => Image( - image: value.first.image, - fit: BoxFit.cover, // use this - ), - ), - ), - ), - const SizedBox(width: 50), - Expanded( - child: Column( - children: [ - AutoSizeText( - advert.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 10), - AutoSizeText( - formatDate(advert.date), - maxLines: 1, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 16), - ), - const SizedBox(height: 10), - Expanded( - child: SingleChildScrollView( - child: TextWithHyperLink( - advert.content, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 16), - ), - ), - ), - ], - ), - ), - const SizedBox(width: 50), - ], - ), - ) - : Container( - margin: const EdgeInsets.symmetric(vertical: 10), - width: width, - height: height, - decoration: BoxDecoration( - color: Colors.white, - boxShadow: const [ - BoxShadow( - blurRadius: 5, - color: Color(0x33000000), - offset: Offset(2, 2), - spreadRadius: 3, - ), - ], - borderRadius: BorderRadius.circular(20), - ), - child: Stack( - children: [ - Column( - children: [ - AutoLoaderChild( - group: posters, - notifier: advertPostersNotifier, - mapKey: advert.id, - loader: (advertId) => - posterNotifier.getAdvertPoster(advertId), - loadingBuilder: (context) => Container( - width: width, - height: imageHeight, - decoration: const BoxDecoration( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - child: HeroIcon(HeroIcons.photo, size: width), - ), - dataBuilder: (context, value) => Container( - width: width, - height: imageHeight, - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - image: DecorationImage( - image: value.first.image, - fit: BoxFit.cover, - ), - ), - ), - ), - Container( - padding: const EdgeInsets.only( - top: 20, - left: 10, - right: 10, - ), - width: width, - height: height - imageHeight, - child: Column( - children: [ - Column( - children: [ - Container( - width: width, - margin: const EdgeInsets.only(bottom: 5), - child: AutoSizeText( - advert.title.trim(), - textAlign: TextAlign.left, - overflow: TextOverflow.ellipsis, - maxLines: 1, - minFontSize: 15, - style: const TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - TextWithHyperLink( - advert.content.trim(), - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.justify, - maxLines: 3, - minFontSize: 13, - maxFontSize: 15, - style: const TextStyle( - color: Colors.black, - fontSize: 15, - ), - ), - ], - ), - ), - ], - ), - Positioned( - top: imageHeight - 40, - left: 15, - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all( - Radius.circular(8), - ), - boxShadow: [ - BoxShadow( - blurRadius: 5, - color: Colors.black.withValues(alpha: 0.3), - offset: const Offset(2, 2), - spreadRadius: 3, - ), - ], - ), - child: ClipRRect( - borderRadius: const BorderRadius.all( - Radius.circular(8), - ), - child: Container( - color: Colors.white, - height: 50, - width: 50, - padding: const EdgeInsets.symmetric( - horizontal: 5, - vertical: 5, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - AutoSizeText( - DateFormat('dd').format(advert.date), - textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.black, - fontSize: 22, - fontWeight: FontWeight.bold, - height: 1.0, - ), - ), - AutoSizeText( - AdvertTextConstants.months[int.parse( - DateFormat('MM').format(advert.date), - ) - - 1], - textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.black, - fontSize: 11, - fontWeight: FontWeight.bold, - height: 1.0, - ), - ), - ], - ), - ), - ), - ), - ), - Positioned( - top: imageHeight - 20, - right: 15, - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all( - Radius.circular(8), - ), - boxShadow: [ - BoxShadow( - blurRadius: 5, - color: Colors.black.withValues(alpha: 0.3), - offset: const Offset(2, 2), - spreadRadius: 3, - ), - ], - ), - child: ClipRRect( - borderRadius: const BorderRadius.all( - Radius.circular(8), - ), - child: Container( - color: Colors.white, - height: 30, - padding: const EdgeInsets.symmetric( - vertical: 5, - horizontal: 10, - ), - alignment: Alignment.center, - child: AutoSizeText( - advert.announcer.name, - textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.black, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/advert/ui/components/announcer_bar.dart b/lib/advert/ui/components/announcer_bar.dart deleted file mode 100644 index ce1a331382..0000000000 --- a/lib/advert/ui/components/announcer_bar.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/advert/providers/announcer_provider.dart'; -import 'package:titan/advert/providers/announcer_list_provider.dart'; -import 'package:titan/tools/ui/builders/async_child.dart'; -import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; -import 'package:titan/tools/ui/layouts/item_chip.dart'; - -class AnnouncerBar extends HookConsumerWidget { - final bool useUserAnnouncers; - final bool multipleSelect; - final bool isNotClickable; - const AnnouncerBar({ - super.key, - required this.multipleSelect, - required this.useUserAnnouncers, - this.isNotClickable = false, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final selected = ref.watch(announcerProvider); - final selectedId = selected.map((e) => e.id).toList(); - final selectedNotifier = ref.read(announcerProvider.notifier); - final announcerList = useUserAnnouncers - ? ref.watch(userAnnouncerListProvider) - : ref.watch(announcerListProvider); - final darkerColor = (isNotClickable) ? Colors.grey[800] : Colors.black; - - return AsyncChild( - value: announcerList, - builder: (context, userAnnouncers) => HorizontalListView.builder( - height: 40, - items: userAnnouncers, - itemBuilder: (context, e, i) => GestureDetector( - onTap: () { - if (isNotClickable) { - return; - } - if (multipleSelect) { - selectedId.contains(e.id) - ? selectedNotifier.removeAnnouncer(e) - : selectedNotifier.addAnnouncer(e); - } else { - bool contain = selectedId.contains(e.id); - selectedNotifier.clearAnnouncer(); - if (!contain) { - selectedNotifier.addAnnouncer(e); - } - } - }, - child: ItemChip( - selected: selectedId.contains(e.id), - child: Text( - e.name, - style: TextStyle( - color: selectedId.contains(e.id) ? Colors.white : darkerColor, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/advert/ui/components/association_bar.dart b/lib/advert/ui/components/association_bar.dart new file mode 100644 index 0000000000..2d85a55942 --- /dev/null +++ b/lib/advert/ui/components/association_bar.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/admin/providers/assocation_list_provider.dart'; +import 'package:titan/admin/providers/my_association_list_provider.dart'; +import 'package:titan/advert/providers/selected_association_provider.dart'; +import 'package:titan/advert/ui/components/association_item.dart'; +import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; + +class AssociationBar extends HookConsumerWidget { + final bool useUserAssociations; + final bool multipleSelect; + final bool isNotClickable; + const AssociationBar({ + super.key, + required this.multipleSelect, + required this.useUserAssociations, + this.isNotClickable = false, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selected = ref.watch(selectedAssociationProvider); + final selectedId = selected.map((e) => e.id).toList(); + final selectedNotifier = ref.read(selectedAssociationProvider.notifier); + final associationList = useUserAssociations + ? ref.watch(asyncMyAssociationListProvider) + : ref.watch(associationListProvider); + return AsyncChild( + value: associationList, + builder: (context, userAssociations) { + return HorizontalListView.builder( + height: 66, + items: userAssociations, + itemBuilder: (context, e, i) { + final selected = selectedId.contains(e.id); + return AssociationItem( + onTap: () { + if (isNotClickable) { + return; + } + if (multipleSelect) { + selected + ? selectedNotifier.removeAssociation(e) + : selectedNotifier.addAssociation(e); + } else { + selectedNotifier.clearAssociation(); + if (!selected) { + selectedNotifier.addAssociation(e); + } + } + }, + associationId: e.id, + name: e.name, + avatarName: () { + try { + final name = e.name.trim(); + if (name.length <= 3) { + return name.toUpperCase(); + } + final parts = name + .split(RegExp(r"[ '\s]+")) + .where((s) => s.isNotEmpty) + .toList(); + + if (parts.length >= 2) { + return parts.take(2).map((s) => s[0].toUpperCase()).join(); + } + return name.substring(0, 3).toUpperCase(); + } catch (_) { + return (e.name.length >= 3) + ? e.name.substring(0, 3).toUpperCase() + : e.name.toUpperCase(); + } + }(), + selected: selected, + ); + }, + ); + }, + ); + } +} diff --git a/lib/advert/ui/components/association_item.dart b/lib/advert/ui/components/association_item.dart new file mode 100644 index 0000000000..7167789e0d --- /dev/null +++ b/lib/advert/ui/components/association_item.dart @@ -0,0 +1,107 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/admin/providers/association_logo_provider.dart'; +import 'package:titan/admin/providers/associations_logo_map_provider.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/ui/builders/auto_loader_child.dart'; + +class AssociationItem extends ConsumerWidget { + final String name, avatarName, associationId; + final bool selected; + final VoidCallback onTap; + const AssociationItem({ + super.key, + + required this.name, + required this.onTap, + required this.selected, + required this.avatarName, + required this.associationId, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final associationLogo = ref.watch( + associationLogoMapProvider.select((value) => value[associationId]), + ); + final associationLogoMapNotifier = ref.watch( + associationLogoMapProvider.notifier, + ); + final associationLogoNotifier = ref.watch(associationLogoProvider.notifier); + return GestureDetector( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 5.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Center( + child: AutoLoaderChild( + group: associationLogo, + notifier: associationLogoMapNotifier, + mapKey: associationId, + loader: (associationId) => + associationLogoNotifier.getAssociationLogo(associationId), + dataBuilder: (context, data) => Container( + width: 44, + height: 44, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: selected + ? Border.all(color: ColorConstants.tertiary, width: 3) + : null, + image: DecorationImage( + image: data.first.image, + fit: BoxFit.cover, + ), + ), + ), + orElseBuilder: (context, stack) => Container( + width: 44, + height: 44, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: selected + ? Border.all(color: ColorConstants.tertiary, width: 3) + : null, + color: Colors.grey.shade100, + ), + child: Center( + child: Text( + avatarName, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: selected + ? ColorConstants.onTertiary + : ColorConstants.tertiary, + ), + ), + ), + ), + ), + ), + const SizedBox(height: 4), + SizedBox( + width: 55, + child: AutoSizeText( + name, + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12, + color: selected + ? ColorConstants.onTertiary + : ColorConstants.tertiary, + fontWeight: selected ? FontWeight.bold : FontWeight.normal, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/advert/ui/components/special_action_button.dart b/lib/advert/ui/components/special_action_button.dart new file mode 100644 index 0000000000..b490804672 --- /dev/null +++ b/lib/advert/ui/components/special_action_button.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:titan/tools/constants.dart'; + +class SpecialActionButton extends StatelessWidget { + final String name; + final VoidCallback onTap; + final Widget icon; + const SpecialActionButton({ + super.key, + required this.name, + required this.onTap, + required this.icon, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 5.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 45, + height: 45, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: ColorConstants.onTertiary, width: 2), + color: ColorConstants.tertiary, + ), + child: Center(child: icon), + ), + const SizedBox(height: 4), + SizedBox( + width: 55, + child: Text( + name, + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12, + color: ColorConstants.onTertiary, + fontWeight: FontWeight.normal, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/advert/ui/components/tag_chip.dart b/lib/advert/ui/components/tag_chip.dart deleted file mode 100644 index fba084f3c5..0000000000 --- a/lib/advert/ui/components/tag_chip.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:titan/advert/tools/functions.dart'; - -class TagChip extends StatelessWidget { - final String tagName; - - const TagChip({super.key, required this.tagName}); - - @override - Widget build(BuildContext context) { - Color bgColor = generateColor(tagName); - Color borderColor = bgColor.computeLuminance() > 0.1 - ? bgColor - : Colors.white; - Color darkerBgColor = Color.from( - alpha: bgColor.a, - red: max(bgColor.r - 0.12, 0), // 0.12 = 30/255 - green: max(bgColor.g - 0.12, 0), - blue: max(bgColor.b - 0.12, 0), - ); - - return Container( - margin: const EdgeInsets.symmetric(horizontal: 5), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5), - height: 30, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomLeft, - colors: [bgColor, darkerBgColor], - stops: const [0.7, 1.0], - ), - borderRadius: BorderRadius.circular(20), - border: Border.all(color: borderColor), - ), - child: Align( - alignment: Alignment.center, - child: Text( - tagName, - textAlign: TextAlign.center, - maxLines: 1, - style: const TextStyle(color: Colors.white, fontSize: 13), - ), - ), - ); - } -} diff --git a/lib/advert/ui/pages/admin_page/admin_advert_card.dart b/lib/advert/ui/pages/admin_page/admin_advert_card.dart index 7397504045..4f4560a659 100644 --- a/lib/advert/ui/pages/admin_page/admin_advert_card.dart +++ b/lib/advert/ui/pages/admin_page/admin_advert_card.dart @@ -1,72 +1,86 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/admin/providers/my_association_list_provider.dart'; import 'package:titan/advert/class/advert.dart'; -import 'package:titan/advert/tools/constants.dart'; -import 'package:titan/advert/ui/components/advert_card.dart'; -import 'package:titan/tools/ui/builders/waiting_button.dart'; -import 'package:titan/tools/ui/layouts/card_button.dart'; +import 'package:timeago/timeago.dart' as timeago; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/ui/styleguide/icon_button.dart'; class AdminAdvertCard extends HookConsumerWidget { - final VoidCallback onTap, onEdit; + final VoidCallback onEdit; final Future Function() onDelete; final Advert advert; const AdminAdvertCard({ super.key, required this.advert, - required this.onTap, required this.onEdit, required this.onDelete, }); @override Widget build(BuildContext context, WidgetRef ref) { + final myAssociations = ref.watch(myAssociationListProvider); + final myAssociationIdList = myAssociations.map((e) => e.id).toList(); return Padding( - padding: const EdgeInsets.symmetric(horizontal: 30), - child: Stack( - children: [ - AdvertCard(onTap: onTap, advert: advert), - Positioned( - top: 10, - right: 15, - child: Container( - margin: const EdgeInsets.only(top: 30), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - GestureDetector( - onTap: onEdit, - child: CardButton( - colors: [Colors.grey.shade100, Colors.grey.shade400], - shadowColor: Colors.grey.shade300.withValues(alpha: 0.2), - child: const HeroIcon( - HeroIcons.pencil, + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Container( + margin: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + advert.title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, color: Colors.black, ), ), - ), - const SizedBox(width: 20), - WaitingButton( - onTap: onDelete, - builder: (child) => CardButton( - colors: const [ - AdvertColorConstants.redGradient1, - AdvertColorConstants.redGradient2, - ], - shadowColor: AdvertColorConstants.redGradient2.withValues( - alpha: 0.2, + Text( + _capitalizeFirst( + timeago.format(advert.date, locale: 'fr_short'), + ), + style: const TextStyle( + fontSize: 12, + color: ColorConstants.tertiary, ), - child: child, ), - child: const HeroIcon(HeroIcons.trash, color: Colors.white), + ], + ), + const Spacer(), + if (myAssociationIdList.contains(advert.associationId)) + CustomIconButton.secondary( + onPressed: onEdit, + icon: const HeroIcon( + HeroIcons.pencil, + color: ColorConstants.tertiary, + ), ), - ], - ), + const SizedBox(width: 20), + CustomIconButton.danger( + onPressed: onDelete, + icon: const HeroIcon( + HeroIcons.trash, + color: ColorConstants.background, + ), + ), + ], ), - ), - ], + ], + ), ), ); } + + String _capitalizeFirst(String text) { + if (text.isEmpty) return text; + return text[0].toUpperCase() + text.substring(1); + } } diff --git a/lib/advert/ui/pages/admin_page/admin_page.dart b/lib/advert/ui/pages/admin_page/admin_page.dart index 6092ee5bc9..31c746c1df 100644 --- a/lib/advert/ui/pages/admin_page/admin_page.dart +++ b/lib/advert/ui/pages/admin_page/admin_page.dart @@ -2,22 +2,25 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/admin/providers/is_admin_provider.dart'; +import 'package:titan/admin/providers/my_association_list_provider.dart'; import 'package:titan/advert/class/advert.dart'; import 'package:titan/advert/providers/advert_list_provider.dart'; import 'package:titan/advert/providers/advert_posters_provider.dart'; import 'package:titan/advert/providers/advert_provider.dart'; -import 'package:titan/advert/providers/announcer_list_provider.dart'; -import 'package:titan/advert/providers/announcer_provider.dart'; -import 'package:titan/advert/tools/constants.dart'; +import 'package:titan/advert/providers/selected_association_provider.dart'; +import 'package:titan/advert/ui/components/special_action_button.dart'; import 'package:titan/advert/ui/pages/admin_page/admin_advert_card.dart'; import 'package:titan/advert/ui/pages/advert.dart'; import 'package:titan/advert/router.dart'; -import 'package:titan/advert/ui/components/announcer_bar.dart'; +import 'package:titan/advert/ui/components/association_bar.dart'; +import 'package:titan/feed/providers/is_user_a_member_of_an_association.dart'; +import 'package:titan/tools/constants.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; -import 'package:titan/tools/ui/layouts/card_layout.dart'; -import 'package:titan/tools/ui/layouts/column_refresher.dart'; +import 'package:titan/tools/ui/layouts/refresher.dart'; import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AdvertAdminPage extends HookConsumerWidget { const AdvertAdminPage({super.key}); @@ -25,118 +28,157 @@ class AdvertAdminPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final advertNotifier = ref.watch(advertProvider.notifier); + final isAdmin = ref.watch(isAdminProvider); final advertList = ref.watch(advertListProvider); - final userAnnouncerListNotifier = ref.watch( - userAnnouncerListProvider.notifier, - ); - final userAnnouncerList = ref.watch(userAnnouncerListProvider); final advertPostersNotifier = ref.watch(advertPostersProvider.notifier); - final advertListNotifier = ref.watch(advertListProvider.notifier); - final selectedAnnouncers = ref.watch(announcerProvider); - final selectedAnnouncersNotifier = ref.read(announcerProvider.notifier); + final selectedAssociations = ref.watch(selectedAssociationProvider); + final selectedAssociationsNotifier = ref.read( + selectedAssociationProvider.notifier, + ); + final myAssociationList = ref.watch(myAssociationListProvider); + final myAssociationListNotifier = ref.watch( + asyncMyAssociationListProvider.notifier, + ); + final isAdvertAdmin = ref.watch(isUserAMemberOfAnAssociationProvider); + return AdvertTemplate( - child: AsyncChild( - value: advertList, - builder: (context, advertData) => AsyncChild( - value: userAnnouncerList, - builder: (context, userAnnouncerData) { - final userAnnouncerAdvert = advertData.where( - (advert) => userAnnouncerData - .where((element) => advert.announcer.id == element.id) - .isNotEmpty, - ); - final sortedUserAnnouncerAdverts = userAnnouncerAdvert - .toList() - .sortedBy((element) => element.date) - .reversed; - final filteredSortedUserAnnouncerAdverts = - sortedUserAnnouncerAdverts - .where( - (advert) => - selectedAnnouncers - .where((e) => advert.announcer.id == e.id) - .isNotEmpty || - selectedAnnouncers.isEmpty, - ) - .toList(); - return ColumnRefresher( - onRefresh: () async { - await advertListNotifier.loadAdverts(); - await userAnnouncerListNotifier.loadMyAnnouncerList(); - advertPostersNotifier.resetTData(); - }, - children: [ - const AnnouncerBar( - useUserAnnouncers: true, + child: Column( + children: [ + Row( + children: [ + Expanded( + child: AssociationBar( + useUserAssociations: !isAdmin, multipleSelect: true, ), - GestureDetector( + ), + if (!isAdmin || isAdvertAdmin) ...[ + SizedBox(width: 5), + Container( + width: 2, + height: 60, + color: ColorConstants.secondary, + ), + SizedBox(width: 5), + SpecialActionButton( onTap: () { advertNotifier.setAdvert(Advert.empty()); + if (myAssociationList.length == 1 && + selectedAssociations.isEmpty) { + selectedAssociationsNotifier.addAssociation( + myAssociationList[0], + ); + } QR.to( AdvertRouter.root + AdvertRouter.admin + AdvertRouter.addEditAdvert, ); }, - child: CardLayout( - margin: const EdgeInsets.only( - bottom: 10, - top: 20, - left: 30, - right: 30, - ), - width: 300, - height: 100, - colors: [Colors.white, Colors.grey.shade100], - shadowColor: Colors.grey.withValues(alpha: 0.2), - child: Center( - child: HeroIcon( - HeroIcons.plus, - size: 40, - color: Colors.grey.shade500, - ), - ), - ), - ), - ...filteredSortedUserAnnouncerAdverts.map( - (advert) => AdminAdvertCard( - onTap: () { - advertNotifier.setAdvert(advert); - QR.to(AdvertRouter.root + AdvertRouter.detail); - }, - onEdit: () { - QR.to( - AdvertRouter.root + - AdvertRouter.admin + - AdvertRouter.addEditAdvert, - ); - advertNotifier.setAdvert(advert); - selectedAnnouncersNotifier.clearAnnouncer(); - selectedAnnouncersNotifier.addAnnouncer(advert.announcer); - }, - onDelete: () async { - await showDialog( - context: context, - builder: (context) { - return CustomDialogBox( - title: AdvertTextConstants.deleting, - descriptions: AdvertTextConstants.deleteAdvert, - onYes: () { - advertListNotifier.deleteAdvert(advert); - advertPostersNotifier.deleteE(advert.id, 0); - }, - ); - }, - ); - }, - advert: advert, + icon: HeroIcon( + HeroIcons.plus, + color: ColorConstants.background, ), + name: "Post", ), + SizedBox(width: 10), ], - ); - }, - ), + ], + ), + const SizedBox(height: 20), + Expanded( + child: AsyncChild( + value: advertList, + builder: (context, advertData) { + final userAssociationAdvert = advertData.where( + (advert) => !isAdmin + ? myAssociationList.any( + (element) => advert.associationId == element.id, + ) + : true, + ); + final sortedUserAssociationAdverts = userAssociationAdvert + .toList() + .sortedBy((element) => element.date) + .reversed; + final filteredSortedUserAssociationAdverts = + sortedUserAssociationAdverts + .where( + (advert) => + selectedAssociations + .where((e) => advert.associationId == e.id) + .isNotEmpty || + selectedAssociations.isEmpty, + ) + .toList(); + return Refresher( + controller: ScrollController(), + onRefresh: () async { + if (isAdmin) { + await ref + .watch(advertListProvider.notifier) + .loadAdverts(); + } + await ref.watch(advertListProvider.notifier).loadAdverts(); + await myAssociationListNotifier.loadAssociations(); + advertPostersNotifier.resetTData(); + }, + child: Column( + children: [ + ...filteredSortedUserAssociationAdverts.map( + (advert) => AdminAdvertCard( + onEdit: () { + QR.to( + AdvertRouter.root + + AdvertRouter.admin + + AdvertRouter.addEditAdvert, + ); + advertNotifier.setAdvert(advert); + selectedAssociationsNotifier.clearAssociation(); + selectedAssociationsNotifier.addAssociation( + myAssociationList.firstWhere( + (element) => element.id == advert.associationId, + ), + ); + }, + onDelete: () async { + await showDialog( + context: context, + builder: (context) { + return CustomDialogBox( + title: AppLocalizations.of( + context, + )!.advertDeleting, + descriptions: AppLocalizations.of( + context, + )!.advertDeleteAdvert, + onYes: () async { + if (isAdmin) { + await ref + .watch(advertListProvider.notifier) + .deleteAdvert(advert); + } else { + await ref + .watch(advertListProvider.notifier) + .deleteAdvert(advert); + } + advertPostersNotifier.deleteE(advert.id, 0); + }, + ); + }, + ); + }, + advert: advert, + ), + ), + SizedBox(height: 80), + ], + ), + ); + }, + ), + ), + ], ), ); } diff --git a/lib/advert/ui/pages/advert.dart b/lib/advert/ui/pages/advert.dart index f737e5dc07..966d6777d0 100644 --- a/lib/advert/ui/pages/advert.dart +++ b/lib/advert/ui/pages/advert.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/advert/providers/announcer_provider.dart'; +import 'package:titan/advert/providers/selected_association_provider.dart'; +import 'package:titan/tools/constants.dart'; import 'package:titan/advert/router.dart'; -import 'package:titan/advert/tools/constants.dart'; import 'package:titan/tools/ui/widgets/top_bar.dart'; class AdvertTemplate extends HookConsumerWidget { @@ -11,21 +11,21 @@ class AdvertTemplate extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final selectedAnnouncersNotifier = ref.read(announcerProvider.notifier); + final selectedAssociationsNotifier = ref.read( + selectedAssociationProvider.notifier, + ); return Scaffold( body: Container( - color: Colors.white, + color: ColorConstants.background, child: SafeArea( child: Column( children: [ TopBar( - title: AdvertTextConstants.advert, root: AdvertRouter.root, onBack: () { - selectedAnnouncersNotifier.clearAnnouncer(); + selectedAssociationsNotifier.clearAssociation(); }, ), - const SizedBox(height: 30), Expanded(child: child), ], ), diff --git a/lib/advert/ui/pages/detail_page/detail.dart b/lib/advert/ui/pages/detail_page/detail.dart deleted file mode 100644 index 643d28fb42..0000000000 --- a/lib/advert/ui/pages/detail_page/detail.dart +++ /dev/null @@ -1,191 +0,0 @@ -import 'package:auto_size_text/auto_size_text.dart'; -import 'package:flutter/material.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:intl/intl.dart'; -import 'package:titan/advert/providers/advert_poster_provider.dart'; -import 'package:titan/advert/providers/advert_posters_provider.dart'; -import 'package:titan/advert/providers/advert_provider.dart'; -import 'package:titan/advert/ui/components/tag_chip.dart'; -import 'package:titan/cinema/tools/functions.dart'; -import 'package:titan/tools/ui/builders/auto_loader_child.dart'; -import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; -import 'package:titan/tools/ui/widgets/text_with_hyper_link.dart'; -import 'package:qlevar_router/qlevar_router.dart'; - -class AdvertDetailPage extends HookConsumerWidget { - const AdvertDetailPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final advert = ref.watch(advertProvider); - final posters = ref.watch( - advertPostersProvider.select((advertPosters) => advertPosters[advert.id]), - ); - final advertPostersNotifier = ref.watch(advertPostersProvider.notifier); - final logoNotifier = ref.watch(advertPosterProvider.notifier); - final filteredTagList = advert.tags - .where((element) => element != "") - .toList(); - final inTagChipsList = [advert.announcer.name] + filteredTagList; - - return Stack( - children: [ - Container( - width: double.infinity, - decoration: const BoxDecoration( - boxShadow: [ - BoxShadow( - color: Colors.black26, - blurRadius: 10, - spreadRadius: 7, - offset: Offset(0, 5), - ), - ], - ), - child: AutoLoaderChild( - group: posters, - notifier: advertPostersNotifier, - mapKey: advert, - loader: (ref) => logoNotifier.getAdvertPoster(advert.id), - dataBuilder: (context, value) => - Image(image: value.first.image, fit: BoxFit.fill), - ), - ), - SingleChildScrollView( - physics: const BouncingScrollPhysics(), - child: Column( - children: [ - const SizedBox(height: 220), - Container( - width: double.infinity, - height: 50, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - const Color.fromARGB(0, 255, 255, 255), - Colors.grey.shade50.withValues(alpha: 0.85), - Colors.grey.shade50, - ], - stops: const [0.0, 0.65, 1.0], - ), - ), - ), - Container( - color: Colors.grey.shade50, - child: Column( - children: [ - const SizedBox(height: 15), - Container( - padding: const EdgeInsets.symmetric(horizontal: 30.0), - alignment: Alignment.center, - child: AutoSizeText( - advert.title, - maxLines: 2, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 30, - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(height: 15), - Container( - padding: const EdgeInsets.symmetric(horizontal: 30.0), - alignment: Alignment.center, - child: Text( - formatDate(advert.date), - style: const TextStyle(fontSize: 18), - ), - ), - const SizedBox(height: 20), - HorizontalListView.builder( - height: 35, - horizontalSpace: 30, - items: inTagChipsList, - itemBuilder: - (BuildContext context, String item, int index) => - TagChip(tagName: item), - ), - const SizedBox(height: 15), - Container( - padding: const EdgeInsets.symmetric(horizontal: 30.0), - child: TextWithHyperLink( - advert.content, - textAlign: TextAlign.left, - style: const TextStyle(fontSize: 15), - ), - ), - const SizedBox(height: 140), - ], - ), - ), - ], - ), - ), - Column( - children: [ - const SizedBox(height: 45), - Row( - children: [ - const SizedBox(width: 20), - GestureDetector( - onTap: QR.back, - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.9), - borderRadius: BorderRadius.circular(18), - boxShadow: [ - BoxShadow( - color: Colors.white.withValues(alpha: 0.3), - blurRadius: 7, - spreadRadius: 2, - offset: const Offset(2, 3), - ), - ], - ), - child: const Icon(Icons.arrow_back, color: Colors.black), - ), - ), - const Spacer(), - Container( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 12, - ), - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.9), - borderRadius: BorderRadius.circular(18), - boxShadow: [ - BoxShadow( - color: Colors.white.withValues(alpha: 0.3), - blurRadius: 7, - spreadRadius: 2, - offset: const Offset(2, 3), - ), - ], - ), - child: Row( - children: [ - const HeroIcon(HeroIcons.calendar, size: 20), - const SizedBox(width: 7), - Text( - DateFormat('dd/MM/yyyy - HH:mm').format(advert.date), - style: const TextStyle(fontSize: 16), - textAlign: TextAlign.center, - ), - ], - ), - ), - const SizedBox(width: 20), - ], - ), - ], - ), - ], - ); - } -} diff --git a/lib/advert/ui/pages/form_page/add_edit_advert_page.dart b/lib/advert/ui/pages/form_page/add_edit_advert_page.dart index 0cac9788ce..5a383a5601 100644 --- a/lib/advert/ui/pages/form_page/add_edit_advert_page.dart +++ b/lib/advert/ui/pages/form_page/add_edit_advert_page.dart @@ -6,23 +6,25 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:titan/admin/class/assocation.dart'; import 'package:titan/advert/class/advert.dart'; -import 'package:titan/advert/class/announcer.dart'; import 'package:titan/advert/providers/advert_list_provider.dart'; import 'package:titan/advert/providers/advert_poster_provider.dart'; import 'package:titan/advert/providers/advert_posters_provider.dart'; import 'package:titan/advert/providers/advert_provider.dart'; -import 'package:titan/advert/providers/announcer_provider.dart'; -import 'package:titan/advert/tools/constants.dart'; +import 'package:titan/advert/providers/selected_association_provider.dart'; import 'package:titan/advert/ui/pages/advert.dart'; -import 'package:titan/advert/ui/components/announcer_bar.dart'; +import 'package:titan/advert/ui/components/association_bar.dart'; +import 'package:titan/event/ui/pages/event_pages/checkbox_entry.dart'; +import 'package:titan/navigation/ui/scroll_to_hide_navbar.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; import 'package:titan/tools/ui/layouts/add_edit_button_layout.dart'; +import 'package:titan/tools/ui/styleguide/text_entry.dart'; import 'package:titan/tools/ui/widgets/image_picker_on_tap.dart'; -import 'package:titan/tools/ui/widgets/text_entry.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AdvertAddEditAdvertPage extends HookConsumerWidget { const AdvertAddEditAdvertPage({super.key}); @@ -34,17 +36,15 @@ class AdvertAddEditAdvertPage extends HookConsumerWidget { final isEdit = advert.id != Advert.empty().id; final title = useTextEditingController(text: advert.title); final content = useTextEditingController(text: advert.content); - final selectedAnnouncers = ref.watch(announcerProvider); - final tags = advert.tags; - var textTags = tags.join(', '); - final textTagsController = useTextEditingController(text: textTags); final advertPosters = ref.watch(advertPostersProvider); final advertListNotifier = ref.watch(advertListProvider.notifier); final posterNotifier = ref.watch(advertPosterProvider.notifier); final poster = useState(null); final posterFile = useState(null); + final selectedAssociation = ref.watch(selectedAssociationProvider); + if (advertPosters[advert.id] != null) { advertPosters[advert.id]!.whenData((data) { if (data.isNotEmpty) { @@ -53,6 +53,9 @@ class AdvertAddEditAdvertPage extends HookConsumerWidget { }); } + final postToFeed = useState(isEdit ? advert.postToFeed : false); + final notification = useState(isEdit ? advert.notification : true); + final ImagePicker picker = ImagePicker(); void displayAdvertToastWithContext(TypeMsg type, String msg) { @@ -60,26 +63,60 @@ class AdvertAddEditAdvertPage extends HookConsumerWidget { } return AdvertTemplate( - child: SingleChildScrollView( - physics: const BouncingScrollPhysics(), + child: ScrollToHideNavbar( + controller: ScrollController(), child: Form( key: key, child: Column( children: [ + FormField>( + validator: (e) { + if (selectedAssociation.isEmpty) { + return AppLocalizations.of( + context, + )!.advertChoosingAnnouncer; + } + return null; + }, + builder: (formFieldState) => Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(20)), + boxShadow: formFieldState.hasError + ? [ + const BoxShadow( + color: Colors.red, + spreadRadius: 3, + blurRadius: 3, + offset: Offset(2, 2), + ), + ] + : [], + ), + child: AssociationBar( + useUserAssociations: true, + multipleSelect: false, + isNotClickable: isEdit, + ), + ), + ), Padding( padding: const EdgeInsets.symmetric(horizontal: 30), child: Column( children: [ + const SizedBox(height: 20), TextEntry( maxLines: 1, - label: AdvertTextConstants.title, + label: AppLocalizations.of(context)!.advertTitle, controller: title, ), const SizedBox(height: 20), FormField( validator: (e) { if (poster.value == null && !isEdit) { - return AdvertTextConstants.choosingPoster; + return AppLocalizations.of( + context, + )!.advertChoosingPoster; } return null; }, @@ -93,144 +130,161 @@ class AdvertAddEditAdvertPage extends HookConsumerWidget { imageNotifier: posterFile, displayToastWithContext: displayAdvertToastWithContext, - child: Container( - decoration: BoxDecoration( - color: Colors.white, - boxShadow: [ - BoxShadow( - color: formFieldState.hasError - ? Colors.red - : Colors.black.withValues(alpha: 0.1), - spreadRadius: 5, - blurRadius: 10, - offset: const Offset(2, 3), + child: AspectRatio( + aspectRatio: 851 / 315, + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(5), ), - ], - ), - child: posterFile.value != null - ? Stack( - children: [ - Container( - width: 285, - height: 160, - decoration: BoxDecoration( - borderRadius: - const BorderRadius.all( - Radius.circular(5), - ), - image: DecorationImage( - image: poster.value != null - ? Image.memory( - poster.value!, - fit: BoxFit.cover, - ).image - : posterFile.value!.image, - fit: BoxFit.cover, + color: Colors.white, + boxShadow: [ + BoxShadow( + color: formFieldState.hasError + ? Colors.red + : Colors.black.withValues( + alpha: 0.1, ), - ), - child: Center( - child: Container( - width: 50, - height: 50, - decoration: BoxDecoration( - borderRadius: - const BorderRadius.all( - Radius.circular(5), - ), - color: Colors.white - .withValues(alpha: 0.4), + spreadRadius: 5, + blurRadius: 10, + offset: const Offset(2, 3), + ), + ], + ), + child: posterFile.value != null + ? Stack( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: + const BorderRadius.all( + Radius.circular(5), + ), + image: DecorationImage( + image: poster.value != null + ? Image.memory( + poster.value!, + fit: BoxFit.cover, + ).image + : posterFile.value!.image, + fit: BoxFit.cover, ), - child: HeroIcon( - HeroIcons.photo, - size: 40, - color: Colors.black - .withValues(alpha: 0.5), + ), + child: Center( + child: Container( + width: 50, + height: 50, + decoration: BoxDecoration( + borderRadius: + const BorderRadius.all( + Radius.circular(5), + ), + color: Colors.white + .withValues(alpha: 0.4), + ), + child: HeroIcon( + HeroIcons.photo, + size: 40, + color: Colors.black + .withValues(alpha: 0.5), + ), ), ), ), + ], + ) + : Container( + decoration: BoxDecoration( + borderRadius: + const BorderRadius.all( + Radius.circular(5), + ), ), - ], - ) - : const HeroIcon( - HeroIcons.photo, - size: 160, - color: Colors.grey, - ), + child: Column( + mainAxisAlignment: + MainAxisAlignment.start, + children: [ + const HeroIcon( + HeroIcons.photo, + size: 100, + color: Colors.grey, + ), + Text("(851/315)"), + ], + ), + ), + ), ), ), ], ), ), ), + const SizedBox(height: 20), TextEntry( minLines: 5, - maxLines: 50, keyboardType: TextInputType.multiline, - label: AdvertTextConstants.content, + label: AppLocalizations.of(context)!.advertContent, controller: content, ), ], ), ), - const SizedBox(height: 50), - FormField>( - validator: (e) { - if (selectedAnnouncers.isEmpty) { - return AdvertTextConstants.choosingAnnouncer; - } - return null; - }, - builder: (formFieldState) => Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: const BorderRadius.all(Radius.circular(20)), - boxShadow: formFieldState.hasError - ? [ - const BoxShadow( - color: Colors.red, - spreadRadius: 3, - blurRadius: 3, - offset: Offset(2, 2), - ), - ] - : [], - ), - child: AnnouncerBar( - useUserAnnouncers: true, - multipleSelect: false, - isNotClickable: isEdit, - ), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 30.0), + child: CheckBoxEntry( + title: AppLocalizations.of(context)!.advertPublishToFeed, + valueNotifier: postToFeed, + onChanged: () { + postToFeed.value = !postToFeed.value; + }, + ), + ), + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 30.0), + child: CheckBoxEntry( + title: AppLocalizations.of(context)!.advertNotification, + valueNotifier: notification, + onChanged: () { + notification.value = !notification.value; + }, ), ), + const SizedBox(height: 30), Padding( padding: const EdgeInsets.symmetric(horizontal: 30), child: Column( children: [ - TextEntry( - maxLines: 1, - label: AdvertTextConstants.tags, - canBeEmpty: true, - controller: textTagsController, - ), - const SizedBox(height: 50), WaitingButton( onTap: () async { if (key.currentState == null) { return; } if (key.currentState!.validate() && - selectedAnnouncers.isNotEmpty && + selectedAssociation.isNotEmpty && (poster.value != null || isEdit)) { await tokenExpireWrapper(ref, () async { final advertList = ref.watch(advertListProvider); Advert newAdvert = Advert( id: isEdit ? advert.id : '', - announcer: selectedAnnouncers[0], + associationId: selectedAssociation[0].id, content: content.text, date: isEdit ? advert.date : DateTime.now(), - tags: textTagsController.text.split(', '), title: title.text, + postToFeed: postToFeed.value, + notification: notification.value, ); + final editedAdvertMsg = AppLocalizations.of( + context, + )!.advertEditedAdvert; + final addedAdvertMsg = AppLocalizations.of( + context, + )!.advertAddedAdvert; + final editingErrorMsg = AppLocalizations.of( + context, + )!.advertEditingError; final value = isEdit ? await advertListNotifier.updateAdvert( newAdvert, @@ -241,7 +295,7 @@ class AdvertAddEditAdvertPage extends HookConsumerWidget { if (isEdit) { displayAdvertToastWithContext( TypeMsg.msg, - AdvertTextConstants.editedAdvert, + editedAdvertMsg, ); advertList.maybeWhen( data: (list) { @@ -257,7 +311,7 @@ class AdvertAddEditAdvertPage extends HookConsumerWidget { } else { displayAdvertToastWithContext( TypeMsg.msg, - AdvertTextConstants.addedAdvert, + addedAdvertMsg, ); advertList.maybeWhen( data: (list) { @@ -273,7 +327,7 @@ class AdvertAddEditAdvertPage extends HookConsumerWidget { } else { displayAdvertToastWithContext( TypeMsg.error, - AdvertTextConstants.editingError, + editingErrorMsg, ); } }); @@ -281,14 +335,16 @@ class AdvertAddEditAdvertPage extends HookConsumerWidget { displayToast( context, TypeMsg.error, - AdvertTextConstants.incorrectOrMissingFields, + AppLocalizations.of( + context, + )!.advertIncorrectOrMissingFields, ); } }, child: Text( isEdit - ? AdvertTextConstants.edit - : AdvertTextConstants.add, + ? AppLocalizations.of(context)!.advertEdit + : AppLocalizations.of(context)!.advertAdd, style: const TextStyle( color: Colors.white, fontSize: 25, @@ -300,7 +356,7 @@ class AdvertAddEditAdvertPage extends HookConsumerWidget { ], ), ), - const SizedBox(height: 20), + const SizedBox(height: 100), ], ), ), diff --git a/lib/advert/ui/pages/form_page/add_rem_announcer_page.dart b/lib/advert/ui/pages/form_page/add_rem_announcer_page.dart deleted file mode 100644 index f3843d7288..0000000000 --- a/lib/advert/ui/pages/form_page/add_rem_announcer_page.dart +++ /dev/null @@ -1,177 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/providers/group_list_provider.dart'; -import 'package:titan/advert/class/announcer.dart'; -import 'package:titan/advert/providers/all_announcer_list_provider.dart'; -import 'package:titan/advert/providers/announcer_list_provider.dart'; -import 'package:titan/advert/tools/constants.dart'; -import 'package:titan/advert/ui/pages/advert.dart'; -import 'package:titan/advert/ui/pages/form_page/announcer_card.dart'; -import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:titan/tools/token_expire_wrapper.dart'; -import 'package:titan/tools/ui/builders/async_child.dart'; -import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; - -class AddRemAnnouncerPage extends HookConsumerWidget { - const AddRemAnnouncerPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final announcerListNotifier = ref.watch(announcerListProvider.notifier); - final announcers = ref.watch(allAnnouncerList); - final groups = ref.watch(allGroupListProvider); - final announcerIds = announcers.map((x) => x.groupManagerId).toList(); - - void displayToastWithContext(TypeMsg type, String msg) { - displayToast(context, type, msg); - } - - return AdvertTemplate( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 30.0), - child: SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics( - parent: BouncingScrollPhysics(), - ), - child: Column( - children: [ - SizedBox( - child: Column( - children: [ - const Align( - alignment: Alignment.centerLeft, - child: Text( - AdvertTextConstants.modifyAnnouncingGroup, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w700, - color: ColorConstants.gradient1, - ), - ), - ), - const SizedBox(height: 30), - AsyncChild( - value: groups, - builder: (context, groupList) { - final canAdd = groupList - .where((x) => !announcerIds.contains(x.id)) - .toList(); - final canRemove = groupList - .where((x) => announcerIds.contains(x.id)) - .toList(); - return (canAdd + canRemove).isNotEmpty - ? Column( - children: - canAdd - .map( - (e) => GestureDetector( - onTap: () { - Announcer newAnnouncer = - Announcer( - groupManagerId: e.id, - id: '', - name: e.name, - ); - tokenExpireWrapper(ref, () async { - final value = - await announcerListNotifier - .addAnnouncer( - newAnnouncer, - ); - if (value) { - displayToastWithContext( - TypeMsg.msg, - AdvertTextConstants - .addedAnnouncer, - ); - } else { - displayToastWithContext( - TypeMsg.error, - AdvertTextConstants - .addingError, - ); - } - announcerListNotifier - .loadAllAnnouncerList(); - }); - }, - child: AnnouncerCard( - e: e, - icon: HeroIcons.plus, - ), - ), - ) - .toList() + - canRemove - .map( - (e) => GestureDetector( - onTap: () async { - await showDialog( - context: context, - builder: (context) { - return CustomDialogBox( - title: AdvertTextConstants - .deleting, - descriptions: - AdvertTextConstants - .deleteAnnouncer, - onYes: () { - tokenExpireWrapper(ref, () async { - final value = await announcerListNotifier - .deleteAnnouncer( - announcers - .where( - (element) => - e.id == - e.id, - ) - .toList()[0], - ); - if (value) { - displayToastWithContext( - TypeMsg.msg, - AdvertTextConstants - .removedAnnouncer, - ); - } else { - displayToastWithContext( - TypeMsg.error, - AdvertTextConstants - .removingError, - ); - } - announcerListNotifier - .loadAllAnnouncerList(); - }); - }, - ); - }, - ); - }, - child: AnnouncerCard( - e: e, - icon: HeroIcons.minus, - ), - ), - ) - .toList(), - ) - : const Center( - child: Text( - AdvertTextConstants.noMoreAnnouncer, - ), - ); - }, - ), - ], - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/advert/ui/pages/main_page/advert_card.dart b/lib/advert/ui/pages/main_page/advert_card.dart new file mode 100644 index 0000000000..6f96b5e91e --- /dev/null +++ b/lib/advert/ui/pages/main_page/advert_card.dart @@ -0,0 +1,258 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/admin/providers/assocation_list_provider.dart'; +import 'package:titan/admin/providers/association_logo_provider.dart'; +import 'package:titan/admin/providers/associations_logo_map_provider.dart'; +import 'package:titan/advert/class/advert.dart'; +import 'package:titan/advert/providers/advert_poster_provider.dart'; +import 'package:titan/advert/providers/advert_posters_provider.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/ui/builders/auto_loader_child.dart'; +import 'package:timeago/timeago.dart' as timeago; +import 'package:url_launcher/url_launcher.dart'; + +class AdvertCard extends HookConsumerWidget { + final Advert advert; + + const AdvertCard({super.key, required this.advert}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isExpanded = useState(false); + final posters = ref.watch( + advertPostersProvider.select((advertPosters) => advertPosters[advert.id]), + ); + final advertPostersNotifier = ref.watch(advertPostersProvider.notifier); + final posterNotifier = ref.watch(advertPosterProvider.notifier); + final asyncAssociationList = ref.watch(associationListProvider); + final associationList = asyncAssociationList.when( + data: (data) => data, + loading: () => [], + error: (_, _) => [], + ); + final associationName = + associationList + .firstWhereOrNull((e) => e.id == advert.associationId) + ?.name ?? + ''; + final associationLogo = ref.watch( + associationLogoMapProvider.select((value) => value[advert.associationId]), + ); + final associationLogoMapNotifier = ref.watch( + associationLogoMapProvider.notifier, + ); + final associationLogoNotifier = ref.watch(associationLogoProvider.notifier); + return Container( + margin: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Row( + children: [ + Center( + child: AutoLoaderChild( + group: associationLogo, + notifier: associationLogoMapNotifier, + mapKey: advert.associationId, + loader: (associationId) => associationLogoNotifier + .getAssociationLogo(associationId), + dataBuilder: (context, data) { + return CircleAvatar( + radius: 20, + backgroundColor: Colors.white, + backgroundImage: Image(image: data.first.image).image, + ); + }, + orElseBuilder: (context, stack) => Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Colors.black, + ), + child: Text( + associationName + .split(' ') + .take(2) + .map((s) => s[0].toUpperCase()) + .join(), + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + const SizedBox(width: 12), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + associationName, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + Text( + _capitalizeFirst( + timeago.format(advert.date, locale: 'fr_short'), + ), + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 15), + + AspectRatio( + aspectRatio: 851 / 315, + child: AutoLoaderChild( + group: posters, + notifier: advertPostersNotifier, + mapKey: advert.id, + loader: (advertId) => posterNotifier.getAdvertPoster(advertId), + loadingBuilder: (context) => Container( + decoration: BoxDecoration( + color: ColorConstants.onBackground, + borderRadius: BorderRadius.circular(20), + ), + child: const Center(child: HeroIcon(HeroIcons.photo, size: 50)), + ), + dataBuilder: (context, value) => Container( + decoration: BoxDecoration( + color: ColorConstants.onBackground, + borderRadius: BorderRadius.circular(20), + image: DecorationImage( + image: value.first.image, + fit: BoxFit.cover, + ), + ), + ), + ), + ), + + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [_buildExpandableText(isExpanded)], + ), + ), + ], + ), + ); + } + + String _capitalizeFirst(String text) { + if (text.isEmpty) return text; + return text[0].toUpperCase() + text.substring(1); + } + + Widget _buildExpandableText(ValueNotifier isExpanded) { + final title = advert.title.trim(); + final content = advert.content.trim(); + + final fullText = title.isNotEmpty ? '$title $content' : content; + + const maxLength = 100; + + final isLong = fullText.length > maxLength; + + String displayContent; + if (isLong && !isExpanded.value) { + displayContent = '${content.substring(0, maxLength)}...'; + } else { + displayContent = content; + } + + final words = displayContent.split(' '); + final List spans = []; + + if (title.isNotEmpty) { + spans.add( + TextSpan( + text: title, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ); + spans.add(const TextSpan(text: ' ')); + } + + for (int i = 0; i < words.length; i++) { + final word = words[i]; + final isLink = word.startsWith('https://') || word.startsWith('http://'); + + if (isLink) { + spans.add( + TextSpan( + text: word, + style: const TextStyle( + color: ColorConstants.main, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () async { + final uri = Uri.parse(word); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } + }, + ), + ); + } else { + spans.add(TextSpan(text: word)); + } + + if (i < words.length - 1) { + spans.add(const TextSpan(text: ' ')); + } + } + + if (isLong) { + spans.add(const TextSpan(text: ' ')); + spans.add( + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: GestureDetector( + onTap: () { + isExpanded.value = !isExpanded.value; + }, + child: Text( + isExpanded.value ? 'voir moins' : 'voir plus', + style: const TextStyle( + color: Colors.grey, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ); + } + + return RichText( + text: TextSpan( + style: const TextStyle(color: Colors.black, fontSize: 14, height: 1.4), + children: spans, + ), + ); + } +} diff --git a/lib/advert/ui/pages/main_page/main_page.dart b/lib/advert/ui/pages/main_page/main_page.dart index 0479c2bdc1..6901e97154 100644 --- a/lib/advert/ui/pages/main_page/main_page.dart +++ b/lib/advert/ui/pages/main_page/main_page.dart @@ -1,105 +1,104 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/admin/providers/is_admin_provider.dart'; import 'package:titan/advert/providers/advert_list_provider.dart'; import 'package:titan/advert/providers/advert_posters_provider.dart'; -import 'package:titan/advert/providers/advert_provider.dart'; -import 'package:titan/advert/providers/announcer_provider.dart'; -import 'package:titan/advert/providers/is_advert_admin_provider.dart'; +import 'package:titan/advert/providers/selected_association_provider.dart'; +import 'package:titan/advert/ui/components/special_action_button.dart'; import 'package:titan/advert/ui/pages/advert.dart'; import 'package:titan/advert/router.dart'; -import 'package:titan/advert/ui/components/announcer_bar.dart'; -import 'package:titan/advert/ui/components/advert_card.dart'; +import 'package:titan/advert/ui/components/association_bar.dart'; +import 'package:titan/advert/ui/pages/main_page/advert_card.dart'; +import 'package:titan/feed/providers/is_user_a_member_of_an_association.dart'; +import 'package:titan/tools/constants.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; -import 'package:titan/tools/ui/layouts/column_refresher.dart'; -import 'package:titan/tools/ui/widgets/admin_button.dart'; +import 'package:titan/tools/ui/layouts/refresher.dart'; import 'package:qlevar_router/qlevar_router.dart'; -import 'package:titan/advert/tools/constants.dart'; class AdvertMainPage extends HookConsumerWidget { const AdvertMainPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final advertNotifier = ref.watch(advertProvider.notifier); final advertList = ref.watch(advertListProvider); final advertListNotifier = ref.watch(advertListProvider.notifier); final advertPostersNotifier = ref.watch(advertPostersProvider.notifier); - final selected = ref.watch(announcerProvider); - final selectedNotifier = ref.watch(announcerProvider.notifier); + final selected = ref.watch(selectedAssociationProvider); + final selectedNotifier = ref.watch(selectedAssociationProvider.notifier); + final isAdvertAdmin = ref.watch(isUserAMemberOfAnAssociationProvider); final isAdmin = ref.watch(isAdminProvider); - final isAdvertAdmin = ref.watch(isAdvertAdminProvider); return AdvertTemplate( - child: Stack( + child: Column( children: [ - AsyncChild( - value: advertList, - builder: (context, advertData) { - final sortedAdvertData = advertData - .sortedBy((element) => element.date) - .reversed; - final filteredSortedAdvertData = sortedAdvertData.where( - (advert) => - selected - .where((e) => advert.announcer.name == e.name) - .isNotEmpty || - selected.isEmpty, - ); - return ColumnRefresher( - onRefresh: () async { - await advertListNotifier.loadAdverts(); - advertPostersNotifier.resetTData(); - }, - children: [ - SizedBox( - width: 300, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - if (isAdvertAdmin) - AdminButton( - onTap: () { - selectedNotifier.clearAnnouncer(); - QR.to(AdvertRouter.root + AdvertRouter.admin); - }, - ), - if (isAdmin) - AdminButton( - onTap: () { - QR.to( - AdvertRouter.root + - AdvertRouter.addRemAnnouncer, - ); - }, - text: AdvertTextConstants.management, - ), - ], - ), - ), - const SizedBox(height: 20), - const AnnouncerBar( - useUserAnnouncers: false, - multipleSelect: true, + Row( + children: [ + Expanded( + child: const AssociationBar( + useUserAssociations: false, + multipleSelect: true, + ), + ), + + if (isAdmin || isAdvertAdmin) ...[ + SizedBox(width: 5), + Container( + width: 2, + height: 60, + color: ColorConstants.secondary, + ), + SizedBox(width: 5), + SpecialActionButton( + onTap: () { + selectedNotifier.clearAssociation(); + QR.to(AdvertRouter.root + AdvertRouter.admin); + }, + icon: HeroIcon( + HeroIcons.userGroup, + color: ColorConstants.background, ), - const SizedBox(height: 20), - ...filteredSortedAdvertData.map( - (advert) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 30), - child: AdvertCard( - onTap: () { - advertNotifier.setAdvert(advert); - QR.to(AdvertRouter.root + AdvertRouter.detail); - }, - advert: advert, + name: "Admin", + ), + SizedBox(width: 10), + ], + ], + ), + + const SizedBox(height: 20), + + Expanded( + child: AsyncChild( + value: advertList, + builder: (context, advertData) { + final sortedAdvertData = advertData + .sortedBy((element) => element.date) + .reversed; + final filteredSortedAdvertData = sortedAdvertData.where( + (advert) => + selected + .where((e) => advert.associationId == e.id) + .isNotEmpty || + selected.isEmpty, + ); + return Refresher( + controller: ScrollController(), + onRefresh: () async { + await advertListNotifier.loadAdverts(); + advertPostersNotifier.resetTData(); + }, + child: Column( + children: [ + ...filteredSortedAdvertData.map( + (advert) => AdvertCard(advert: advert), ), - ), + SizedBox(height: 80), + ], ), - ], - ); - }, + ); + }, + ), ), - const SizedBox(height: 20), ], ), ); diff --git a/lib/amap/providers/is_amap_admin_provider.dart b/lib/amap/providers/is_amap_admin_provider.dart index ecc2ab30e8..dbfaab9153 100644 --- a/lib/amap/providers/is_amap_admin_provider.dart +++ b/lib/amap/providers/is_amap_admin_provider.dart @@ -5,5 +5,5 @@ final isAmapAdminProvider = StateProvider((ref) { final me = ref.watch(userProvider); return me.groups .map((e) => e.id) - .contains("70db65ee-d533-4f6b-9ffa-a4d70a17b7ef"); + .contains("70db65ee-d533-4f6b-9ffa-a4d70a17b7ef"); // admin_amap }); diff --git a/lib/amap/router.dart b/lib/amap/router.dart index 47cfe0129f..80c43189ea 100644 --- a/lib/amap/router.dart +++ b/lib/amap/router.dart @@ -1,6 +1,5 @@ -import 'package:either_dart/either.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:heroicons/heroicons.dart'; import 'package:titan/amap/providers/is_amap_admin_provider.dart'; import 'package:titan/amap/ui/pages/admin_page/admin_page.dart' deferred as admin_page; @@ -18,7 +17,8 @@ import 'package:titan/amap/ui/pages/presentation_page/text.dart' deferred as presentation_page; import 'package:titan/amap/ui/pages/product_pages/add_edit_product.dart' deferred as add_edit_product; -import 'package:titan/drawer/class/module.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/class/module.dart'; import 'package:titan/tools/middlewares/admin_middleware.dart'; import 'package:titan/tools/middlewares/authenticated_middleware.dart'; import 'package:titan/tools/middlewares/deferred_middleware.dart'; @@ -35,10 +35,10 @@ class AmapRouter { static const String presentation = '/presentation'; static const String addEditProduct = '/add_edit_product'; static final Module module = Module( - name: "Amap", - icon: const Left(HeroIcons.shoppingCart), + getName: (context) => AppLocalizations.of(context)!.moduleAmap, + getDescription: (context) => + AppLocalizations.of(context)!.moduleAmapDescription, root: AmapRouter.root, - selected: false, ); AmapRouter(this.ref); @@ -50,6 +50,10 @@ class AmapRouter { AuthenticatedMiddleware(ref), DeferredLoadingMiddleware(main_page.loadLibrary), ], + pageType: QCustomPage( + transitionsBuilder: (_, animation, _, child) => + FadeTransition(opacity: animation, child: child), + ), children: [ QRoute( path: admin, diff --git a/lib/amap/tools/constants.dart b/lib/amap/tools/constants.dart index db49483f8c..65fc60d714 100644 --- a/lib/amap/tools/constants.dart +++ b/lib/amap/tools/constants.dart @@ -20,133 +20,3 @@ class AMAPColorConstants extends ColorConstants { static const Color redGradient1 = Color(0xFF9E131F); static const Color redGradient2 = Color(0xFF590512); } - -class AMAPTextConstants { - static const String accounts = "Comptes"; - static const String add = "Ajouter"; - static const String addDelivery = "Ajouter une livraison"; - static const String addedCommand = "Commande ajoutée"; - static const String addedOrder = "Commande ajoutée"; - static const String addedProduct = "Produit ajouté"; - static const String addedUser = "Utilisateur ajouté"; - static const String addProduct = "Ajouter un produit"; - static const String addUser = "Ajouter un utilisateur"; - static const String addingACommand = "Ajouter une commande"; - static const String addingCommand = "Ajouter la commande"; - static const String addingError = "Erreur lors de l'ajout"; - static const String addingProduct = "Ajouter un produit"; - static const String addOrder = "Ajouter une commande"; - static const String admin = "Admin"; - static const String alreadyExistCommand = - "Il existe déjà une commande à cette date"; - static const String amap = "Amap"; - static const String amount = "Solde"; - static const String archive = "Archiver"; - static const String archiveDelivery = "Archiver"; - static const String archivingDelivery = "Archivage de la livraison"; - static const String category = "Catégorie"; - static const String closeDelivery = "Verrouiller"; - static const String commandDate = "Date de la commande"; - static const String commandProducts = "Produits de la commande"; - static const String confirm = "Confirmer"; - static const String contact = "Contacts associatifs "; - static const String createCategory = "Créer une catégorie"; - static const String delete = "Supprimer"; - static const String deleteDelivery = "Supprimer la livraison ?"; - static const String deleteDeliveryDescription = - "Voulez-vous vraiment supprimer cette livraison ?"; - static const String deletedDelivery = "Livraison supprimée"; - static const String deletedOrder = "Commande supprimée"; - static const String deletedProduct = "Produit supprimé"; - static const String deleteProduct = "Supprimer le produit ?"; - static const String deleteProductDescription = - "Voulez-vous vraiment supprimer ce produit ?"; - static const String deleting = "Suppression"; - static const String deletingDelivery = "Supprimer la livraison ?"; - static const String deletingError = "Erreur lors de la suppression"; - static const String deletingOrder = "Supprimer la commande ?"; - static const String deletingProduct = "Supprimer le produit ?"; - static const String deliver = "Livraison teminée ?"; - static const String deliveries = "Livraisons"; - static const String deliveringDelivery = - "Toutes les commandes sont livrées ?"; - static const String delivery = "Livraison"; - static const String deliveryArchived = "Livraison archivée"; - static const String deliveryDate = "Date de livraison"; - static const String deliveryDelivered = "Livraison effectuée"; - static const String deliveryHistory = "Historique des livraisons"; - static const String deliveryList = "Liste des livraisons"; - static const String deliveryLocked = "Livraison verrouillée"; - static const String deliveryOn = "Livraison le"; - static const String deliveryOpened = "Livraison ouverte"; - static const String deliveryNotArchived = "Livraison non archivée"; - static const String deliveryNotLocked = "Livraison non verrouillée"; - static const String deliveryNotDelivered = "Livraison non effectuée"; - static const String deliveryNotOpened = "Livraison non ouverte"; - static const String editDelivery = "Modifier la livraison"; - static const String editedCommand = "Commande modifiée"; - static const String editingError = "Erreur lors de la modification"; - static const String editProduct = "Modifier le produit"; - static const String endingDelivery = "Fin de la livraison"; - static const String error = "Erreur"; - static const String errorLink = "Erreur lors de l'ouverture du lien"; - static const String errorLoadingUser = - "Erreur lors du chargement des utilisateurs"; - static const String evening = "Soir"; - static const String expectingNumber = "Veuillez entrer un nombre"; - static const String fillField = "Veuillez remplir ce champ"; - static const String handlingAccount = "Gérer les comptes"; - static const String loading = "Chargement..."; - static const String loadingError = "Erreur lors du chargement"; - static const String lock = "Verrouiller"; - static const String locked = "Verrouillée"; - static const String lockedDelivery = "Livraison verrouillée"; - static const String lockedOrder = "Commande verrouillée"; - static const String looking = "Rechercher"; - static const String lockingDelivery = "Verrouiller la livraison ?"; - static const String midDay = "Midi"; - static const String myOrders = "Mes commandes"; - static const String name = "Nom"; - static const String nextStep = "Étape suivante"; - static const String noProduct = "Pas de produit"; - static const String noCurrentOrder = "Pas de commande en cours"; - static const String noMoney = "Pas assez d'argent"; - static const String noOpennedDelivery = "Pas de livraison ouverte"; - static const String noOrder = "Pas de commande"; - static const String noSelectedDelivery = "Pas de livraison sélectionnée"; - static const String notEnoughMoney = "Pas assez d'argent"; - static const String notPlannedDelivery = "Pas de livraison planifiée"; - static const String oneOrder = "commande"; - static const String openDelivery = "Ouvrir"; - static const String opened = "Ouverte"; - static const String openningDelivery = "Ouvrir la livraison ?"; - static const String order = "Commander"; - static const String orders = "Commandes"; - static const String pickChooseCategory = - "Veuillez entrer une valeur ou choisir une catégorie existante"; - static const String pickDeliveryMoment = "Choisissez un moment de livraison"; - static const String presentation = "Présentation"; - static const String presentation1 = - "L'AMAP (association pour le maintien d'une agriculture paysanne) est un service proposé par l'association Planet&Co de l'ECL. Vous pouvez ainsi recevoir des produits (paniers de fruits et légumes, jus, confitures...) directement sur le campus !\n\nLes commandes doivent être passées avant le vendredi 21h et sont livrées sur le campus le mardi de 13h à 13h45 (ou de 18h15 à 18h30 si vous ne pouvez pas passer le midi) dans le hall du M16.\n\nVous ne pouvez commander que si votre solde le permet. Vous pouvez recharger votre solde via la collecte Lydia ou bien avec un chèque que vous pouvez nous transmettre lors des permanences.\n\nLien vers la collecte Lydia pour le rechargement : "; - static const String presentation2 = - "\n\nN'hésitez pas à nous contacter en cas de problème !"; - static const String price = "Prix"; - static const String product = "produit"; - static const String products = "Produits"; - static const String productInDelivery = - "Produit dans une livraison non terminée"; - static const String quantity = "Quantité"; - static const String requiredDate = "La date est requise"; - static const String seeMore = "Voir plus"; - static const String the = "Le"; - static const String unlock = "Dévérouiller"; - static const String unlockedDelivery = "Livraison dévérouillée"; - static const String unlockingDelivery = "Dévérouiller la livraison ?"; - static const String update = "Modifier"; - static const String updatedAmount = "Solde modifié"; - static const String updatedOrder = "Commande modifiée"; - static const String updatedProduct = "Produit modifié"; - static const String updatingError = "Echec de la modification"; - static const String usersNotFound = "Aucun utilisateur trouvé"; - static const String waiting = "En attente"; -} diff --git a/lib/amap/tools/functions.dart b/lib/amap/tools/functions.dart index a3b5eba7dc..11088e67d8 100644 --- a/lib/amap/tools/functions.dart +++ b/lib/amap/tools/functions.dart @@ -1,14 +1,15 @@ +import 'package:flutter/widgets.dart'; import 'package:titan/amap/class/delivery.dart'; import 'package:titan/amap/class/order.dart'; -import 'package:titan/amap/tools/constants.dart'; +import 'package:titan/l10n/app_localizations.dart'; // Slots in Titan UI must changed based on language -String uiCollectionSlotToString(CollectionSlot slot) { +String uiCollectionSlotToString(CollectionSlot slot, BuildContext context) { switch (slot) { case CollectionSlot.midDay: - return AMAPTextConstants.midDay; + return AppLocalizations.of(context)!.amapMidDay; case CollectionSlot.evening: - return AMAPTextConstants.evening; + return AppLocalizations.of(context)!.amapEvening; } } diff --git a/lib/amap/ui/amap.dart b/lib/amap/ui/amap.dart index d2e8c6b9b9..dc52d09b93 100644 --- a/lib/amap/ui/amap.dart +++ b/lib/amap/ui/amap.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; +import 'package:qlevar_router/qlevar_router.dart'; import 'package:titan/amap/router.dart'; -import 'package:titan/amap/tools/constants.dart'; +import 'package:titan/tools/constants.dart'; import 'package:titan/tools/ui/widgets/top_bar.dart'; -import 'package:qlevar_router/qlevar_router.dart'; class AmapTemplate extends StatelessWidget { final Widget child; @@ -11,27 +11,29 @@ class AmapTemplate extends StatelessWidget { @override Widget build(BuildContext context) { - return SafeArea( - child: Column( - children: [ - TopBar( - title: AMAPTextConstants.amap, - root: AmapRouter.root, - rightIcon: QR.currentPath == AmapRouter.root - ? IconButton( - onPressed: () { - QR.to(AmapRouter.root + AmapRouter.presentation); - }, - icon: const HeroIcon( - HeroIcons.informationCircle, - color: Colors.black, - size: 40, - ), - ) - : null, - ), - Expanded(child: child), - ], + return Container( + color: ColorConstants.background, + child: SafeArea( + child: Column( + children: [ + TopBar( + root: AmapRouter.root, + rightIcon: QR.currentPath == AmapRouter.root + ? IconButton( + onPressed: () { + QR.to(AmapRouter.root + AmapRouter.presentation); + }, + icon: const HeroIcon( + HeroIcons.informationCircle, + color: Colors.black, + size: 40, + ), + ) + : null, + ), + Expanded(child: child), + ], + ), ), ); } diff --git a/lib/amap/ui/components/order_ui.dart b/lib/amap/ui/components/order_ui.dart index 6c92e6bbd3..ca26eac62d 100644 --- a/lib/amap/ui/components/order_ui.dart +++ b/lib/amap/ui/components/order_ui.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:titan/amap/class/order.dart'; import 'package:titan/amap/providers/user_amount_provider.dart'; import 'package:titan/amap/providers/user_order_list_provider.dart'; @@ -13,6 +14,7 @@ import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; +import 'package:titan/l10n/app_localizations.dart'; class OrderUI extends HookConsumerWidget { final Order order; @@ -29,6 +31,7 @@ class OrderUI extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final locale = Localizations.localeOf(context); final orderListNotifier = ref.watch(userOrderListProvider.notifier); final orderNotifier = ref.watch(orderProvider.notifier); final balanceNotifier = ref.watch(userAmountProvider.notifier); @@ -53,7 +56,7 @@ class OrderUI extends HookConsumerWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - '${AMAPTextConstants.the} ${processDate(order.deliveryDate)}', + '${AppLocalizations.of(context)!.amapThe} ${DateFormat.yMd(locale).format(order.deliveryDate)}', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -78,7 +81,7 @@ class OrderUI extends HookConsumerWidget { Row( children: [ Text( - "${order.products.length} ${AMAPTextConstants.product}${order.products.length != 1 ? "s" : ""}", + "${order.products.length} ${AppLocalizations.of(context)!.amapProduct}${order.products.length != 1 ? "s" : ""}", style: const TextStyle( fontSize: 17, fontWeight: FontWeight.w700, @@ -98,7 +101,7 @@ class OrderUI extends HookConsumerWidget { ), const SizedBox(height: 3), Text( - uiCollectionSlotToString(order.collectionSlot), + uiCollectionSlotToString(order.collectionSlot, context), style: const TextStyle( fontSize: 17, fontWeight: FontWeight.w700, @@ -132,9 +135,17 @@ class OrderUI extends HookConsumerWidget { await showDialog( context: context, builder: ((context) => CustomDialogBox( - title: AMAPTextConstants.delete, - descriptions: AMAPTextConstants.deletingOrder, + title: AppLocalizations.of(context)!.amapDelete, + descriptions: AppLocalizations.of( + context, + )!.amapDeletingOrder, onYes: () async { + final deletedOrderMsg = AppLocalizations.of( + context, + )!.amapDeletedOrder; + final deletingErrorMsg = AppLocalizations.of( + context, + )!.amapDeletingError; await tokenExpireWrapper(ref, () async { orderListNotifier.deleteOrder(order).then(( value, @@ -143,12 +154,12 @@ class OrderUI extends HookConsumerWidget { balanceNotifier.updateCash(order.amount); displayToastWithContext( TypeMsg.msg, - AMAPTextConstants.deletedOrder, + deletedOrderMsg, ); } else { displayToastWithContext( TypeMsg.error, - AMAPTextConstants.deletingError, + deletingErrorMsg, ); } }); @@ -171,9 +182,9 @@ class OrderUI extends HookConsumerWidget { ), ], ) - : const Text( - AMAPTextConstants.locked, - style: TextStyle( + : Text( + AppLocalizations.of(context)!.amapLocked, + style: const TextStyle( fontSize: 17, fontWeight: FontWeight.w700, color: AMAPColorConstants.textDark, diff --git a/lib/amap/ui/components/product_ui.dart b/lib/amap/ui/components/product_ui.dart index 3c3ef3c323..68d2901568 100644 --- a/lib/amap/ui/components/product_ui.dart +++ b/lib/amap/ui/components/product_ui.dart @@ -6,6 +6,7 @@ import 'package:titan/amap/tools/constants.dart'; import 'package:titan/tools/ui/layouts/card_button.dart'; import 'package:titan/tools/ui/layouts/card_layout.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; +import 'package:titan/l10n/app_localizations.dart'; class ProductCard extends StatelessWidget { final Product product; @@ -103,7 +104,7 @@ class ProductCard extends StatelessWidget { : Container( margin: const EdgeInsets.only(bottom: 5), child: Text( - "${AMAPTextConstants.quantity} : ${product.quantity}", + "${AppLocalizations.of(context)!.amapQuantity} : ${product.quantity}", style: const TextStyle( fontSize: 15, fontWeight: FontWeight.bold, diff --git a/lib/amap/ui/pages/admin_page/account_handler.dart b/lib/amap/ui/pages/admin_page/account_handler.dart index 3f20ade3d5..3a4b9dff46 100644 --- a/lib/amap/ui/pages/admin_page/account_handler.dart +++ b/lib/amap/ui/pages/admin_page/account_handler.dart @@ -11,6 +11,7 @@ import 'package:titan/tools/ui/layouts/card_layout.dart'; import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; import 'package:titan/tools/ui/widgets/styled_search_bar.dart'; import 'package:titan/user/providers/user_list_provider.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AccountHandler extends HookConsumerWidget { const AccountHandler({super.key}); @@ -28,7 +29,7 @@ class AccountHandler extends HookConsumerWidget { return Column( children: [ StyledSearchBar( - label: AMAPTextConstants.accounts, + label: AppLocalizations.of(context)!.amapAccounts, color: AMAPColorConstants.textDark, onChanged: (value) async { if (!searchingAmapUser) { diff --git a/lib/amap/ui/pages/admin_page/admin_page.dart b/lib/amap/ui/pages/admin_page/admin_page.dart index 328c3e652d..a7910e6fef 100644 --- a/lib/amap/ui/pages/admin_page/admin_page.dart +++ b/lib/amap/ui/pages/admin_page/admin_page.dart @@ -19,6 +19,7 @@ class AdminPage extends HookConsumerWidget { final productListNotifier = ref.read(productListProvider.notifier); return AmapTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await cashNotifier.loadCashList(); await deliveryListNotifier.loadDeliveriesList(); diff --git a/lib/amap/ui/pages/admin_page/delivery_handler.dart b/lib/amap/ui/pages/admin_page/delivery_handler.dart index b500fabeba..60be021992 100644 --- a/lib/amap/ui/pages/admin_page/delivery_handler.dart +++ b/lib/amap/ui/pages/admin_page/delivery_handler.dart @@ -13,6 +13,7 @@ import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/layouts/card_layout.dart'; import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class DeliveryHandler extends HookConsumerWidget { const DeliveryHandler({super.key}); @@ -24,9 +25,9 @@ class DeliveryHandler extends HookConsumerWidget { final selectedNotifier = ref.watch(selectedListProvider.notifier); return Column( children: [ - const AlignLeftText( - AMAPTextConstants.deliveries, - padding: EdgeInsets.symmetric(horizontal: 30), + AlignLeftText( + AppLocalizations.of(context)!.amapDeliveries, + padding: const EdgeInsets.symmetric(horizontal: 30), color: AMAPColorConstants.textDark, ), const SizedBox(height: 10), diff --git a/lib/amap/ui/pages/admin_page/delivery_ui.dart b/lib/amap/ui/pages/admin_page/delivery_ui.dart index 809115ea2b..38f0da2991 100644 --- a/lib/amap/ui/pages/admin_page/delivery_ui.dart +++ b/lib/amap/ui/pages/admin_page/delivery_ui.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:titan/amap/class/delivery.dart'; import 'package:titan/amap/providers/delivery_id_provider.dart'; import 'package:titan/amap/providers/delivery_list_provider.dart'; @@ -19,6 +20,7 @@ import 'package:titan/tools/functions.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class DeliveryUi extends HookConsumerWidget { final Delivery delivery; @@ -26,6 +28,7 @@ class DeliveryUi extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final locale = Localizations.localeOf(context); final deliveryIdNotifier = ref.watch(deliveryIdProvider.notifier); final deliveryListNotifier = ref.watch(deliveryListProvider.notifier); final deliveryOrders = ref.watch( @@ -67,7 +70,7 @@ class DeliveryUi extends HookConsumerWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - '${AMAPTextConstants.the} ${processDate(delivery.deliveryDate)}', + '${AppLocalizations.of(context)!.amapThe} ${DateFormat.yMd(locale).format(delivery.deliveryDate)}', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -102,8 +105,8 @@ class DeliveryUi extends HookConsumerWidget { dataBuilder: (context, orders) { return Text( orders.isEmpty - ? AMAPTextConstants.noCurrentOrder - : '${orders.length} ${AMAPTextConstants.oneOrder}${orders.length != 1 ? "s" : ""}', + ? AppLocalizations.of(context)!.amapNoCurrentOrder + : '${orders.length} ${AppLocalizations.of(context)!.amapOneOrder}${orders.length != 1 ? "s" : ""}', style: const TextStyle( fontSize: 15, fontWeight: FontWeight.bold, @@ -113,7 +116,7 @@ class DeliveryUi extends HookConsumerWidget { }, ), Text( - "${delivery.products.length} ${AMAPTextConstants.product}${delivery.products.length != 1 ? "s" : ""}", + "${delivery.products.length} ${AppLocalizations.of(context)!.amapProduct}${delivery.products.length != 1 ? "s" : ""}", style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w700, @@ -177,10 +180,19 @@ class DeliveryUi extends HookConsumerWidget { await showDialog( context: context, builder: ((context) => CustomDialogBox( - title: AMAPTextConstants.deleteDelivery, - descriptions: - AMAPTextConstants.deleteDeliveryDescription, + title: AppLocalizations.of( + context, + )!.amapDeleteDelivery, + descriptions: AppLocalizations.of( + context, + )!.amapDeleteDeliveryDescription, onYes: () async { + final deletedDeliveryMsg = AppLocalizations.of( + context, + )!.amapDeletedDelivery; + final deletingErrorMsg = AppLocalizations.of( + context, + )!.amapDeletingError; await tokenExpireWrapper(ref, () async { deliveryListNotifier .deleteDelivery(delivery) @@ -188,12 +200,12 @@ class DeliveryUi extends HookConsumerWidget { if (value) { displayVoteWithContext( TypeMsg.msg, - AMAPTextConstants.deletedDelivery, + deletedDeliveryMsg, ); } else { displayVoteWithContext( TypeMsg.error, - AMAPTextConstants.deletingError, + deletingErrorMsg, ); } }); @@ -221,20 +233,48 @@ class DeliveryUi extends HookConsumerWidget { context: context, builder: ((context) => CustomDialogBox( title: delivery.status == DeliveryStatus.creation - ? AMAPTextConstants.openDelivery + ? AppLocalizations.of(context)!.amapOpenDelivery : delivery.status == DeliveryStatus.available - ? AMAPTextConstants.lock + ? AppLocalizations.of(context)!.amapLock : delivery.status == DeliveryStatus.locked - ? AMAPTextConstants.deliver - : AMAPTextConstants.archive, + ? AppLocalizations.of(context)!.amapDeliver + : AppLocalizations.of(context)!.amapArchive, descriptions: delivery.status == DeliveryStatus.creation - ? AMAPTextConstants.openningDelivery + ? AppLocalizations.of(context)!.amapOpenningDelivery : delivery.status == DeliveryStatus.available - ? AMAPTextConstants.lockingDelivery + ? AppLocalizations.of(context)!.amapLockingDelivery : delivery.status == DeliveryStatus.locked - ? AMAPTextConstants.deliveringDelivery - : AMAPTextConstants.archivingDelivery, + ? AppLocalizations.of( + context, + )!.amapDeliveringDelivery + : AppLocalizations.of( + context, + )!.amapArchivingDelivery, onYes: () async { + final openedDeliveryMsg = AppLocalizations.of( + context, + )!.amapDeliveryOpened; + final notOpenedDeliveryMsg = AppLocalizations.of( + context, + )!.amapDeliveryNotOpened; + final lockedDeliveryMsg = AppLocalizations.of( + context, + )!.amapDeliveryLocked; + final notLockedDeliveryMsg = AppLocalizations.of( + context, + )!.amapDeliveryNotLocked; + final deliveredDeliveryMsg = AppLocalizations.of( + context, + )!.amapDeliveryDelivered; + final notDeliveredDeliveryMsg = AppLocalizations.of( + context, + )!.amapDeliveryNotDelivered; + final archivedDeliveryMsg = AppLocalizations.of( + context, + )!.amapDeliveryArchived; + final notArchivedDeliveryMsg = AppLocalizations.of( + context, + )!.amapDeliveryNotArchived; await tokenExpireWrapper(ref, () async { switch (delivery.status) { case DeliveryStatus.creation: @@ -243,12 +283,12 @@ class DeliveryUi extends HookConsumerWidget { if (value) { displayVoteWithContext( TypeMsg.msg, - AMAPTextConstants.deliveryOpened, + openedDeliveryMsg, ); } else { displayVoteWithContext( TypeMsg.error, - AMAPTextConstants.deliveryNotOpened, + notOpenedDeliveryMsg, ); } break; @@ -258,12 +298,12 @@ class DeliveryUi extends HookConsumerWidget { if (value) { displayVoteWithContext( TypeMsg.msg, - AMAPTextConstants.deliveryLocked, + lockedDeliveryMsg, ); } else { displayVoteWithContext( TypeMsg.error, - AMAPTextConstants.deliveryNotLocked, + notLockedDeliveryMsg, ); } break; @@ -273,12 +313,12 @@ class DeliveryUi extends HookConsumerWidget { if (value) { displayVoteWithContext( TypeMsg.msg, - AMAPTextConstants.deliveryDelivered, + deliveredDeliveryMsg, ); } else { displayVoteWithContext( TypeMsg.error, - AMAPTextConstants.deliveryNotDelivered, + notDeliveredDeliveryMsg, ); } break; @@ -288,12 +328,12 @@ class DeliveryUi extends HookConsumerWidget { if (value) { displayVoteWithContext( TypeMsg.msg, - AMAPTextConstants.deliveryArchived, + archivedDeliveryMsg, ); } else { displayVoteWithContext( TypeMsg.error, - AMAPTextConstants.deliveryNotArchived, + notArchivedDeliveryMsg, ); } break; @@ -345,12 +385,14 @@ class DeliveryUi extends HookConsumerWidget { padding: const EdgeInsets.only(bottom: 2), child: Text( delivery.status == DeliveryStatus.creation - ? AMAPTextConstants.openDelivery + ? AppLocalizations.of(context)!.amapOpenDelivery : delivery.status == DeliveryStatus.available - ? AMAPTextConstants.closeDelivery + ? AppLocalizations.of(context)!.amapCloseDelivery : delivery.status == DeliveryStatus.locked - ? AMAPTextConstants.endingDelivery - : AMAPTextConstants.archiveDelivery, + ? AppLocalizations.of(context)!.amapEndingDelivery + : AppLocalizations.of( + context, + )!.amapArchiveDelivery, style: const TextStyle( color: Colors.white, fontSize: 20, diff --git a/lib/amap/ui/pages/admin_page/product_handler.dart b/lib/amap/ui/pages/admin_page/product_handler.dart index 57abe7c04f..90b89117bb 100644 --- a/lib/amap/ui/pages/admin_page/product_handler.dart +++ b/lib/amap/ui/pages/admin_page/product_handler.dart @@ -15,6 +15,7 @@ import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class ProductHandler extends HookConsumerWidget { const ProductHandler({super.key}); @@ -37,9 +38,9 @@ class ProductHandler extends HookConsumerWidget { return Column( children: [ - const AlignLeftText( - padding: EdgeInsets.symmetric(horizontal: 30), - AMAPTextConstants.products, + AlignLeftText( + padding: const EdgeInsets.symmetric(horizontal: 30), + AppLocalizations.of(context)!.amapProducts, color: AMAPColorConstants.textDark, ), const SizedBox(height: 10), @@ -74,7 +75,9 @@ class ProductHandler extends HookConsumerWidget { ), ), products.isEmpty - ? const Center(child: Text(AMAPTextConstants.noProduct)) + ? Center( + child: Text(AppLocalizations.of(context)!.amapNoProduct), + ) : Row( children: products .map( @@ -84,22 +87,33 @@ class ProductHandler extends HookConsumerWidget { await showDialog( context: context, builder: (context) => CustomDialogBox( - title: AMAPTextConstants.deleteProduct, - descriptions: AMAPTextConstants - .deleteProductDescription, + title: AppLocalizations.of( + context, + )!.amapDeleteProduct, + descriptions: AppLocalizations.of( + context, + )!.amapDeleteProductDescription, onYes: () { + final deletedProductMsg = + AppLocalizations.of( + context, + )!.amapDeletedProduct; + final deletingErrorMsg = + AppLocalizations.of( + context, + )!.amapDeletingError; tokenExpireWrapper(ref, () async { final value = await productsNotifier .deleteProduct(e); if (value) { displayToastWithContext( TypeMsg.msg, - AMAPTextConstants.deletedProduct, + deletedProductMsg, ); } else { displayToastWithContext( TypeMsg.error, - AMAPTextConstants.productInDelivery, + deletingErrorMsg, ); } }); diff --git a/lib/amap/ui/pages/admin_page/user_cash_ui.dart b/lib/amap/ui/pages/admin_page/user_cash_ui.dart index 898f85f058..0f7f7a48e6 100644 --- a/lib/amap/ui/pages/admin_page/user_cash_ui.dart +++ b/lib/amap/ui/pages/admin_page/user_cash_ui.dart @@ -13,6 +13,7 @@ import 'package:titan/tools/functions.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; import 'package:titan/tools/ui/widgets/text_entry.dart'; +import 'package:titan/l10n/app_localizations.dart'; class UserCashUi extends HookConsumerWidget { final Cash cash; @@ -150,6 +151,12 @@ class UserCashUi extends HookConsumerWidget { if (key.currentState == null) { return; } + final updatedAmountMsg = AppLocalizations.of( + context, + )!.amapUpdatedAmount; + final updatingErrorMsg = AppLocalizations.of( + context, + )!.amapUpdatingError; if (key.currentState!.validate()) { await tokenExpireWrapper(ref, () async { await ref @@ -168,12 +175,12 @@ class UserCashUi extends HookConsumerWidget { toggle(); displayVoteWithContext( TypeMsg.msg, - AMAPTextConstants.updatedAmount, + updatedAmountMsg, ); } else { displayVoteWithContext( TypeMsg.error, - AMAPTextConstants.updatingError, + updatingErrorMsg, ); } }); diff --git a/lib/amap/ui/pages/delivery_pages/add_edit_delivery_cmd_page.dart b/lib/amap/ui/pages/delivery_pages/add_edit_delivery_cmd_page.dart index a7a01c23ba..8c8a08ef01 100644 --- a/lib/amap/ui/pages/delivery_pages/add_edit_delivery_cmd_page.dart +++ b/lib/amap/ui/pages/delivery_pages/add_edit_delivery_cmd_page.dart @@ -1,6 +1,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; import 'package:titan/amap/class/delivery.dart'; import 'package:titan/amap/providers/delivery_list_provider.dart'; import 'package:titan/amap/providers/delivery_order_list_provider.dart'; @@ -19,17 +20,19 @@ import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/widgets/date_entry.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AddEditDeliveryPage extends HookConsumerWidget { const AddEditDeliveryPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final locale = Localizations.localeOf(context); final formKey = GlobalKey(); final delivery = ref.watch(deliveryProvider); final isEdit = delivery.id != Delivery.empty().id; final dateController = useTextEditingController( - text: isEdit ? processDate(delivery.deliveryDate) : '', + text: isEdit ? DateFormat.yMd(locale).format(delivery.deliveryDate) : '', ); final productList = ref.watch(productListProvider); final sortedProductsList = ref.watch(sortedByCategoryProductsProvider); @@ -55,23 +58,23 @@ class AddEditDeliveryPage extends HookConsumerWidget { child: Column( children: [ const SizedBox(height: 20), - const AlignLeftText( - AMAPTextConstants.addDelivery, + AlignLeftText( + AppLocalizations.of(context)!.amapAddDelivery, color: AMAPColorConstants.green2, ), Container( margin: const EdgeInsets.symmetric(vertical: 30), child: DateEntry( onTap: () => getOnlyDayDate(context, dateController), - label: AMAPTextConstants.commandDate, + label: AppLocalizations.of(context)!.amapCommandDate, controller: dateController, enabledColor: AMAPColorConstants.enabled, color: AMAPColorConstants.greenGradient2, ), ), const SizedBox(height: 15), - const AlignLeftText( - AMAPTextConstants.commandProducts, + AlignLeftText( + AppLocalizations.of(context)!.amapCommandProducts, fontSize: 25, ), const SizedBox(height: 35), @@ -140,7 +143,7 @@ class AddEditDeliveryPage extends HookConsumerWidget { ) .toList(), deliveryDate: DateTime.parse( - processDateBack(date), + processDateBack(date, locale.toString()), ), status: DeliveryStatus.creation, ); @@ -148,6 +151,19 @@ class AddEditDeliveryPage extends HookConsumerWidget { final deliveryNotifier = ref.watch( deliveryListProvider.notifier, ); + final editedCommandMsg = AppLocalizations.of( + context, + )!.amapEditedCommand; + final addedCommandMsg = AppLocalizations.of( + context, + )!.amapAddedCommand; + final editingErrorMsg = AppLocalizations.of( + context, + )!.amapEditingError; + final alreadyExistCommandMsg = + AppLocalizations.of( + context, + )!.amapAlreadyExistCommand; final value = isEdit ? await deliveryNotifier.updateDelivery( del, @@ -158,7 +174,7 @@ class AddEditDeliveryPage extends HookConsumerWidget { if (isEdit) { displayToastWithContext( TypeMsg.msg, - AMAPTextConstants.editedCommand, + editedCommandMsg, ); } else { final deliveryOrdersNotifier = ref.watch( @@ -174,19 +190,19 @@ class AddEditDeliveryPage extends HookConsumerWidget { }); displayToastWithContext( TypeMsg.msg, - AMAPTextConstants.addedCommand, + addedCommandMsg, ); } } else { if (isEdit) { displayToastWithContext( TypeMsg.error, - AMAPTextConstants.editingError, + editingErrorMsg, ); } else { displayToastWithContext( TypeMsg.error, - AMAPTextConstants.alreadyExistCommand, + alreadyExistCommandMsg, ); } } @@ -195,14 +211,18 @@ class AddEditDeliveryPage extends HookConsumerWidget { displayToast( context, TypeMsg.error, - AMAPTextConstants.addingError, + AppLocalizations.of(context)!.amapAddingError, ); } }, child: Text( isEdit - ? AMAPTextConstants.editDelivery - : AMAPTextConstants.addDelivery, + ? AppLocalizations.of( + context, + )!.amapEditDelivery + : AppLocalizations.of( + context, + )!.amapAddDelivery, style: TextStyle( fontSize: 20, fontWeight: FontWeight.w700, diff --git a/lib/amap/ui/pages/detail_delivery_page/detail_page.dart b/lib/amap/ui/pages/detail_delivery_page/detail_page.dart index b84ed8b469..6aef738a04 100644 --- a/lib/amap/ui/pages/detail_delivery_page/detail_page.dart +++ b/lib/amap/ui/pages/detail_delivery_page/detail_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:titan/amap/class/order.dart'; import 'package:titan/amap/class/product.dart'; import 'package:titan/amap/providers/cash_list_provider.dart'; @@ -12,17 +13,18 @@ import 'package:titan/amap/tools/constants.dart'; import 'package:titan/amap/ui/amap.dart'; import 'package:titan/amap/ui/pages/detail_delivery_page/order_detail_ui.dart'; import 'package:titan/amap/ui/pages/detail_delivery_page/product_detail_ui.dart'; -import 'package:titan/tools/functions.dart'; import 'package:titan/tools/ui/widgets/align_left_text.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/widgets/loader.dart'; import 'package:titan/tools/ui/layouts/refresher.dart'; +import 'package:titan/l10n/app_localizations.dart'; class DetailDeliveryPage extends HookConsumerWidget { const DetailDeliveryPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final locale = Localizations.localeOf(context); final delivery = ref.watch(deliveryProvider); final deliveryOrders = ref.watch(adminDeliveryOrderListProvider); final orders = deliveryOrders[delivery.id]; @@ -36,6 +38,7 @@ class DetailDeliveryPage extends HookConsumerWidget { final cash = ref.watch(cashListProvider); return AmapTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await deliveryProductListNotifier.loadProductList(delivery.products); await deliveryListNotifier.loadDeliveriesList(); @@ -47,15 +50,15 @@ class DetailDeliveryPage extends HookConsumerWidget { child: Column( children: [ Text( - "${AMAPTextConstants.deliveryDate} : ${processDate(delivery.deliveryDate)}", + "${AppLocalizations.of(context)!.amapDeliveryDate} : ${DateFormat.yMd(locale).format(delivery.deliveryDate)}", style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 20), - const AlignLeftText( - "${AMAPTextConstants.products} :", + AlignLeftText( + "${AppLocalizations.of(context)!.amapProducts} :", color: AMAPColorConstants.textDark, ), ], @@ -111,9 +114,9 @@ class DetailDeliveryPage extends HookConsumerWidget { ); }).values, const SizedBox(height: 20), - const AlignLeftText( - "${AMAPTextConstants.orders} :", - padding: EdgeInsets.only(left: 30), + AlignLeftText( + "${AppLocalizations.of(context)!.amapOrders} :", + padding: const EdgeInsets.only(left: 30), color: AMAPColorConstants.textDark, ), const SizedBox(height: 30), @@ -128,8 +131,10 @@ class DetailDeliveryPage extends HookConsumerWidget { if (data.isEmpty) { return Container( margin: const EdgeInsets.only(bottom: 50), - child: const Center( - child: Text(AMAPTextConstants.noOrder), + child: Center( + child: Text( + AppLocalizations.of(context)!.amapNoOrder, + ), ), ); } diff --git a/lib/amap/ui/pages/detail_delivery_page/order_detail_ui.dart b/lib/amap/ui/pages/detail_delivery_page/order_detail_ui.dart index 73963f39f3..0c8065e7f5 100644 --- a/lib/amap/ui/pages/detail_delivery_page/order_detail_ui.dart +++ b/lib/amap/ui/pages/detail_delivery_page/order_detail_ui.dart @@ -14,6 +14,7 @@ import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; +import 'package:titan/l10n/app_localizations.dart'; class DetailOrderUI extends HookConsumerWidget { final Order order; @@ -105,7 +106,7 @@ class DetailOrderUI extends HookConsumerWidget { Row( children: [ Text( - "${order.products.fold(0, (value, product) => value + product.quantity)} ${AMAPTextConstants.product}${order.products.fold(0, (value, product) => value + product.quantity) != 1 ? "s" : ""}", + "${order.products.fold(0, (value, product) => value + product.quantity)} ${AppLocalizations.of(context)!.amapProduct}${order.products.fold(0, (value, product) => value + product.quantity) != 1 ? "s" : ""}", style: const TextStyle( fontSize: 17, fontWeight: FontWeight.w700, @@ -127,7 +128,7 @@ class DetailOrderUI extends HookConsumerWidget { Row( children: [ Text( - "${AMAPTextConstants.amount} : ${userCash.balance.toStringAsFixed(2)}€", + "${AppLocalizations.of(context)!.amapAmount} : ${userCash.balance.toStringAsFixed(2)}€", style: const TextStyle( fontSize: 17, fontWeight: FontWeight.w700, @@ -140,9 +141,17 @@ class DetailOrderUI extends HookConsumerWidget { await showDialog( context: context, builder: ((context) => CustomDialogBox( - title: AMAPTextConstants.delete, - descriptions: AMAPTextConstants.deletingOrder, + title: AppLocalizations.of(context)!.amapDelete, + descriptions: AppLocalizations.of( + context, + )!.amapDeletingOrder, onYes: () async { + final deletedOrderMsg = AppLocalizations.of( + context, + )!.amapDeletedOrder; + final deletingErrorMsg = AppLocalizations.of( + context, + )!.amapDeletingError; await tokenExpireWrapper(ref, () async { final index = orderList.maybeWhen( data: (data) => data.indexWhere( @@ -167,12 +176,12 @@ class DetailOrderUI extends HookConsumerWidget { ); displayToastWithContext( TypeMsg.msg, - AMAPTextConstants.deletedOrder, + deletedOrderMsg, ); } else { displayToastWithContext( TypeMsg.error, - AMAPTextConstants.deletingError, + deletingErrorMsg, ); } }); diff --git a/lib/amap/ui/pages/detail_delivery_page/product_detail_ui.dart b/lib/amap/ui/pages/detail_delivery_page/product_detail_ui.dart index 08db18c5d5..6376a197b3 100644 --- a/lib/amap/ui/pages/detail_delivery_page/product_detail_ui.dart +++ b/lib/amap/ui/pages/detail_delivery_page/product_detail_ui.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:titan/amap/class/product.dart'; import 'package:titan/amap/tools/constants.dart'; import 'package:titan/tools/ui/layouts/card_layout.dart'; +import 'package:titan/l10n/app_localizations.dart'; class ProductDetailCard extends StatelessWidget { final Product product; @@ -41,7 +42,7 @@ class ProductDetailCard extends StatelessWidget { ), const SizedBox(height: 4), AutoSizeText( - "${AMAPTextConstants.quantity} : $quantity", + "${AppLocalizations.of(context)!.amapQuantity} : $quantity", maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( diff --git a/lib/amap/ui/pages/detail_page/detail_page.dart b/lib/amap/ui/pages/detail_page/detail_page.dart index cdb4009ade..3e204b7226 100644 --- a/lib/amap/ui/pages/detail_page/detail_page.dart +++ b/lib/amap/ui/pages/detail_page/detail_page.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/amap/providers/order_provider.dart'; -import 'package:titan/amap/tools/constants.dart'; import 'package:titan/amap/ui/amap.dart'; import 'package:titan/amap/ui/components/order_ui.dart'; import 'package:titan/amap/ui/components/product_ui.dart'; import 'package:titan/tools/ui/widgets/align_left_text.dart'; +import 'package:titan/l10n/app_localizations.dart'; class DetailPage extends HookConsumerWidget { const DetailPage({super.key}); @@ -39,9 +39,9 @@ class DetailPage extends HookConsumerWidget { child: Column( children: [ const SizedBox(height: 50), - const AlignLeftText( - padding: EdgeInsets.symmetric(horizontal: 20), - AMAPTextConstants.products, + AlignLeftText( + padding: const EdgeInsets.symmetric(horizontal: 20), + AppLocalizations.of(context)!.amapProducts, fontSize: 25, ), const SizedBox(height: 10), diff --git a/lib/amap/ui/pages/list_products_page/category_page.dart b/lib/amap/ui/pages/list_products_page/category_page.dart index a390f1472e..9afb0a22cf 100644 --- a/lib/amap/ui/pages/list_products_page/category_page.dart +++ b/lib/amap/ui/pages/list_products_page/category_page.dart @@ -10,6 +10,7 @@ import 'package:titan/amap/tools/constants.dart'; import 'package:titan/amap/ui/pages/list_products_page/product_ui_list.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/ui/widgets/align_left_text.dart'; +import 'package:titan/l10n/app_localizations.dart'; class CategoryPage extends HookConsumerWidget { final String category; @@ -160,7 +161,7 @@ class CategoryPage extends HookConsumerWidget { color: AMAPColorConstants.background, ), Text( - AMAPTextConstants.seeMore, + AppLocalizations.of(context)!.amapSeeMore, style: TextStyle( fontSize: 18, color: AMAPColorConstants.background, diff --git a/lib/amap/ui/pages/list_products_page/list_products.dart b/lib/amap/ui/pages/list_products_page/list_products.dart index e2829189e6..7d831a439f 100644 --- a/lib/amap/ui/pages/list_products_page/list_products.dart +++ b/lib/amap/ui/pages/list_products_page/list_products.dart @@ -6,10 +6,10 @@ import 'package:titan/amap/providers/scroll_provider.dart'; import 'package:titan/amap/providers/sorted_delivery_product.dart'; import 'package:titan/amap/providers/page_controller_provider.dart'; import 'package:titan/amap/providers/scroll_controller_provider.dart'; -import 'package:titan/amap/tools/constants.dart'; import 'package:titan/amap/ui/pages/list_products_page/category_page.dart'; import 'package:titan/amap/ui/pages/list_products_page/web_page_navigation_button.dart'; -import 'package:titan/drawer/providers/is_web_format_provider.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/providers/is_web_format_provider.dart'; class ListProducts extends HookConsumerWidget { const ListProducts({super.key}); @@ -48,10 +48,10 @@ class ListProducts extends HookConsumerWidget { physics: const BouncingScrollPhysics(), children: sortedDeliveryProductsList.isEmpty ? [ - const Center( + Center( child: Text( - AMAPTextConstants.noProduct, - style: TextStyle( + AppLocalizations.of(context)!.amapNoProduct, + style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, ), diff --git a/lib/amap/ui/pages/list_products_page/product_choice_button.dart b/lib/amap/ui/pages/list_products_page/product_choice_button.dart index 789ba76192..07fd6634da 100644 --- a/lib/amap/ui/pages/list_products_page/product_choice_button.dart +++ b/lib/amap/ui/pages/list_products_page/product_choice_button.dart @@ -13,6 +13,7 @@ import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; import 'package:titan/user/providers/user_provider.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class ProductChoiceButton extends HookConsumerWidget { const ProductChoiceButton({super.key}); @@ -69,7 +70,7 @@ class ProductChoiceButton extends HookConsumerWidget { displayToast( context, TypeMsg.error, - AMAPTextConstants.noProduct, + AppLocalizations.of(context)!.amapNoProduct, ); } else { Order newOrder = order.copyWith( @@ -78,6 +79,18 @@ class ProductChoiceButton extends HookConsumerWidget { lastAmount: order.amount, ); await tokenExpireWrapper(ref, () async { + final updatedOrderMsg = AppLocalizations.of( + context, + )!.amapUpdatedOrder; + final addedOrderMsg = AppLocalizations.of( + context, + )!.amapAddedOrder; + final updatingErrorMsg = AppLocalizations.of( + context, + )!.amapUpdatingError; + final addingErrorMsg = AppLocalizations.of( + context, + )!.amapAddingError; final value = isEdit ? await orderListNotifier.updateOrder(newOrder) : await orderListNotifier.addOrder(newOrder); @@ -87,34 +100,25 @@ class ProductChoiceButton extends HookConsumerWidget { order.lastAmount - order.amount, ); if (isEdit) { - displayToastWithContext( - TypeMsg.msg, - AMAPTextConstants.updatedOrder, - ); + displayToastWithContext(TypeMsg.msg, updatedOrderMsg); } else { - displayToastWithContext( - TypeMsg.msg, - AMAPTextConstants.addedOrder, - ); + displayToastWithContext(TypeMsg.msg, addedOrderMsg); } } else { if (isEdit) { displayToastWithContext( TypeMsg.error, - AMAPTextConstants.updatingError, + updatingErrorMsg, ); } else { - displayToastWithContext( - TypeMsg.error, - AMAPTextConstants.addingError, - ); + displayToastWithContext(TypeMsg.error, addingErrorMsg); } } }); } }, child: Text( - "${AMAPTextConstants.confirm} (${order.amount.toStringAsFixed(2)}€)", + "${AppLocalizations.of(context)!.amapConfirm} (${order.amount.toStringAsFixed(2)}€)", style: TextStyle( fontSize: 20, fontWeight: FontWeight.w700, @@ -159,8 +163,10 @@ class ProductChoiceButton extends HookConsumerWidget { showDialog( context: context, builder: (BuildContext context) => CustomDialogBox( - descriptions: AMAPTextConstants.deletingOrder, - title: AMAPTextConstants.deleting, + descriptions: AppLocalizations.of( + context, + )!.amapDeletingOrder, + title: AppLocalizations.of(context)!.amapDeleting, onYes: () { orderNotifier.setOrder(Order.empty()); QR.back(); diff --git a/lib/amap/ui/pages/main_page/collection_slot_selector.dart b/lib/amap/ui/pages/main_page/collection_slot_selector.dart index 9de132519b..3a541075da 100644 --- a/lib/amap/ui/pages/main_page/collection_slot_selector.dart +++ b/lib/amap/ui/pages/main_page/collection_slot_selector.dart @@ -38,7 +38,7 @@ class CollectionSlotSelector extends HookConsumerWidget { ), child: Center( child: Text( - capitalize(uiCollectionSlotToString(collectionSlot)), + capitalize(uiCollectionSlotToString(collectionSlot, context)), style: TextStyle( fontSize: 25, fontWeight: FontWeight.bold, diff --git a/lib/amap/ui/pages/main_page/delivery_section.dart b/lib/amap/ui/pages/main_page/delivery_section.dart index 65aba3ecfd..0921d0010e 100644 --- a/lib/amap/ui/pages/main_page/delivery_section.dart +++ b/lib/amap/ui/pages/main_page/delivery_section.dart @@ -7,6 +7,7 @@ import 'package:titan/amap/tools/constants.dart'; import 'package:titan/amap/ui/pages/main_page/delivery_ui.dart'; import 'package:titan/tools/ui/widgets/align_left_text.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/l10n/app_localizations.dart'; class DeliverySection extends HookConsumerWidget { final bool showSelected; @@ -30,7 +31,7 @@ class DeliverySection extends HookConsumerWidget { return Column( children: [ AlignLeftText( - AMAPTextConstants.deliveries, + AppLocalizations.of(context)!.amapDeliveries, padding: const EdgeInsets.symmetric(horizontal: 30), color: showSelected ? Colors.white : AMAPColorConstants.textDark, ), @@ -38,8 +39,10 @@ class DeliverySection extends HookConsumerWidget { value: deliveries, builder: (context, data) { if (availableDeliveries.isEmpty) { - return const Center( - child: Text(AMAPTextConstants.notPlannedDelivery), + return Center( + child: Text( + AppLocalizations.of(context)!.amapNotPlannedDelivery, + ), ); } return SingleChildScrollView( diff --git a/lib/amap/ui/pages/main_page/delivery_ui.dart b/lib/amap/ui/pages/main_page/delivery_ui.dart index 14d82a026e..903e3eb26a 100644 --- a/lib/amap/ui/pages/main_page/delivery_ui.dart +++ b/lib/amap/ui/pages/main_page/delivery_ui.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:titan/amap/class/delivery.dart'; import 'package:titan/amap/providers/delivery_provider.dart'; import 'package:titan/amap/tools/constants.dart'; -import 'package:titan/tools/functions.dart'; +import 'package:titan/l10n/app_localizations.dart'; class DeliveryUi extends HookConsumerWidget { final Delivery delivery; @@ -18,6 +19,7 @@ class DeliveryUi extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final locale = Localizations.localeOf(context); final selectedDelivery = ref.watch(deliveryProvider); final selected = selectedDelivery.id == delivery.id; return GestureDetector( @@ -55,7 +57,7 @@ class DeliveryUi extends HookConsumerWidget { children: [ const SizedBox(width: 10), Text( - '${AMAPTextConstants.the} ${processDate(delivery.deliveryDate)}', + '${AppLocalizations.of(context)!.amapThe} ${DateFormat.yMd(locale).format(delivery.deliveryDate)}', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -66,7 +68,7 @@ class DeliveryUi extends HookConsumerWidget { ), const Spacer(), Text( - "${delivery.products.length} ${AMAPTextConstants.product}${delivery.products.length != 1 ? "s" : ""}", + "${delivery.products.length} ${AppLocalizations.of(context)!.amapProduct}${delivery.products.length != 1 ? "s" : ""}", style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, diff --git a/lib/amap/ui/pages/main_page/main_page.dart b/lib/amap/ui/pages/main_page/main_page.dart index 1d5c673abf..3f38eb0bee 100644 --- a/lib/amap/ui/pages/main_page/main_page.dart +++ b/lib/amap/ui/pages/main_page/main_page.dart @@ -26,6 +26,7 @@ import 'package:titan/tools/ui/layouts/refresher.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/user/providers/user_provider.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AmapMainPage extends HookConsumerWidget { const AmapMainPage({super.key}); @@ -65,6 +66,7 @@ class AmapMainPage extends HookConsumerWidget { return AmapTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await ordersNotifier.loadOrderList(me.id); await balanceNotifier.loadCashByUser(me.id); @@ -83,7 +85,7 @@ class AmapMainPage extends HookConsumerWidget { child: AsyncChild( value: balance, builder: (context, s) => Text( - "${AMAPTextConstants.amount} : ${s.balance.toStringAsFixed(2)}€", + "${AppLocalizations.of(context)!.amapAmount} : ${s.balance.toStringAsFixed(2)}€", style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, @@ -181,8 +183,8 @@ class AmapMainPage extends HookConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const AlignLeftText( - AMAPTextConstants.addOrder, + AlignLeftText( + AppLocalizations.of(context)!.amapAddOrder, color: Colors.white, ), IconButton( @@ -234,7 +236,9 @@ class AmapMainPage extends HookConsumerWidget { } else { displayToastWithoutContext( TypeMsg.error, - AMAPTextConstants.noSelectedDelivery, + AppLocalizations.of( + context, + )!.amapNoSelectedDelivery, ); } }, @@ -267,10 +271,10 @@ class AmapMainPage extends HookConsumerWidget { child: Container( padding: const EdgeInsets.only(bottom: 5), width: double.infinity, - child: const Center( + child: Center( child: Text( - AMAPTextConstants.nextStep, - style: TextStyle( + AppLocalizations.of(context)!.amapNextStep, + style: const TextStyle( fontSize: 25, fontWeight: FontWeight.w900, color: Colors.white, diff --git a/lib/amap/ui/pages/main_page/orders_section.dart b/lib/amap/ui/pages/main_page/orders_section.dart index bbfca64b5d..f59c684f73 100644 --- a/lib/amap/ui/pages/main_page/orders_section.dart +++ b/lib/amap/ui/pages/main_page/orders_section.dart @@ -13,6 +13,7 @@ import 'package:titan/tools/ui/widgets/align_left_text.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/layouts/card_layout.dart'; import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; +import 'package:titan/l10n/app_localizations.dart'; class OrderSection extends HookConsumerWidget { final VoidCallback onTap, addOrder, onEdit; @@ -38,9 +39,9 @@ class OrderSection extends HookConsumerWidget { return Column( children: [ - const AlignLeftText( - AMAPTextConstants.orders, - padding: EdgeInsets.symmetric(horizontal: 30), + AlignLeftText( + AppLocalizations.of(context)!.amapOrders, + padding: const EdgeInsets.symmetric(horizontal: 30), color: AMAPColorConstants.textDark, ), const SizedBox(height: 10), diff --git a/lib/amap/ui/pages/presentation_page/text.dart b/lib/amap/ui/pages/presentation_page/text.dart index 16caacedf2..6933662d84 100644 --- a/lib/amap/ui/pages/presentation_page/text.dart +++ b/lib/amap/ui/pages/presentation_page/text.dart @@ -7,6 +7,7 @@ import 'package:titan/amap/ui/amap.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:titan/l10n/app_localizations.dart'; class PresentationPage extends HookConsumerWidget { const PresentationPage({super.key}); @@ -31,7 +32,7 @@ class PresentationPage extends HookConsumerWidget { text: TextSpan( children: [ TextSpan( - text: AMAPTextConstants.presentation1, + text: AppLocalizations.of(context)!.amapPresentation1, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, @@ -60,6 +61,9 @@ class PresentationPage extends HookConsumerWidget { ), recognizer: TapGestureRecognizer() ..onTap = () async { + final errorLinkMsg = AppLocalizations.of( + context, + )!.amapErrorLink; try { await launchUrl( Uri.parse(info.link), @@ -68,23 +72,22 @@ class PresentationPage extends HookConsumerWidget { } catch (e) { displayToastWithContext( TypeMsg.msg, - AMAPTextConstants.errorLink, + errorLinkMsg, ); } }, ), - error: (Object error, StackTrace stackTrace) => - const TextSpan( - text: AMAPTextConstants.loadingError, - style: TextStyle(color: Colors.red), - ), - loading: () => const TextSpan( - text: AMAPTextConstants.loading, - style: TextStyle(color: Colors.red), + error: (Object error, StackTrace stackTrace) => TextSpan( + text: AppLocalizations.of(context)!.amapLoadingError, + style: const TextStyle(color: Colors.red), + ), + loading: () => TextSpan( + text: AppLocalizations.of(context)!.amapLoading, + style: const TextStyle(color: Colors.red), ), ), TextSpan( - text: AMAPTextConstants.presentation2, + text: AppLocalizations.of(context)!.amapPresentation2, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, @@ -109,7 +112,7 @@ class PresentationPage extends HookConsumerWidget { AsyncChild( value: information, builder: (context, info) => Text( - "${AMAPTextConstants.contact} : ${info.manager} ", + "${AppLocalizations.of(context)!.amapContact} : ${info.manager} ", style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, diff --git a/lib/amap/ui/pages/product_pages/add_edit_product.dart b/lib/amap/ui/pages/product_pages/add_edit_product.dart index efdec80220..5c716ce56c 100644 --- a/lib/amap/ui/pages/product_pages/add_edit_product.dart +++ b/lib/amap/ui/pages/product_pages/add_edit_product.dart @@ -16,6 +16,7 @@ import 'package:titan/tools/ui/widgets/align_left_text.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; import 'package:titan/tools/ui/widgets/text_entry.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AddEditProduct extends HookConsumerWidget { const AddEditProduct({super.key}); @@ -34,7 +35,7 @@ class AddEditProduct extends HookConsumerWidget { ); final beginState = isEdit ? product.category - : AMAPTextConstants.createCategory; + : AppLocalizations.of(context)!.amapCreateCategory; final categoryController = ref.watch(selectedCategoryProvider(beginState)); final categoryNotifier = ref.watch( selectedCategoryProvider(beginState).notifier, @@ -57,8 +58,8 @@ class AddEditProduct extends HookConsumerWidget { const SizedBox(height: 10), AlignLeftText( isEdit - ? AMAPTextConstants.editProduct - : AMAPTextConstants.addProduct, + ? AppLocalizations.of(context)!.amapEditProduct + : AppLocalizations.of(context)!.amapAddProduct, color: AMAPColorConstants.green2, ), const SizedBox(height: 40), @@ -66,7 +67,7 @@ class AddEditProduct extends HookConsumerWidget { children: [ Center( child: TextEntry( - label: AMAPTextConstants.name, + label: AppLocalizations.of(context)!.amapName, controller: nameController, color: AMAPColorConstants.greenGradient2, enabledColor: AMAPColorConstants.enabled, @@ -75,7 +76,7 @@ class AddEditProduct extends HookConsumerWidget { const SizedBox(height: 30), Center( child: TextEntry( - label: AMAPTextConstants.price, + label: AppLocalizations.of(context)!.amapPrice, isDouble: true, color: AMAPColorConstants.greenGradient2, enabledColor: AMAPColorConstants.enabled, @@ -84,15 +85,15 @@ class AddEditProduct extends HookConsumerWidget { ), ), const SizedBox(height: 30), - const AlignLeftText( - AMAPTextConstants.category, + AlignLeftText( + AppLocalizations.of(context)!.amapCategory, fontSize: 20, color: AMAPColorConstants.greenGradient2, ), const SizedBox(height: 10), Center( child: DropdownButtonFormField( - value: categoryController, + initialValue: categoryController, decoration: const InputDecoration( enabledBorder: UnderlineInputBorder( borderSide: BorderSide( @@ -110,32 +111,41 @@ class AddEditProduct extends HookConsumerWidget { ), ), ), - items: [AMAPTextConstants.createCategory, ...categories] - .map((String value) { + items: + [ + AppLocalizations.of(context)!.amapCreateCategory, + ...categories, + ].map((String value) { return DropdownMenuItem( value: value, child: Text(value), ); - }) - .toList(), + }).toList(), onChanged: (value) { categoryNotifier.setText( - value ?? AMAPTextConstants.createCategory, + value ?? + AppLocalizations.of( + context, + )!.amapCreateCategory, ); newCategory.text = ""; }, ), ), if (categoryController == - AMAPTextConstants.createCategory) ...[ + AppLocalizations.of(context)!.amapCreateCategory) ...[ const SizedBox(height: 30), Center( child: TextEntry( - label: AMAPTextConstants.createCategory, - noValueError: AMAPTextConstants.pickChooseCategory, + label: AppLocalizations.of( + context, + )!.amapCreateCategory, + noValueError: AppLocalizations.of( + context, + )!.amapPickChooseCategory, enabled: categoryController == - AMAPTextConstants.createCategory, + AppLocalizations.of(context)!.amapCreateCategory, onChanged: (value) { newCategory.text = value; newCategory.selection = TextSelection.fromPosition( @@ -162,7 +172,9 @@ class AddEditProduct extends HookConsumerWidget { if (formKey.currentState!.validate()) { String cate = categoryController == - AMAPTextConstants.createCategory + AppLocalizations.of( + context, + )!.amapCreateCategory ? newCategory.text : categoryController; Product newProduct = Product( @@ -175,6 +187,18 @@ class AddEditProduct extends HookConsumerWidget { quantity: 0, ); await tokenExpireWrapper(ref, () async { + final updatedProductMsg = isEdit + ? AppLocalizations.of( + context, + )!.amapUpdatedProduct + : AppLocalizations.of( + context, + )!.amapAddedProduct; + final addingErrorMsg = isEdit + ? AppLocalizations.of( + context, + )!.amapUpdatingError + : AppLocalizations.of(context)!.amapAddingError; final value = isEdit ? await productsNotifier.updateProduct( newProduct, @@ -183,10 +207,6 @@ class AddEditProduct extends HookConsumerWidget { if (value) { if (isEdit) { formKey.currentState!.reset(); - displayToastWithContext( - TypeMsg.msg, - AMAPTextConstants.updatedProduct, - ); } else { ref .watch(selectedListProvider.notifier) @@ -196,23 +216,16 @@ class AddEditProduct extends HookConsumerWidget { orElse: () => [], ), ); - displayToastWithContext( - TypeMsg.msg, - AMAPTextConstants.addedProduct, - ); } + displayToastWithContext( + TypeMsg.msg, + updatedProductMsg, + ); } else { - if (isEdit) { - displayToastWithContext( - TypeMsg.error, - AMAPTextConstants.updatingError, - ); - } else { - displayToastWithContext( - TypeMsg.error, - AMAPTextConstants.addingError, - ); - } + displayToastWithContext( + TypeMsg.error, + addingErrorMsg, + ); } QR.back(); }); @@ -220,8 +233,8 @@ class AddEditProduct extends HookConsumerWidget { }, child: Text( isEdit - ? AMAPTextConstants.update - : AMAPTextConstants.add, + ? AppLocalizations.of(context)!.amapUpdate + : AppLocalizations.of(context)!.amapAdd, style: TextStyle( fontSize: 20, fontWeight: FontWeight.w700, diff --git a/lib/auth/providers/openid_provider.dart b/lib/auth/providers/openid_provider.dart index 9fc42216ad..d0c3bcf3c8 100644 --- a/lib/auth/providers/openid_provider.dart +++ b/lib/auth/providers/openid_provider.dart @@ -19,10 +19,7 @@ final authTokenProvider = StateNotifierProvider>>( (ref) { OpenIdTokenProvider openIdTokenProvider = OpenIdTokenProvider(); - final isConnected = ref.watch(isConnectedProvider); - if (isConnected) { - openIdTokenProvider.getTokenFromStorage(); - } + openIdTokenProvider.getTokenFromStorage(); return openIdTokenProvider; }, ); @@ -123,8 +120,11 @@ class OpenIdTokenProvider final String refreshTokenKey = "refresh_token"; final String redirectURLScheme = "${getTitanPackageName()}://authorized"; final String redirectURL = "${getTitanURL()}/static.html"; - final String discoveryUrl = - "${Repository.host}.well-known/openid-configuration"; + final AuthorizationServiceConfiguration authorizationServiceConfiguration = + AuthorizationServiceConfiguration( + authorizationEndpoint: "${Repository.host}auth/authorize", + tokenEndpoint: "${Repository.host}auth/token", + ); final List scopes = ["API"]; OpenIdTokenProvider() : super(const AsyncValue.loading()); @@ -221,7 +221,7 @@ class OpenIdTokenProvider AuthorizationTokenRequest( clientId, redirectURLScheme, - discoveryUrl: discoveryUrl, + serviceConfiguration: authorizationServiceConfiguration, scopes: scopes, allowInsecureConnections: kDebugMode, ), @@ -239,7 +239,8 @@ class OpenIdTokenProvider Future getTokenFromStorage() async { state = const AsyncValue.loading(); - _secureStorage.read(key: tokenName).then((token) async { + try { + final token = await _secureStorage.read(key: tokenName); if (token != null) { try { if (kIsWeb) { @@ -262,7 +263,7 @@ class OpenIdTokenProvider TokenRequest( clientId, redirectURLScheme, - discoveryUrl: discoveryUrl, + serviceConfiguration: authorizationServiceConfiguration, scopes: scopes, refreshToken: token, allowInsecureConnections: kDebugMode, @@ -280,51 +281,62 @@ class OpenIdTokenProvider } else { state = const AsyncValue.error("No token found", StackTrace.empty); } - }); + } catch (e) { + state = AsyncValue.error(e, StackTrace.empty); + } } Future getAuthToken(String authorizationToken) async { - appAuth - .token( - TokenRequest( - clientId, - redirectURLScheme, - discoveryUrl: discoveryUrl, - scopes: scopes, - authorizationCode: authorizationToken, - allowInsecureConnections: kDebugMode, - ), - ) - .then((resp) { - state = AsyncValue.data({ - tokenKey: resp.accessToken!, - refreshTokenKey: resp.refreshToken!, - }); - }); + try { + final resp = await appAuth.token( + TokenRequest( + clientId, + redirectURLScheme, + serviceConfiguration: authorizationServiceConfiguration, + scopes: scopes, + authorizationCode: authorizationToken, + allowInsecureConnections: kDebugMode, + ), + ); + state = AsyncValue.data({ + tokenKey: resp.accessToken!, + refreshTokenKey: resp.refreshToken!, + }); + } catch (e) { + state = AsyncValue.error(e, StackTrace.empty); + } } Future refreshToken() async { return state.when( data: (token) async { if (token[refreshTokenKey] != null && token[refreshTokenKey] != "") { - TokenResponse? resp = await appAuth.token( - TokenRequest( - clientId, - redirectURLScheme, - discoveryUrl: discoveryUrl, - scopes: scopes, - refreshToken: token[refreshTokenKey] as String, - allowInsecureConnections: kDebugMode, - ), - ); - state = AsyncValue.data({ - tokenKey: resp.accessToken!, - refreshTokenKey: resp.refreshToken!, - }); - storeToken(); - return true; + try { + TokenResponse? resp = await appAuth.token( + TokenRequest( + clientId, + redirectURLScheme, + serviceConfiguration: authorizationServiceConfiguration, + scopes: scopes, + refreshToken: token[refreshTokenKey] as String, + allowInsecureConnections: kDebugMode, + ), + ); + state = AsyncValue.data({ + tokenKey: resp.accessToken!, + refreshTokenKey: resp.refreshToken!, + }); + storeToken(); + return true; + } catch (e) { + state = AsyncValue.error(e, StackTrace.empty); + return false; + } } - state = const AsyncValue.error(e, StackTrace.empty); + state = const AsyncValue.error( + "No refresh token available", + StackTrace.empty, + ); return false; }, error: (error, stackTrace) { diff --git a/lib/booking/router.dart b/lib/booking/router.dart index 27abc4f2ef..7821bb328b 100644 --- a/lib/booking/router.dart +++ b/lib/booking/router.dart @@ -1,6 +1,5 @@ -import 'package:either_dart/either.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:heroicons/heroicons.dart'; import 'package:titan/booking/providers/is_admin_provider.dart'; import 'package:titan/booking/providers/is_manager_provider.dart'; import 'package:titan/booking/ui/pages/admin_pages/add_edit_manager_page.dart' @@ -17,7 +16,8 @@ import 'package:titan/booking/ui/pages/manager_page/manager_page.dart' deferred as manager_page; import 'package:titan/booking/ui/pages/admin_pages/add_edit_room_page.dart' deferred as add_edit_room_page; -import 'package:titan/drawer/class/module.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/class/module.dart'; import 'package:titan/tools/middlewares/admin_middleware.dart'; import 'package:titan/tools/middlewares/authenticated_middleware.dart'; import 'package:titan/tools/middlewares/deferred_middleware.dart'; @@ -32,10 +32,10 @@ class BookingRouter { static const String detail = '/detail'; static const String room = '/room'; static final Module module = Module( - name: "Réservation", - icon: const Left(HeroIcons.tableCells), + getName: (context) => AppLocalizations.of(context)!.moduleBooking, + getDescription: (context) => + AppLocalizations.of(context)!.moduleBookingDescription, root: BookingRouter.root, - selected: false, ); BookingRouter(this.ref); @@ -47,6 +47,10 @@ class BookingRouter { AuthenticatedMiddleware(ref), DeferredLoadingMiddleware(main_page.loadLibrary), ], + pageType: QCustomPage( + transitionsBuilder: (_, animation, _, child) => + FadeTransition(opacity: animation, child: child), + ), children: [ QRoute( path: admin, diff --git a/lib/booking/tools/constants.dart b/lib/booking/tools/constants.dart index c45327ae84..d34e7df9f4 100644 --- a/lib/booking/tools/constants.dart +++ b/lib/booking/tools/constants.dart @@ -1,113 +1,11 @@ import 'package:syncfusion_flutter_calendar/calendar.dart'; -class BookingTextConstants { - static const String add = "Ajouter"; - static const String addBookingPage = "Demande"; - static const String addRoom = "Ajouter une salle"; - static const String addBooking = "Ajouter une réservation"; - static const String addedBooking = "Demande ajoutée"; - static const String addedRoom = "Salle ajoutée"; - static const String addedManager = "Gestionnaire ajouté"; - static const String addingError = "Erreur lors de l'ajout"; - static const String addManager = "Ajouter un gestionnaire"; - static const String adminPage = "Administrateur"; - static const String allDay = "Toute la journée"; - static const String bookedfor = "Réservé pour"; - static const String booking = "Réservation"; - static const String bookingCreated = "Réservation créée"; - static const String bookingDemand = "Demande de réservation"; - static const String bookingNote = "Note de la réservation"; - static const String bookingPage = "Réservation"; - static const String bookingReason = "Motif de la réservation"; - static const String by = "par"; - static const String confirm = "Confirmer"; - static const String confirmation = "Confirmation"; - static const String confirmBooking = "Confirmer la réservation ?"; - static const String confirmed = "Validée"; - static const String dates = "Dates"; - static const String decline = "Refuser"; - static const String declineBooking = "Refuser la réservation ?"; - static const String declined = "Refusée"; - static const String delete = "Supprimer"; - static const String deleting = "Suppression"; - static const String deleteBooking = "Suppression"; - static const String deleteBookingConfirmation = - "Êtes-vous sûr de vouloir supprimer cette réservation ?"; - static const String deletedBooking = "Réservation supprimée"; - static const String deletedRoom = "Salle supprimée"; - static const String deletedManager = "Gestionnaire supprimé"; - static const String deleteRoomConfirmation = - "Êtes-vous sûr de vouloir supprimer cette salle ?\n\nLa salle ne doit avoir aucune réservation en cours ou à venir pour être supprimée"; - static const String deleteManagerConfirmation = - "Êtes-vous sûr de vouloir supprimer ce gestionnaire ?\n\nLe gestionnaire ne doit être associé à aucune salle pour pouvoir être supprimé"; - static const String deletingBooking = "Supprimer la réservation ?"; - static const String deletingError = "Erreur lors de la suppression"; - static const String deletingRoom = "Supprimer la salle ?"; - static const String edit = "Modifier"; - static const String editBooking = "Modifier une réservation"; - static const String editionError = "Erreur lors de la modification"; - static const String editedBooking = "Réservation modifiée"; - static const String editedRoom = "Salle modifiée"; - static const String editedManager = "Gestionnaire modifié"; - static const String editManager = "Modifier ou supprimer un gestionnaire"; - static const String editRoom = "Modifier ou supprimer une salle"; - static const String endDate = "Date de fin"; - static const String endHour = "Heure de fin"; - static const String entity = "Pour qui ?"; - static const String error = "Erreur"; - static const String eventEvery = "Tous les"; - static const String historyPage = "Historique"; - static const String incorrectOrMissingFields = - "Champs incorrects ou manquants"; - static const String interval = "Intervalle"; - static const String invalidIntervalError = "Intervalle invalide"; - static const String invalidDates = "Dates invalides"; - static const String invalidRoom = "Salle invalide"; - static const String keysRequested = "Clés demandées"; - static const String management = "Gestion"; - static const String manager = "Gestionnaire"; - static const String managerName = "Nom du gestionnaire"; - static const String multipleDay = "Plusieurs jours"; - static const String myBookings = "Mes réservations"; - static const String necessaryKey = "Clé nécessaire"; - static const String next = "Suivant"; - static const String no = "Non"; - static const String noCurrentBooking = "Pas de réservation en cours"; - static const String noDateError = "Veuillez choisir une date"; - static const String noAppointmentInReccurence = - "Aucun créneau existe avec ces paramètres de récurrence"; - static const String noDaySelected = "Aucun jour sélectionné"; - static const String noDescriptionError = "Veuillez entrer une description"; - static const String noKeys = "Aucune clé"; - static const String noNoteError = "Veuillez entrer une note"; - static const String noPhoneRegistered = "Numéro non renseigné"; - static const String noReasonError = "Veuillez entrer un motif"; - static const String noRoomFoundError = "Aucune salle enregistrée"; - static const String noRoomFound = "Aucune salle trouvée"; - static const String note = "Note"; - static const String other = "Autre"; - static const String pending = "En attente"; - static const String previous = "Précédent"; - static const String reason = "Motif"; - static const String recurrence = "Récurrence"; - static const String recurrenceDays = "Jours de récurrence"; - static const String recurrenceEndDate = "Date de fin de récurrence"; - static const String recurrent = "Récurrent"; - static const String registeredRooms = "Salles enregistrées"; - static const String room = "Salle"; - static const String roomName = "Nom de la salle"; - static const String startDate = "Date de début"; - static const String startHour = "Heure de début"; - static const String weeks = "Semaines"; - static const String yes = "Oui"; - - static const List weekDaysOrdered = [ - WeekDays.monday, - WeekDays.tuesday, - WeekDays.wednesday, - WeekDays.thursday, - WeekDays.friday, - WeekDays.saturday, - WeekDays.sunday, - ]; -} +final weekDaysOrdered = [ + WeekDays.monday, + WeekDays.tuesday, + WeekDays.wednesday, + WeekDays.thursday, + WeekDays.friday, + WeekDays.saturday, + WeekDays.sunday, +]; diff --git a/lib/booking/tools/functions.dart b/lib/booking/tools/functions.dart index e65867d478..92caafbf6b 100644 --- a/lib/booking/tools/functions.dart +++ b/lib/booking/tools/functions.dart @@ -1,33 +1,35 @@ -import 'package:titan/booking/tools/constants.dart'; +import 'package:flutter/material.dart'; import 'package:titan/tools/functions.dart'; import 'package:syncfusion_flutter_calendar/calendar.dart'; +import 'package:titan/l10n/app_localizations.dart'; -String decisionToString(Decision d) { +String decisionToString(Decision d, BuildContext context) { switch (d) { case Decision.approved: - return BookingTextConstants.confirmed; + return AppLocalizations.of(context)!.bookingConfirmed; case Decision.declined: - return BookingTextConstants.declined; + return AppLocalizations.of(context)!.bookingDeclined; case Decision.pending: - return BookingTextConstants.pending; + return AppLocalizations.of(context)!.bookingPending; } } -String weekDayToString(WeekDays day) { +String weekDayToLocalizedString(BuildContext context, WeekDays day) { + final loc = AppLocalizations.of(context)!; switch (day) { - case WeekDays.sunday: - return "Dimanche"; case WeekDays.monday: - return "Lundi"; + return loc.bookingWeekDayMon; case WeekDays.tuesday: - return "Mardi"; + return loc.bookingWeekDayTue; case WeekDays.wednesday: - return "Mercredi"; + return loc.bookingWeekDayWed; case WeekDays.thursday: - return "Jeudi"; + return loc.bookingWeekDayThu; case WeekDays.friday: - return "Vendredi"; + return loc.bookingWeekDayFri; case WeekDays.saturday: - return "Samedi"; + return loc.bookingWeekDaySat; + case WeekDays.sunday: + return loc.bookingWeekDaySun; } } diff --git a/lib/booking/ui/booking.dart b/lib/booking/ui/booking.dart index 1371781d90..fe9a136d95 100644 --- a/lib/booking/ui/booking.dart +++ b/lib/booking/ui/booking.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:titan/booking/router.dart'; -import 'package:titan/booking/tools/constants.dart'; import 'package:titan/tools/ui/widgets/top_bar.dart'; +import 'package:titan/tools/constants.dart'; class BookingTemplate extends StatelessWidget { final Widget child; @@ -9,16 +9,18 @@ class BookingTemplate extends StatelessWidget { @override Widget build(BuildContext context) { - return SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const TopBar( - title: BookingTextConstants.booking, - root: BookingRouter.root, + return Scaffold( + body: Container( + decoration: const BoxDecoration(color: ColorConstants.background), + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const TopBar(root: BookingRouter.root), + Expanded(child: child), + ], ), - Expanded(child: child), - ], + ), ), ); } diff --git a/lib/booking/ui/calendar/calendar.dart b/lib/booking/ui/calendar/calendar.dart index 61d1559a65..ee15f535c5 100644 --- a/lib/booking/ui/calendar/calendar.dart +++ b/lib/booking/ui/calendar/calendar.dart @@ -6,7 +6,7 @@ import 'package:titan/booking/providers/confirmed_booking_list_provider.dart'; import 'package:titan/booking/providers/manager_confirmed_booking_list_provider.dart'; import 'package:titan/booking/ui/calendar/appointment_data_source.dart'; import 'package:titan/booking/ui/calendar/calendar_dialog.dart'; -import 'package:titan/drawer/providers/is_web_format_provider.dart'; +import 'package:titan/navigation/providers/is_web_format_provider.dart'; import 'package:titan/tools/constants.dart'; import 'package:syncfusion_flutter_calendar/calendar.dart'; diff --git a/lib/booking/ui/calendar/calendar_dialog.dart b/lib/booking/ui/calendar/calendar_dialog.dart index 9a8b0027f3..819e8576f1 100644 --- a/lib/booking/ui/calendar/calendar_dialog.dart +++ b/lib/booking/ui/calendar/calendar_dialog.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:titan/booking/class/booking.dart'; -import 'package:titan/booking/tools/constants.dart'; import 'package:titan/booking/ui/calendar/calendar_dialog_button.dart'; import 'package:titan/tools/functions.dart'; +import 'package:titan/l10n/app_localizations.dart'; class CalendarDialog extends StatelessWidget { final Booking booking; @@ -17,6 +17,7 @@ class CalendarDialog extends StatelessWidget { @override Widget build(BuildContext context) { + final locale = Localizations.localeOf(context); return Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)), child: Stack( @@ -41,6 +42,7 @@ class CalendarDialog extends StatelessWidget { booking.end, booking.recurrenceRule, false, + locale.toString(), ), style: TextStyle( fontWeight: FontWeight.w400, @@ -50,7 +52,7 @@ class CalendarDialog extends StatelessWidget { ), const SizedBox(height: 10), Text( - "${BookingTextConstants.bookedfor} ${booking.entity} ${BookingTextConstants.by} ${booking.applicant.getName()}", + "${AppLocalizations.of(context)!.bookingBookedFor} ${booking.entity} ${AppLocalizations.of(context)!.bookingBy} ${booking.applicant.getName()}", style: const TextStyle( fontWeight: FontWeight.w400, fontSize: 15, @@ -103,7 +105,9 @@ class CalendarDialog extends StatelessWidget { Flexible( child: Text( booking.applicant.phone ?? - BookingTextConstants.noPhoneRegistered, + AppLocalizations.of( + context, + )!.bookingNoPhoneRegistered, style: const TextStyle( fontWeight: FontWeight.w400, fontSize: 15, diff --git a/lib/booking/ui/components/booking_card.dart b/lib/booking/ui/components/booking_card.dart index fb3df46a66..3c6d094d74 100644 --- a/lib/booking/ui/components/booking_card.dart +++ b/lib/booking/ui/components/booking_card.dart @@ -2,12 +2,12 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:titan/booking/class/booking.dart'; -import 'package:titan/booking/tools/constants.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; import 'package:titan/tools/ui/layouts/card_button.dart'; import 'package:titan/tools/ui/layouts/card_layout.dart'; import 'package:syncfusion_flutter_calendar/calendar.dart'; +import 'package:titan/l10n/app_localizations.dart'; class BookingCard extends StatelessWidget { final Booking booking; @@ -29,6 +29,7 @@ class BookingCard extends StatelessWidget { @override Widget build(BuildContext context) { + final locale = Localizations.localeOf(context); final isNotEnded = booking.recurrenceRule.isNotEmpty ? SfCalendar.parseRRule( booking.recurrenceRule, @@ -126,6 +127,7 @@ class BookingCard extends StatelessWidget { booking.end, booking.recurrenceRule, false, + locale.toString(), ), style: TextStyle( fontSize: 13, @@ -153,10 +155,10 @@ class BookingCard extends StatelessWidget { children: [ Text( booking.decision == Decision.pending - ? BookingTextConstants.pending + ? AppLocalizations.of(context)!.bookingPending : booking.decision == Decision.approved - ? BookingTextConstants.confirmed - : BookingTextConstants.declined, + ? AppLocalizations.of(context)!.bookingConfirmed + : AppLocalizations.of(context)!.bookingDeclined, style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, @@ -164,7 +166,7 @@ class BookingCard extends StatelessWidget { ), ), Text( - '${BookingTextConstants.keysRequested}: ${booking.key ? BookingTextConstants.yes : BookingTextConstants.no}', + '${AppLocalizations.of(context)!.bookingKeysRequested}: ${booking.key ? AppLocalizations.of(context)!.bookingYes : AppLocalizations.of(context)!.bookingNo}', style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, diff --git a/lib/booking/ui/pages/admin_pages/add_edit_manager_page.dart b/lib/booking/ui/pages/admin_pages/add_edit_manager_page.dart index e89d0c1405..c02ac990b2 100644 --- a/lib/booking/ui/pages/admin_pages/add_edit_manager_page.dart +++ b/lib/booking/ui/pages/admin_pages/add_edit_manager_page.dart @@ -6,7 +6,6 @@ import 'package:titan/admin/providers/group_id_provider.dart'; import 'package:titan/booking/class/manager.dart'; import 'package:titan/booking/providers/manager_list_provider.dart'; import 'package:titan/booking/providers/manager_provider.dart'; -import 'package:titan/booking/tools/constants.dart'; import 'package:titan/booking/ui/booking.dart'; import 'package:titan/booking/ui/pages/admin_pages/admin_entry.dart'; import 'package:titan/booking/ui/pages/admin_pages/admin_scroll_chips.dart'; @@ -17,6 +16,7 @@ import 'package:titan/tools/ui/layouts/item_chip.dart'; import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; import 'package:qlevar_router/qlevar_router.dart'; import 'package:titan/admin/providers/group_list_provider.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AddEditManagerPage extends HookConsumerWidget { final GlobalKey dataKey = GlobalKey(); @@ -46,8 +46,8 @@ class AddEditManagerPage extends HookConsumerWidget { alignment: Alignment.centerLeft, child: Text( isEdit - ? BookingTextConstants.editManager - : BookingTextConstants.addManager, + ? AppLocalizations.of(context)!.bookingEditManager + : AppLocalizations.of(context)!.bookingAddManager, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -61,7 +61,7 @@ class AddEditManagerPage extends HookConsumerWidget { children: [ const SizedBox(height: 50), AdminEntry( - name: BookingTextConstants.managerName, + name: AppLocalizations.of(context)!.bookingManagerName, nameController: name, ), const SizedBox(height: 50), @@ -105,6 +105,12 @@ class AddEditManagerPage extends HookConsumerWidget { name: name.text, groupId: groupId, ); + final editedManagerMsg = isEdit + ? AppLocalizations.of(context)!.bookingEditedManager + : AppLocalizations.of(context)!.bookingAddedManager; + final editedManagerErrorMsg = isEdit + ? AppLocalizations.of(context)!.bookingEditionError + : AppLocalizations.of(context)!.bookingAddingError; final value = isEdit ? await managerListNotifier.updateManager( newManager, @@ -112,31 +118,21 @@ class AddEditManagerPage extends HookConsumerWidget { : await managerListNotifier.addManager(newManager); if (value) { QR.back(); - isEdit - ? displayToastWithContext( - TypeMsg.msg, - BookingTextConstants.editedManager, - ) - : displayToastWithContext( - TypeMsg.msg, - BookingTextConstants.addedManager, - ); + displayToastWithContext( + TypeMsg.msg, + editedManagerMsg, + ); } else { - isEdit - ? displayToastWithContext( - TypeMsg.error, - BookingTextConstants.editionError, - ) - : displayToastWithContext( - TypeMsg.error, - BookingTextConstants.addingError, - ); + displayToastWithContext( + TypeMsg.error, + editedManagerErrorMsg, + ); } }); }, buttonText: isEdit - ? BookingTextConstants.edit - : BookingTextConstants.add, + ? AppLocalizations.of(context)!.bookingEdit + : AppLocalizations.of(context)!.bookingAdd, ), if (isEdit) ...[ const SizedBox(height: 30), @@ -146,30 +142,39 @@ class AddEditManagerPage extends HookConsumerWidget { await showDialog( context: context, builder: (context) => CustomDialogBox( - descriptions: BookingTextConstants - .deleteManagerConfirmation, + descriptions: AppLocalizations.of( + context, + )!.bookingDeleteManagerConfirmation, onYes: () async { + final deletedManagerMsg = AppLocalizations.of( + context, + )!.bookingDeletedManager; + final deletingErrorMsg = AppLocalizations.of( + context, + )!.bookingDeletingError; final value = await managerListNotifier .deleteManager(manager); if (value) { QR.back(); displayToastWithContext( TypeMsg.msg, - BookingTextConstants.deletedManager, + deletedManagerMsg, ); } else { displayToastWithContext( TypeMsg.error, - BookingTextConstants.deletingError, + deletingErrorMsg, ); } }, - title: BookingTextConstants.deleting, + title: AppLocalizations.of( + context, + )!.bookingDeleting, ), ); }); }, - buttonText: BookingTextConstants.delete, + buttonText: AppLocalizations.of(context)!.bookingDelete, ), ], const SizedBox(height: 30), diff --git a/lib/booking/ui/pages/admin_pages/add_edit_room_page.dart b/lib/booking/ui/pages/admin_pages/add_edit_room_page.dart index 009859b011..17a2da96f0 100644 --- a/lib/booking/ui/pages/admin_pages/add_edit_room_page.dart +++ b/lib/booking/ui/pages/admin_pages/add_edit_room_page.dart @@ -7,7 +7,6 @@ import 'package:titan/booking/providers/manager_list_provider.dart'; import 'package:titan/booking/providers/manager_id_provider.dart'; import 'package:titan/service/providers/room_list_provider.dart'; import 'package:titan/booking/providers/room_provider.dart'; -import 'package:titan/booking/tools/constants.dart'; import 'package:titan/booking/ui/booking.dart'; import 'package:titan/booking/ui/pages/admin_pages/admin_entry.dart'; import 'package:titan/booking/ui/pages/admin_pages/admin_scroll_chips.dart'; @@ -17,6 +16,7 @@ import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/tools/ui/layouts/item_chip.dart'; import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AddEditRoomPage extends HookConsumerWidget { final dataKey = GlobalKey(); @@ -46,8 +46,8 @@ class AddEditRoomPage extends HookConsumerWidget { alignment: Alignment.centerLeft, child: Text( isEdit - ? BookingTextConstants.editRoom - : BookingTextConstants.addRoom, + ? AppLocalizations.of(context)!.bookingEditRoom + : AppLocalizations.of(context)!.bookingAddRoom, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -61,7 +61,7 @@ class AddEditRoomPage extends HookConsumerWidget { children: [ const SizedBox(height: 50), AdminEntry( - name: BookingTextConstants.roomName, + name: AppLocalizations.of(context)!.bookingRoomName, nameController: name, ), const SizedBox(height: 50), @@ -105,36 +105,29 @@ class AddEditRoomPage extends HookConsumerWidget { name: name.text, managerId: managerId, ); + final editedRoomMsg = isEdit + ? AppLocalizations.of(context)!.bookingEditedRoom + : AppLocalizations.of(context)!.bookingAddedRoom; + final addingErrorMsg = isEdit + ? AppLocalizations.of(context)!.bookingEditionError + : AppLocalizations.of(context)!.bookingAddingError; final value = isEdit ? await roomListNotifier.updateRoom(newRoom) : await roomListNotifier.addRoom(newRoom); if (value) { QR.back(); - isEdit - ? displayToastWithContext( - TypeMsg.msg, - BookingTextConstants.editedRoom, - ) - : displayToastWithContext( - TypeMsg.msg, - BookingTextConstants.addedRoom, - ); + displayToastWithContext(TypeMsg.msg, editedRoomMsg); } else { - isEdit - ? displayToastWithContext( - TypeMsg.error, - BookingTextConstants.editionError, - ) - : displayToastWithContext( - TypeMsg.error, - BookingTextConstants.addingError, - ); + displayToastWithContext( + TypeMsg.error, + addingErrorMsg, + ); } }); }, buttonText: isEdit - ? BookingTextConstants.edit - : BookingTextConstants.add, + ? AppLocalizations.of(context)!.bookingEdit + : AppLocalizations.of(context)!.bookingAdd, ), if (isEdit) ...[ const SizedBox(height: 30), @@ -144,9 +137,16 @@ class AddEditRoomPage extends HookConsumerWidget { await showDialog( context: context, builder: (context) => CustomDialogBox( - descriptions: - BookingTextConstants.deleteRoomConfirmation, + descriptions: AppLocalizations.of( + context, + )!.bookingDeleteRoomConfirmation, onYes: () async { + final deletedRoomMsg = AppLocalizations.of( + context, + )!.bookingDeletedRoom; + final deletingErrorMsg = AppLocalizations.of( + context, + )!.bookingDeletingError; final value = await roomListNotifier.deleteRoom( room, ); @@ -154,21 +154,23 @@ class AddEditRoomPage extends HookConsumerWidget { QR.back(); displayToastWithContext( TypeMsg.msg, - BookingTextConstants.deletedRoom, + deletedRoomMsg, ); } else { displayToastWithContext( TypeMsg.error, - BookingTextConstants.deletingError, + deletingErrorMsg, ); } }, - title: BookingTextConstants.deleteBooking, + title: AppLocalizations.of( + context, + )!.bookingDeleteBooking, ), ); }); }, - buttonText: BookingTextConstants.delete, + buttonText: AppLocalizations.of(context)!.bookingDelete, ), ], const SizedBox(height: 30), diff --git a/lib/booking/ui/pages/admin_pages/admin_page.dart b/lib/booking/ui/pages/admin_pages/admin_page.dart index ef481dac5e..3cba6f3e47 100644 --- a/lib/booking/ui/pages/admin_pages/admin_page.dart +++ b/lib/booking/ui/pages/admin_pages/admin_page.dart @@ -13,13 +13,13 @@ import 'package:titan/booking/providers/manager_provider.dart'; import 'package:titan/service/providers/room_list_provider.dart'; import 'package:titan/booking/providers/room_provider.dart'; import 'package:titan/booking/router.dart'; -import 'package:titan/booking/tools/constants.dart'; import 'package:titan/booking/ui/booking.dart'; import 'package:titan/booking/ui/calendar/calendar.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/ui/layouts/item_chip.dart'; import 'package:titan/tools/ui/layouts/refresher.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AdminPage extends HookConsumerWidget { const AdminPage({super.key}); @@ -37,6 +37,7 @@ class AdminPage extends HookConsumerWidget { return BookingTemplate( child: LayoutBuilder( builder: (context, constraints) => Refresher( + controller: ScrollController(), onRefresh: () async { await ref.watch(roomListProvider.notifier).loadRooms(); await ref @@ -53,13 +54,13 @@ class AdminPage extends HookConsumerWidget { const SizedBox(height: 20), const Expanded(child: Calendar(isManagerPage: false)), const SizedBox(height: 30), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 30.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 30.0), child: Align( alignment: Alignment.centerLeft, child: Text( - BookingTextConstants.room, - style: TextStyle( + AppLocalizations.of(context)!.bookingRoom, + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Color.fromARGB(255, 149, 149, 149), @@ -123,13 +124,13 @@ class AdminPage extends HookConsumerWidget { }, ), const SizedBox(height: 20), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 30.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 30.0), child: Align( alignment: Alignment.centerLeft, child: Text( - BookingTextConstants.manager, - style: TextStyle( + AppLocalizations.of(context)!.bookingManager, + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Color.fromARGB(255, 149, 149, 149), diff --git a/lib/booking/ui/pages/booking_pages/add_edit_booking_page.dart b/lib/booking/ui/pages/booking_pages/add_edit_booking_page.dart index ff76e882a5..b287e3b79f 100644 --- a/lib/booking/ui/pages/booking_pages/add_edit_booking_page.dart +++ b/lib/booking/ui/pages/booking_pages/add_edit_booking_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:titan/booking/class/booking.dart'; import 'package:titan/service/class/room.dart'; import 'package:titan/booking/providers/booking_provider.dart'; @@ -29,6 +30,7 @@ import 'package:titan/tools/ui/widgets/text_entry.dart'; import 'package:titan/user/providers/user_provider.dart'; import 'package:qlevar_router/qlevar_router.dart'; import 'package:syncfusion_flutter_calendar/calendar.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AddEditBookingPage extends HookConsumerWidget { final dataKey = GlobalKey(); @@ -38,6 +40,7 @@ class AddEditBookingPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final locale = Localizations.localeOf(context); final now = DateTime.now(); final user = ref.watch(userProvider); final key = GlobalKey(); @@ -60,14 +63,14 @@ class AddEditBookingPage extends HookConsumerWidget { text: isEdit ? recurrent.value ? processDateOnlyHour(booking.start) - : processDateWithHour(booking.start) + : DateFormat.yMd(locale).add_Hm().format(booking.start) : "", ); final end = useTextEditingController( text: isEdit ? recurrent.value ? processDateOnlyHour(booking.end) - : processDateWithHour(booking.end) + : DateFormat.yMd(locale).add_Hm().format(booking.end) : "", ); final motif = useTextEditingController(text: booking.reason); @@ -83,7 +86,7 @@ class AddEditBookingPage extends HookConsumerWidget { ); final recurrenceEndDate = useTextEditingController( text: booking.recurrenceRule != "" - ? processDate( + ? DateFormat.yMd(locale).format( DateTime.parse( booking.recurrenceRule.split(";UNTIL=")[1].split(";")[0], ), @@ -106,8 +109,8 @@ class AddEditBookingPage extends HookConsumerWidget { padding: const EdgeInsets.symmetric(horizontal: 30), child: AlignLeftText( isEdit - ? BookingTextConstants.editBooking - : BookingTextConstants.addBooking, + ? AppLocalizations.of(context)!.bookingEditBooking + : AppLocalizations.of(context)!.bookingAddBooking, color: Colors.grey, ), ), @@ -153,27 +156,27 @@ class AddEditBookingPage extends HookConsumerWidget { children: [ TextEntry( controller: entity, - label: BookingTextConstants.entity, + label: AppLocalizations.of(context)!.bookingEntity, ), const SizedBox(height: 30), TextEntry( controller: motif, - label: BookingTextConstants.reason, + label: AppLocalizations.of(context)!.bookingReason, ), const SizedBox(height: 30), TextEntry( - label: BookingTextConstants.note, + label: AppLocalizations.of(context)!.bookingNote, controller: note, canBeEmpty: true, ), const SizedBox(height: 20), CheckBoxEntry( - title: BookingTextConstants.necessaryKey, + title: AppLocalizations.of(context)!.bookingNecessaryKey, valueNotifier: keyRequired, ), const SizedBox(height: 20), CheckBoxEntry( - title: BookingTextConstants.recurrence, + title: AppLocalizations.of(context)!.bookingRecurrence, valueNotifier: recurrent, onChanged: () { start.text = ""; @@ -183,7 +186,7 @@ class AddEditBookingPage extends HookConsumerWidget { ), const SizedBox(height: 20), CheckBoxEntry( - title: BookingTextConstants.allDay, + title: AppLocalizations.of(context)!.bookingAllDay, valueNotifier: allDay, onChanged: () { start.text = ""; @@ -195,13 +198,15 @@ class AddEditBookingPage extends HookConsumerWidget { recurrent.value ? Column( children: [ - const Text( - BookingTextConstants.recurrenceDays, - style: TextStyle(color: Colors.black), + Text( + AppLocalizations.of( + context, + )!.bookingRecurrenceDays, + style: const TextStyle(color: Colors.black), ), const SizedBox(height: 10), Column( - children: BookingTextConstants.weekDaysOrdered + children: weekDaysOrdered .map( (e) => GestureDetector( onTap: () { @@ -213,7 +218,10 @@ class AddEditBookingPage extends HookConsumerWidget { MainAxisAlignment.spaceBetween, children: [ Text( - weekDayToString(e), + weekDayToLocalizedString( + context, + e, + ), style: TextStyle( color: Colors.grey.shade700, fontSize: 16, @@ -234,15 +242,21 @@ class AddEditBookingPage extends HookConsumerWidget { .toList(), ), const SizedBox(height: 20), - const Text( - BookingTextConstants.interval, + Text( + AppLocalizations.of(context)!.bookingInterval, style: TextStyle(color: Colors.black), ), const SizedBox(height: 10), TextEntry( - label: BookingTextConstants.interval, - prefix: BookingTextConstants.eventEvery, - suffix: BookingTextConstants.weeks, + label: AppLocalizations.of( + context, + )!.bookingInterval, + prefix: AppLocalizations.of( + context, + )!.bookingEventEvery, + suffix: AppLocalizations.of( + context, + )!.bookingWeeks, controller: interval, isInt: true, ), @@ -254,14 +268,18 @@ class AddEditBookingPage extends HookConsumerWidget { onTap: () => getOnlyHourDate(context, start), controller: start, - label: BookingTextConstants.startHour, + label: AppLocalizations.of( + context, + )!.bookingStartHour, ), const SizedBox(height: 30), DateEntry( onTap: () => getOnlyHourDate(context, end), controller: end, - label: BookingTextConstants.endHour, + label: AppLocalizations.of( + context, + )!.bookingEndHour, ), const SizedBox(height: 30), ], @@ -270,7 +288,9 @@ class AddEditBookingPage extends HookConsumerWidget { onTap: () => getOnlyDayDate(context, recurrenceEndDate), controller: recurrenceEndDate, - label: BookingTextConstants.recurrenceEndDate, + label: AppLocalizations.of( + context, + )!.bookingRecurrenceEndDate, ), ], ) @@ -281,7 +301,9 @@ class AddEditBookingPage extends HookConsumerWidget { ? getOnlyDayDate(context, start) : getFullDate(context, start), controller: start, - label: BookingTextConstants.startDate, + label: AppLocalizations.of( + context, + )!.bookingStartDate, ), const SizedBox(height: 30), DateEntry( @@ -289,7 +311,9 @@ class AddEditBookingPage extends HookConsumerWidget { ? getOnlyDayDate(context, end) : getFullDate(context, end), controller: end, - label: BookingTextConstants.endDate, + label: AppLocalizations.of( + context, + )!.bookingEndDate, ), ], ), @@ -300,6 +324,18 @@ class AddEditBookingPage extends HookConsumerWidget { if (key.currentState == null) { return; } + final editedBookingMsg = AppLocalizations.of( + context, + )!.bookingEditedBooking; + final addedBookingMsg = AppLocalizations.of( + context, + )!.bookingAddedBooking; + final editionErrorMsg = AppLocalizations.of( + context, + )!.bookingEditionError; + final addingErrorMsg = AppLocalizations.of( + context, + )!.bookingAddingError; if (key.currentState!.validate()) { if (allDay.value) { start.text = "${start.text} 00:00"; @@ -307,35 +343,39 @@ class AddEditBookingPage extends HookConsumerWidget { } if (end.text.contains("/") && isDateBefore( - processDateBack(end.text), - processDateBack(start.text), + processDateBack(end.text, locale.toString()), + processDateBack(start.text, locale.toString()), )) { displayToast( context, TypeMsg.error, - BookingTextConstants.invalidDates, + AppLocalizations.of(context)!.bookingInvalidDates, ); } else if (room.value.id.isEmpty) { displayToast( context, TypeMsg.error, - BookingTextConstants.invalidRoom, + AppLocalizations.of(context)!.bookingInvalidRoom, ); } else if (recurrent.value && selectedDays.isEmpty) { displayToast( context, TypeMsg.error, - BookingTextConstants.noDaySelected, + AppLocalizations.of( + context, + )!.bookingNoDaySelected, ); } else { String recurrenceRule = ""; String startString = start.text; if (!startString.contains("/")) { - startString = "${processDate(now)} $startString"; + startString = + "${DateFormat.yMd(locale).format(now)} $startString"; } String endString = end.text; if (!endString.contains("/")) { - endString = "${processDate(now)} $endString"; + endString = + "${DateFormat.yMd(locale).format(now)} $endString"; } if (recurrent.value) { RecurrenceProperties recurrence = @@ -344,17 +384,26 @@ class AddEditBookingPage extends HookConsumerWidget { recurrence.recurrenceRange = RecurrenceRange.endDate; recurrence.endDate = DateTime.parse( - processDateBack(recurrenceEndDate.text), + processDateBack( + recurrenceEndDate.text, + locale.toString(), + ), ); recurrence.weekDays = selectedDays; recurrence.interval = int.parse(interval.text); recurrenceRule = SfCalendar.generateRRule( recurrence, DateTime.parse( - processDateBackWithHour(startString), + processDateBackWithHour( + startString, + locale.toString(), + ), ), DateTime.parse( - processDateBackWithHour(endString), + processDateBackWithHour( + endString, + locale.toString(), + ), ), ); try { @@ -366,8 +415,9 @@ class AddEditBookingPage extends HookConsumerWidget { displayToast( context, TypeMsg.error, - BookingTextConstants - .noAppointmentInReccurence, + AppLocalizations.of( + context, + )!.bookingNoAppointmentInReccurence, ); return; } @@ -377,10 +427,16 @@ class AddEditBookingPage extends HookConsumerWidget { id: isEdit ? booking.id : "", reason: motif.text, start: DateTime.parse( - processDateBackWithHour(startString), + processDateBackWithHour( + startString, + locale.toString(), + ), ), end: DateTime.parse( - processDateBackWithHour(endString), + processDateBackWithHour( + endString, + locale.toString(), + ), ), creation: DateTime.now(), note: note.text.isEmpty ? null : note.text, @@ -429,24 +485,24 @@ class AddEditBookingPage extends HookConsumerWidget { if (isEdit) { displayToastWithContext( TypeMsg.msg, - BookingTextConstants.editedBooking, + editedBookingMsg, ); } else { displayToastWithContext( TypeMsg.msg, - BookingTextConstants.addedBooking, + addedBookingMsg, ); } } else { if (isEdit) { displayToastWithContext( TypeMsg.error, - BookingTextConstants.editionError, + editionErrorMsg, ); } else { displayToastWithContext( TypeMsg.error, - BookingTextConstants.addingError, + addingErrorMsg, ); } } @@ -456,14 +512,16 @@ class AddEditBookingPage extends HookConsumerWidget { displayToast( context, TypeMsg.error, - BookingTextConstants.incorrectOrMissingFields, + AppLocalizations.of( + context, + )!.bookingIncorrectOrMissingFields, ); } }, child: Text( isEdit - ? BookingTextConstants.edit - : BookingTextConstants.add, + ? AppLocalizations.of(context)!.bookingEdit + : AppLocalizations.of(context)!.bookingAdd, style: const TextStyle( color: Colors.white, fontSize: 25, diff --git a/lib/booking/ui/pages/detail_pages/detail_booking.dart b/lib/booking/ui/pages/detail_pages/detail_booking.dart index b8715de07a..094dcf7fcb 100644 --- a/lib/booking/ui/pages/detail_pages/detail_booking.dart +++ b/lib/booking/ui/pages/detail_pages/detail_booking.dart @@ -3,13 +3,13 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/booking/providers/booking_provider.dart'; -import 'package:titan/booking/tools/constants.dart'; import 'package:titan/booking/tools/functions.dart'; import 'package:titan/booking/ui/booking.dart'; import 'package:titan/booking/ui/components/booking_card.dart'; import 'package:titan/booking/ui/pages/detail_pages/contact_button.dart'; import 'package:titan/tools/functions.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:titan/l10n/app_localizations.dart'; class DetailBookingPage extends HookConsumerWidget { final bool isAdmin; @@ -56,7 +56,7 @@ class DetailBookingPage extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - decisionToString(booking.decision), + decisionToString(booking.decision, context), style: const TextStyle( fontSize: 25, fontWeight: FontWeight.bold, @@ -115,7 +115,7 @@ class DetailBookingPage extends HookConsumerWidget { Column( children: [ AutoSizeText( - "${BookingTextConstants.bookedfor} ${booking.entity}", + "${AppLocalizations.of(context)!.bookingBookedFor} ${booking.entity}", style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -126,7 +126,9 @@ class DetailBookingPage extends HookConsumerWidget { const SizedBox(height: 50), Text( booking.applicant.phone ?? - BookingTextConstants.noPhoneRegistered, + AppLocalizations.of( + context, + )!.bookingNoPhoneRegistered, style: const TextStyle(fontSize: 25), ), const SizedBox(height: 50), diff --git a/lib/booking/ui/pages/main_page/main_page.dart b/lib/booking/ui/pages/main_page/main_page.dart index 3282ce988d..98ae22f341 100644 --- a/lib/booking/ui/pages/main_page/main_page.dart +++ b/lib/booking/ui/pages/main_page/main_page.dart @@ -12,7 +12,6 @@ import 'package:titan/booking/providers/manager_booking_list_provider.dart'; import 'package:titan/booking/providers/selected_days_provider.dart'; import 'package:titan/booking/providers/user_booking_list_provider.dart'; import 'package:titan/booking/router.dart'; -import 'package:titan/booking/tools/constants.dart'; import 'package:titan/booking/ui/booking.dart'; import 'package:titan/booking/ui/calendar/calendar.dart'; import 'package:titan/booking/ui/components/booking_card.dart'; @@ -26,6 +25,7 @@ import 'package:titan/tools/ui/layouts/refresher.dart'; import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; import 'package:qlevar_router/qlevar_router.dart'; import 'package:syncfusion_flutter_calendar/calendar.dart'; +import 'package:titan/l10n/app_localizations.dart'; class BookingMainPage extends HookConsumerWidget { const BookingMainPage({super.key}); @@ -61,6 +61,7 @@ class BookingMainPage extends HookConsumerWidget { return BookingTemplate( child: LayoutBuilder( builder: (context, constraints) => Refresher( + controller: ScrollController(), onRefresh: () async { await confirmedBookingsNotifier.loadConfirmedBooking(); await bookingsNotifier.loadUserBookings(); @@ -80,7 +81,7 @@ class BookingMainPage extends HookConsumerWidget { children: [ if (isManager) AdminButton( - text: BookingTextConstants.management, + text: AppLocalizations.of(context)!.bookingManagement, onTap: () { QR.to(BookingRouter.root + BookingRouter.manager); }, @@ -97,13 +98,13 @@ class BookingMainPage extends HookConsumerWidget { const SizedBox(height: 10), const Expanded(child: Calendar(isManagerPage: false)), const SizedBox(height: 30), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 30.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 30.0), child: Align( alignment: Alignment.centerLeft, child: Text( - BookingTextConstants.myBookings, - style: TextStyle( + AppLocalizations.of(context)!.bookingMyBookings, + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Color.fromARGB(255, 149, 149, 149), @@ -151,11 +152,19 @@ class BookingMainPage extends HookConsumerWidget { await showDialog( context: context, builder: (context) => CustomDialogBox( - descriptions: BookingTextConstants - .deleteBookingConfirmation, + descriptions: AppLocalizations.of( + context, + )!.bookingDeleteBookingConfirmation, onYes: () async { + final deleteMsg = AppLocalizations.of( + context, + )!.bookingDeleteBooking; + final errorMsg = AppLocalizations.of( + context, + )!.bookingDeletingError; final value = await bookingsNotifier .deleteBooking(e); + if (value) { ref .read( @@ -164,16 +173,18 @@ class BookingMainPage extends HookConsumerWidget { .loadUserManageBookings; displayToastWithContext( TypeMsg.msg, - BookingTextConstants.deleteBooking, + deleteMsg, ); } else { displayToastWithContext( TypeMsg.error, - BookingTextConstants.deletingError, + errorMsg, ); } }, - title: BookingTextConstants.deleteBooking, + title: AppLocalizations.of( + context, + )!.bookingDeleteBooking, ), ); }); diff --git a/lib/booking/ui/pages/manager_page/list_booking.dart b/lib/booking/ui/pages/manager_page/list_booking.dart index 559337eef2..c5a928de3f 100644 --- a/lib/booking/ui/pages/manager_page/list_booking.dart +++ b/lib/booking/ui/pages/manager_page/list_booking.dart @@ -10,7 +10,6 @@ import 'package:titan/booking/providers/manager_confirmed_booking_list_provider. import 'package:titan/booking/providers/user_booking_list_provider.dart'; import 'package:titan/booking/providers/selected_days_provider.dart'; import 'package:titan/booking/router.dart'; -import 'package:titan/booking/tools/constants.dart'; import 'package:titan/booking/ui/components/booking_card.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; @@ -18,6 +17,7 @@ import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; import 'package:qlevar_router/qlevar_router.dart'; import 'package:syncfusion_flutter_calendar/calendar.dart'; +import 'package:titan/l10n/app_localizations.dart'; class ListBooking extends HookConsumerWidget { final List bookings; @@ -119,8 +119,10 @@ class ListBooking extends HookConsumerWidget { context: context, builder: (context) { return CustomDialogBox( - title: BookingTextConstants.confirm, - descriptions: BookingTextConstants.confirmBooking, + title: AppLocalizations.of(context)!.bookingConfirm, + descriptions: AppLocalizations.of( + context, + )!.bookingConfirmBooking, onYes: () async { await tokenExpireWrapper(ref, () async { Booking newBooking = e.copyWith( @@ -156,8 +158,10 @@ class ListBooking extends HookConsumerWidget { context: context, builder: (context) { return CustomDialogBox( - title: BookingTextConstants.decline, - descriptions: BookingTextConstants.declineBooking, + title: AppLocalizations.of(context)!.bookingDecline, + descriptions: AppLocalizations.of( + context, + )!.bookingDeclineBooking, onYes: () async { await tokenExpireWrapper(ref, () async { Booking newBooking = e.copyWith( diff --git a/lib/booking/ui/pages/manager_page/manager_page.dart b/lib/booking/ui/pages/manager_page/manager_page.dart index b10f5ac4e0..b272f143ec 100644 --- a/lib/booking/ui/pages/manager_page/manager_page.dart +++ b/lib/booking/ui/pages/manager_page/manager_page.dart @@ -4,12 +4,12 @@ import 'package:titan/booking/class/booking.dart'; import 'package:titan/booking/providers/manager_booking_list_provider.dart'; import 'package:titan/booking/providers/manager_confirmed_booking_list_provider.dart'; import 'package:titan/service/providers/room_list_provider.dart'; -import 'package:titan/booking/tools/constants.dart'; import 'package:titan/booking/ui/booking.dart'; import 'package:titan/booking/ui/calendar/calendar.dart'; import 'package:titan/booking/ui/pages/manager_page/list_booking.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/ui/layouts/refresher.dart'; +import 'package:titan/l10n/app_localizations.dart'; class ManagerPage extends HookConsumerWidget { const ManagerPage({super.key}); @@ -43,6 +43,7 @@ class ManagerPage extends HookConsumerWidget { ); return BookingTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await ref .watch(managerBookingListProvider.notifier) @@ -63,10 +64,10 @@ class ManagerPage extends HookConsumerWidget { if (pendingBookings.isEmpty && confirmedBookings.isEmpty && canceledBookings.isEmpty) - const Center( + Center( child: Text( - BookingTextConstants.noCurrentBooking, - style: TextStyle( + AppLocalizations.of(context)!.bookingNoCurrentBooking, + style: const TextStyle( color: Colors.black, fontSize: 20, fontWeight: FontWeight.bold, @@ -74,16 +75,16 @@ class ManagerPage extends HookConsumerWidget { ), ), ListBooking( - title: BookingTextConstants.pending, + title: AppLocalizations.of(context)!.bookingPending, bookings: pendingBookings, canToggle: false, ), ListBooking( - title: BookingTextConstants.confirmed, + title: AppLocalizations.of(context)!.bookingConfirmed, bookings: confirmedBookings, ), ListBooking( - title: BookingTextConstants.declined, + title: AppLocalizations.of(context)!.bookingDeclined, bookings: canceledBookings, ), const SizedBox(height: 30), diff --git a/lib/centralisation/router.dart b/lib/centralisation/router.dart index 160e528a33..69d90b9d67 100644 --- a/lib/centralisation/router.dart +++ b/lib/centralisation/router.dart @@ -1,10 +1,9 @@ -import 'package:either_dart/either.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:titan/centralisation/tools/constants.dart'; import 'package:titan/centralisation/ui/pages/main_page.dart' deferred as main_page; -import 'package:titan/drawer/class/module.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/class/module.dart'; import 'package:titan/tools/middlewares/authenticated_middleware.dart'; import 'package:titan/tools/middlewares/deferred_middleware.dart'; import 'package:qlevar_router/qlevar_router.dart'; @@ -13,10 +12,10 @@ class CentralisationRouter { final Ref ref; static const String root = '/centralisation'; static final Module module = Module( - name: CentralisationTextConstants.centralisation, - icon: const Left(HeroIcons.link), + getName: (context) => AppLocalizations.of(context)!.moduleCentralisation, + getDescription: (context) => + AppLocalizations.of(context)!.moduleCentralisationDescription, root: CentralisationRouter.root, - selected: false, ); CentralisationRouter(this.ref); @@ -28,5 +27,9 @@ class CentralisationRouter { AuthenticatedMiddleware(ref), DeferredLoadingMiddleware(main_page.loadLibrary), ], + pageType: QCustomPage( + transitionsBuilder: (_, animation, _, child) => + FadeTransition(opacity: animation, child: child), + ), ); } diff --git a/lib/centralisation/ui/centralisation.dart b/lib/centralisation/ui/centralisation.dart index 3cba972886..21faa3339d 100644 --- a/lib/centralisation/ui/centralisation.dart +++ b/lib/centralisation/ui/centralisation.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:titan/centralisation/router.dart'; -import 'package:titan/centralisation/tools/constants.dart'; import 'package:titan/tools/ui/widgets/top_bar.dart'; +import 'package:titan/tools/constants.dart'; class CentralisationTemplate extends StatelessWidget { final Widget child; @@ -11,15 +11,12 @@ class CentralisationTemplate extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( body: Container( - color: Colors.white, + color: ColorConstants.background, child: SafeArea( child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ - const TopBar( - title: CentralisationTextConstants.centralisation, - root: CentralisationRouter.root, - ), + const TopBar(root: CentralisationRouter.root), Expanded(child: child), ], ), diff --git a/lib/cinema/providers/is_cinema_admin.dart b/lib/cinema/providers/is_cinema_admin.dart index 5a15b340c5..0e8f1e5afb 100644 --- a/lib/cinema/providers/is_cinema_admin.dart +++ b/lib/cinema/providers/is_cinema_admin.dart @@ -5,5 +5,5 @@ final isCinemaAdminProvider = StateProvider((ref) { final me = ref.watch(userProvider); return me.groups .map((e) => e.id) - .contains("ce5f36e6-5377-489f-9696-de70e2477300"); + .contains("ce5f36e6-5377-489f-9696-de70e2477300"); // admin_cinema }); diff --git a/lib/cinema/router.dart b/lib/cinema/router.dart index 01108fcdee..be9230bbd6 100644 --- a/lib/cinema/router.dart +++ b/lib/cinema/router.dart @@ -1,6 +1,5 @@ -import 'package:either_dart/either.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:heroicons/heroicons.dart'; import 'package:titan/cinema/providers/is_cinema_admin.dart'; import 'package:titan/cinema/ui/pages/admin_page/admin_page.dart' deferred as admin_page; @@ -10,7 +9,8 @@ import 'package:titan/cinema/ui/pages/main_page/main_page.dart' deferred as main_page; import 'package:titan/cinema/ui/pages/session_pages/add_edit_session.dart' deferred as add_edit_session_page; -import 'package:titan/drawer/class/module.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/class/module.dart'; import 'package:titan/tools/middlewares/admin_middleware.dart'; import 'package:titan/tools/middlewares/authenticated_middleware.dart'; import 'package:titan/tools/middlewares/deferred_middleware.dart'; @@ -24,10 +24,10 @@ class CinemaRouter { static const String addEdit = '/add_edit'; static const String detail = '/detail'; static final Module module = Module( - name: "Cinéma", - icon: const Left(HeroIcons.ticket), + getName: (context) => AppLocalizations.of(context)!.moduleCinema, + getDescription: (context) => + AppLocalizations.of(context)!.moduleCinemaDescription, root: CinemaRouter.root, - selected: false, ); CinemaRouter(this.ref); @@ -40,6 +40,10 @@ class CinemaRouter { NotificationMiddleWare(ref), DeferredLoadingMiddleware(main_page.loadLibrary), ], + pageType: QCustomPage( + transitionsBuilder: (_, animation, _, child) => + FadeTransition(opacity: animation, child: child), + ), children: [ QRoute( path: detail, diff --git a/lib/cinema/tools/constants.dart b/lib/cinema/tools/constants.dart index 933f612c0b..2b625d283a 100644 --- a/lib/cinema/tools/constants.dart +++ b/lib/cinema/tools/constants.dart @@ -3,37 +3,3 @@ import 'package:flutter/material.dart'; class CinemaColorConstants { static const Color tmdbColor = Color(0xffe2b616); } - -class CinemaTextConstants { - static const String add = "Ajouter"; - static const String addedSession = "Séance ajoutée"; - static const String addingError = "Erreur lors de l'ajout"; - static const String addSession = "Ajouter une séance"; - static const String cinema = "Cinéma"; - static const String deleteSession = "Supprimer la séance ?"; - static const String deleting = "Suppression"; - static const String duration = "Durée"; - static const String edit = "Modifier"; - static const String editedSession = "Séance modifiée"; - static const String editingError = "Erreur lors de la modification"; - static const String editSession = "Modifier la séance"; - static const String emptyUrl = "Veuillez entrer une URL"; - static const String importFromTMDB = "Importer depuis TMDB"; - static const String incomingSession = "A l'affiche"; - static const String incorrectOrMissingFields = - "Champs incorrects ou manquants"; - static const String invalidUrl = "URL invalide"; - static const String genre = "Genre"; - static const String name = "Nom"; - static const String noDateError = "Veuillez entrer une date"; - static const String noDuration = "Veuillez entrer une durée"; - static const String noOverview = "Aucun synopsis"; - static const String noPoster = "Aucune affiche"; - static const String noSession = "Aucune séance"; - static const String overview = "Synopsis"; - static const String posterUrl = "URL de l'affiche"; - static const String sessionDate = "Jour de la séance"; - static const String startHour = "Heure de début"; - static const String tagline = "Slogan"; - static const String the = "Le"; -} diff --git a/lib/cinema/ui/cinema.dart b/lib/cinema/ui/cinema.dart index 6376ce34c6..5d6c4f3f83 100644 --- a/lib/cinema/ui/cinema.dart +++ b/lib/cinema/ui/cinema.dart @@ -1,9 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/cinema/providers/main_page_index_provider.dart'; -import 'package:titan/cinema/providers/scroll_provider.dart'; import 'package:titan/cinema/router.dart'; -import 'package:titan/cinema/tools/constants.dart'; +import 'package:titan/tools/constants.dart'; import 'package:titan/tools/ui/widgets/top_bar.dart'; class CinemaTemplate extends HookConsumerWidget { @@ -12,22 +10,16 @@ class CinemaTemplate extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final initialPageNotifier = ref.watch(mainPageIndexProvider.notifier); - final scrollNotifier = ref.watch(scrollProvider.notifier); - return SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - TopBar( - title: CinemaTextConstants.cinema, - root: CinemaRouter.root, - onMenu: () { - initialPageNotifier.reset(); - scrollNotifier.reset(); - }, - ), - Expanded(child: child), - ], + return Container( + color: ColorConstants.background, + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + TopBar(root: CinemaRouter.root), + Expanded(child: child), + ], + ), ), ); } diff --git a/lib/cinema/ui/pages/admin_page/admin_page.dart b/lib/cinema/ui/pages/admin_page/admin_page.dart index e32adf28e7..46718e86ff 100644 --- a/lib/cinema/ui/pages/admin_page/admin_page.dart +++ b/lib/cinema/ui/pages/admin_page/admin_page.dart @@ -5,13 +5,13 @@ import 'package:titan/cinema/class/session.dart'; import 'package:titan/cinema/providers/session_list_provider.dart'; import 'package:titan/cinema/providers/session_provider.dart'; import 'package:titan/cinema/router.dart'; -import 'package:titan/cinema/tools/constants.dart'; import 'package:titan/cinema/ui/cinema.dart'; import 'package:titan/cinema/ui/pages/admin_page/admin_session_card.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/layouts/card_layout.dart'; import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AdminPage extends HookConsumerWidget { const AdminPage({super.key}); @@ -60,8 +60,10 @@ class AdminPage extends HookConsumerWidget { context: context, builder: (context) { return CustomDialogBox( - title: CinemaTextConstants.deleting, - descriptions: CinemaTextConstants.deleteSession, + title: AppLocalizations.of(context)!.cinemaDeleting, + descriptions: AppLocalizations.of( + context, + )!.cinemaDeleteSession, onYes: () { sessionListNotifier.deleteSession(session); }, diff --git a/lib/cinema/ui/pages/detail_page/detail_page.dart b/lib/cinema/ui/pages/detail_page/detail_page.dart index b9d8976307..d9e959e64e 100644 --- a/lib/cinema/ui/pages/detail_page/detail_page.dart +++ b/lib/cinema/ui/pages/detail_page/detail_page.dart @@ -9,7 +9,6 @@ import 'package:titan/cinema/providers/cinema_topic_provider.dart'; import 'package:titan/cinema/providers/session_poster_map_provider.dart'; import 'package:titan/cinema/providers/session_poster_provider.dart'; import 'package:titan/cinema/providers/session_provider.dart'; -import 'package:titan/cinema/tools/constants.dart'; import 'package:titan/cinema/tools/functions.dart'; import 'package:titan/service/class/message.dart'; import 'package:titan/service/local_notification_service.dart'; @@ -17,6 +16,7 @@ import 'package:titan/tools/functions.dart'; import 'package:titan/tools/ui/builders/auto_loader_child.dart'; import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class DetailPage extends HookConsumerWidget { const DetailPage({super.key}); @@ -153,7 +153,7 @@ class DetailPage extends HookConsumerWidget { child: Text( session.overview != null ? session.overview! - : CinemaTextConstants.noOverview, + : AppLocalizations.of(context)!.cinemaNoOverview, textAlign: TextAlign.left, style: const TextStyle(fontSize: 15), ), diff --git a/lib/cinema/ui/pages/main_page/main_page.dart b/lib/cinema/ui/pages/main_page/main_page.dart index 1423fe525d..5f345e4551 100644 --- a/lib/cinema/ui/pages/main_page/main_page.dart +++ b/lib/cinema/ui/pages/main_page/main_page.dart @@ -9,14 +9,14 @@ import 'package:titan/cinema/providers/session_list_provider.dart'; import 'package:titan/cinema/providers/session_poster_map_provider.dart'; import 'package:titan/cinema/providers/session_provider.dart'; import 'package:titan/cinema/router.dart'; -import 'package:titan/cinema/tools/constants.dart'; import 'package:titan/cinema/ui/cinema.dart'; import 'package:titan/cinema/ui/pages/main_page/session_card.dart'; -import 'package:titan/drawer/providers/is_web_format_provider.dart'; +import 'package:titan/navigation/providers/is_web_format_provider.dart'; import 'package:titan/tools/ui/widgets/admin_button.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/layouts/refresher.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class CinemaMainPage extends HookConsumerWidget { const CinemaMainPage({super.key}); @@ -45,6 +45,7 @@ class CinemaMainPage extends HookConsumerWidget { return CinemaTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await sessionListNotifier.loadSessions(); ref.watch(mainPageIndexProvider.notifier).reset(); @@ -63,9 +64,9 @@ class CinemaMainPage extends HookConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( - CinemaTextConstants.incomingSession, - style: TextStyle( + Text( + AppLocalizations.of(context)!.cinemaIncomingSession, + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.grey, @@ -88,12 +89,12 @@ class CinemaMainPage extends HookConsumerWidget { builder: (context, data) { data.sort((a, b) => a.start.compareTo(b.start)); if (data.isEmpty) { - return const SizedBox( + return SizedBox( height: 200, child: Center( child: Text( - CinemaTextConstants.noSession, - style: TextStyle( + AppLocalizations.of(context)!.cinemaNoSession, + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black, diff --git a/lib/cinema/ui/pages/main_page/session_card.dart b/lib/cinema/ui/pages/main_page/session_card.dart index c1513a9ad0..afa5f5eb9d 100644 --- a/lib/cinema/ui/pages/main_page/session_card.dart +++ b/lib/cinema/ui/pages/main_page/session_card.dart @@ -6,10 +6,10 @@ import 'package:titan/cinema/providers/cinema_topic_provider.dart'; import 'package:titan/cinema/providers/scroll_provider.dart'; import 'package:titan/cinema/providers/session_poster_map_provider.dart'; import 'package:titan/cinema/providers/session_poster_provider.dart'; -import 'package:titan/cinema/tools/constants.dart'; import 'package:titan/cinema/tools/functions.dart'; -import 'package:titan/drawer/providers/is_web_format_provider.dart'; +import 'package:titan/navigation/providers/is_web_format_provider.dart'; import 'package:titan/tools/ui/builders/auto_loader_child.dart'; +import 'package:titan/l10n/app_localizations.dart'; class SessionCard extends HookConsumerWidget { final Session session; @@ -164,7 +164,9 @@ class SessionCard extends HookConsumerWidget { const SizedBox(height: 10), Text( session.overview ?? - CinemaTextConstants.noOverview, + AppLocalizations.of( + context, + )!.cinemaNoOverview, textAlign: TextAlign.center, style: const TextStyle(fontSize: 16), ), diff --git a/lib/cinema/ui/pages/session_pages/add_edit_session.dart b/lib/cinema/ui/pages/session_pages/add_edit_session.dart index 86e350a3ec..23026ebd99 100644 --- a/lib/cinema/ui/pages/session_pages/add_edit_session.dart +++ b/lib/cinema/ui/pages/session_pages/add_edit_session.dart @@ -4,13 +4,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:titan/cinema/class/session.dart'; import 'package:titan/cinema/providers/session_list_provider.dart'; import 'package:titan/cinema/providers/session_poster_map_provider.dart'; import 'package:titan/cinema/providers/session_poster_provider.dart'; import 'package:titan/cinema/providers/session_provider.dart'; import 'package:titan/cinema/providers/the_movie_db_genre_provider.dart'; -import 'package:titan/cinema/tools/constants.dart'; import 'package:titan/cinema/tools/functions.dart'; import 'package:titan/cinema/ui/cinema.dart'; import 'package:titan/cinema/ui/pages/session_pages/tmdb_button.dart'; @@ -22,12 +22,14 @@ import 'package:titan/tools/ui/widgets/date_entry.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; import 'package:titan/tools/ui/widgets/text_entry.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AddEditSessionPage extends HookConsumerWidget { const AddEditSessionPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final locale = Localizations.localeOf(context); final session = ref.watch(sessionProvider); final movieNotifier = ref.watch(theMovieDBMovieProvider.notifier); final isEdit = session.id != Session.empty().id; @@ -42,7 +44,7 @@ class AddEditSessionPage extends HookConsumerWidget { final genre = useTextEditingController(text: session.genre ?? ''); final overview = useTextEditingController(text: session.overview ?? ''); final start = useTextEditingController( - text: isEdit ? processDateWithHour(session.start) : '', + text: isEdit ? DateFormat.yMd(locale).add_Hm().format(session.start) : '', ); final tagline = useTextEditingController(text: session.tagline ?? ''); final sessionPosterMap = ref.watch(sessionPosterMapProvider); @@ -74,8 +76,8 @@ class AddEditSessionPage extends HookConsumerWidget { children: [ AlignLeftText( isEdit - ? CinemaTextConstants.editSession - : CinemaTextConstants.addSession, + ? AppLocalizations.of(context)!.cinemaEditSession + : AppLocalizations.of(context)!.cinemaAddSession, color: Colors.grey, ), const SizedBox(height: 30), @@ -83,7 +85,9 @@ class AddEditSessionPage extends HookConsumerWidget { controller: tmdbUrl, cursorColor: Colors.black, decoration: InputDecoration( - labelText: CinemaTextConstants.importFromTMDB, + labelText: AppLocalizations.of( + context, + )!.cinemaImportFromTMDB, labelStyle: const TextStyle( color: Colors.black, fontSize: 20, @@ -95,7 +99,7 @@ class AddEditSessionPage extends HookConsumerWidget { if (tmdbUrl.text.isEmpty) { displayToastWithContext( TypeMsg.error, - CinemaTextConstants.emptyUrl, + AppLocalizations.of(context)!.cinemaEmptyUrl, ); return; } @@ -135,7 +139,7 @@ class AddEditSessionPage extends HookConsumerWidget { } on FormatException catch (_) { displayToastWithContext( TypeMsg.error, - CinemaTextConstants.invalidUrl, + AppLocalizations.of(context)!.cinemaInvalidUrl, ); return; } @@ -183,10 +187,13 @@ class AddEditSessionPage extends HookConsumerWidget { ) : Image.memory(logo.value!, fit: BoxFit.cover), const SizedBox(height: 30), - TextEntry(label: CinemaTextConstants.name, controller: name), + TextEntry( + label: AppLocalizations.of(context)!.cinemaName, + controller: name, + ), const SizedBox(height: 30), TextEntry( - label: CinemaTextConstants.posterUrl, + label: AppLocalizations.of(context)!.cinemaPosterUrl, controller: posterUrl, onChanged: (value) async { logo.value = await getFromUrl(posterUrl.text); @@ -196,30 +203,30 @@ class AddEditSessionPage extends HookConsumerWidget { const SizedBox(height: 30), DateEntry( onTap: () => getFullDate(context, start), - label: CinemaTextConstants.sessionDate, + label: AppLocalizations.of(context)!.cinemaSessionDate, controller: start, ), const SizedBox(height: 30), DateEntry( onTap: () => getOnlyHourDate(context, duration), - label: CinemaTextConstants.duration, + label: AppLocalizations.of(context)!.cinemaDuration, controller: duration, ), const SizedBox(height: 30), TextEntry( - label: CinemaTextConstants.genre, + label: AppLocalizations.of(context)!.cinemaGenre, controller: genre, canBeEmpty: true, ), const SizedBox(height: 30), TextEntry( - label: CinemaTextConstants.overview, + label: AppLocalizations.of(context)!.cinemaOverview, controller: overview, canBeEmpty: true, ), const SizedBox(height: 30), TextEntry( - label: CinemaTextConstants.tagline, + label: AppLocalizations.of(context)!.cinemaTagline, controller: tagline, canBeEmpty: true, ), @@ -230,11 +237,23 @@ class AddEditSessionPage extends HookConsumerWidget { if (key.currentState == null) { return; } + final editedSessionMsg = AppLocalizations.of( + context, + )!.cinemaEditedSession; + final addedSessionMsg = AppLocalizations.of( + context, + )!.cinemaAddedSession; + final editingErrorMsg = AppLocalizations.of( + context, + )!.cinemaEditingError; + final addingErrorMsg = AppLocalizations.of( + context, + )!.cinemaAddingError; if (key.currentState!.validate()) { if (logo.value == null && logoFile.value == null) { displayToastWithContext( TypeMsg.error, - CinemaTextConstants.noPoster, + AppLocalizations.of(context)!.cinemaNoPoster, ); return; } @@ -248,7 +267,10 @@ class AddEditSessionPage extends HookConsumerWidget { ? null : overview.text, start: DateTime.parse( - processDateBackWithHour(start.text), + processDateBackWithHour( + start.text, + locale.toString(), + ), ), tagline: tagline.text.isEmpty ? null : tagline.text, ); @@ -279,7 +301,7 @@ class AddEditSessionPage extends HookConsumerWidget { ); displayToastWithContext( TypeMsg.msg, - CinemaTextConstants.editedSession, + editedSessionMsg, ); } else { sessionList.maybeWhen( @@ -302,19 +324,19 @@ class AddEditSessionPage extends HookConsumerWidget { ); displayToastWithContext( TypeMsg.msg, - CinemaTextConstants.addedSession, + addedSessionMsg, ); } } else { if (isEdit) { displayToastWithContext( TypeMsg.error, - CinemaTextConstants.editingError, + editingErrorMsg, ); } else { displayToastWithContext( TypeMsg.error, - CinemaTextConstants.addingError, + addingErrorMsg, ); } } @@ -323,12 +345,16 @@ class AddEditSessionPage extends HookConsumerWidget { displayToast( context, TypeMsg.error, - CinemaTextConstants.incorrectOrMissingFields, + AppLocalizations.of( + context, + )!.cinemaIncorrectOrMissingFields, ); } }, child: Text( - isEdit ? CinemaTextConstants.edit : CinemaTextConstants.add, + isEdit + ? AppLocalizations.of(context)!.cinemaEdit + : AppLocalizations.of(context)!.cinemaAdd, style: const TextStyle( color: Colors.white, fontSize: 25, diff --git a/lib/drawer/class/module.dart b/lib/drawer/class/module.dart deleted file mode 100644 index d32bcec9b8..0000000000 --- a/lib/drawer/class/module.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:either_dart/either.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:heroicons/heroicons.dart'; - -enum ModuleType { - calendar, - settings, - amap, - loan, - booking, - admin, - event, - vote, - tombola, - cinema, - paiement, -} - -class Module { - String name; - Either icon; - String root; - bool selected; - - Module({ - required this.name, - required this.icon, - required this.root, - required this.selected, - }); - - Module copy({ - String? name, - Either? icon, - String? root, - bool? selected, - }) => Module( - name: name ?? this.name, - icon: icon ?? this.icon, - root: root ?? this.root, - selected: selected ?? this.selected, - ); - - Widget getIcon(Color color, {double size = 30}) { - return icon.fold( - (heroIcon) => HeroIcon(heroIcon, color: color, size: size), - (svgPath) => SvgPicture.asset( - svgPath, - width: size, - height: size, - colorFilter: ColorFilter.mode(color, BlendMode.srcIn), - ), - ); - } -} diff --git a/lib/drawer/class/top_bar_callback.dart b/lib/drawer/class/top_bar_callback.dart deleted file mode 100644 index 09f0a59bcf..0000000000 --- a/lib/drawer/class/top_bar_callback.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:flutter/material.dart'; - -class TopBarCallback { - final String moduleRoot; - final VoidCallback? onMenu; - final VoidCallback? onBack; - - TopBarCallback({this.onMenu, this.onBack, required this.moduleRoot}); -} diff --git a/lib/drawer/providers/already_displayed_popup.dart b/lib/drawer/providers/already_displayed_popup.dart deleted file mode 100644 index 67393b6838..0000000000 --- a/lib/drawer/providers/already_displayed_popup.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -class AlreadyDisplayedNotifier extends StateNotifier { - AlreadyDisplayedNotifier() : super(false); - - void setAlreadyDisplayed() { - state = true; - } -} - -final alreadyDisplayedProvider = - StateNotifierProvider((ref) { - return AlreadyDisplayedNotifier(); - }); diff --git a/lib/drawer/providers/modules_provider.dart b/lib/drawer/providers/modules_provider.dart deleted file mode 100644 index 04e61ae8aa..0000000000 --- a/lib/drawer/providers/modules_provider.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:titan/drawer/class/module.dart'; -import 'package:titan/settings/providers/module_list_provider.dart'; - -class ModuleListNotifier extends StateNotifier> { - ModuleListNotifier(super.listModule); - - void select(int i) { - List r = state.sublist(0); - - for (int j = 0; j < r.length; j++) { - if (i == j) { - r[i].selected = true; - } else { - r[j].selected = false; - } - } - - state = r; - } -} - -final listModuleProvider = - StateNotifierProvider>((ref) { - final modules = ref.watch(modulesProvider); - return ModuleListNotifier(modules); - }); diff --git a/lib/drawer/providers/swipe_provider.dart b/lib/drawer/providers/swipe_provider.dart deleted file mode 100644 index b4e79beba6..0000000000 --- a/lib/drawer/providers/swipe_provider.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -class SwipeControllerNotifier extends StateNotifier { - SwipeControllerNotifier(super.controller); - - static const double maxSlide = 255; - static const dragRightStartVal = 60; - static const dragLeftStartVal = maxSlide - 30; - static bool shouldDrag = false; - - void close() { - state.reverse(); - } - - void open() { - state.forward(); - } - - void toggle() { - if (state.isCompleted) { - close(); - } else { - open(); - } - } - - void onDragStart(DragStartDetails startDetails) { - bool isDraggingFromLeft = - state.isDismissed && startDetails.globalPosition.dx < dragRightStartVal; - bool isDraggingFromRight = - !state.isDismissed && startDetails.globalPosition.dx > dragLeftStartVal; - shouldDrag = isDraggingFromLeft || isDraggingFromRight; - } - - void onDragUpdate(DragUpdateDetails updateDetails) { - if (shouldDrag) { - double delta = updateDetails.primaryDelta! / maxSlide; - state.value += delta; - } - } - - void onDragEnd(DragEndDetails endDetails, double width) { - if (!state.isDismissed && !state.isCompleted) { - double minFlingVelocity = 365.0; - double dragVelocity = endDetails.velocity.pixelsPerSecond.dx.abs(); - if (dragVelocity >= minFlingVelocity) { - double visualVelocity = endDetails.velocity.pixelsPerSecond.dx / width; - state.fling(velocity: visualVelocity); - } else if (state.value < 0.5) { - close(); - } else { - open(); - } - } - } -} - -final swipeControllerProvider = - StateNotifierProvider.family< - SwipeControllerNotifier, - AnimationController, - AnimationController - >((ref, animationController) { - return SwipeControllerNotifier(animationController); - }); diff --git a/lib/drawer/providers/top_bar_callback_provider.dart b/lib/drawer/providers/top_bar_callback_provider.dart deleted file mode 100644 index a4f7644f6b..0000000000 --- a/lib/drawer/providers/top_bar_callback_provider.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:titan/drawer/class/top_bar_callback.dart'; - -class AnimationNotifier extends StateNotifier { - AnimationNotifier() : super(TopBarCallback(moduleRoot: '')); - - void setCallBacks(TopBarCallback callbacks) { - if (state.moduleRoot == callbacks.moduleRoot) { - return; - } - state = callbacks; - } -} - -final topBarCallBackProvider = - StateNotifierProvider((ref) { - return AnimationNotifier(); - }); diff --git a/lib/drawer/tools/constants.dart b/lib/drawer/tools/constants.dart deleted file mode 100644 index d488510137..0000000000 --- a/lib/drawer/tools/constants.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter/material.dart'; - -class DrawerColorConstants { - static final Color lightText = Colors.grey.shade100.withValues(alpha: 0.6); - static final Color selectedText = Colors.grey.shade100; - static const Color lightBlue = Color.fromARGB(255, 86, 95, 95); - static const Color darkBlue = Color.fromARGB(255, 62, 62, 62); - static const Color fakePageBlue = Color.fromARGB(24, 161, 161, 161); - static const Color fakePageShadow = Color.fromARGB(14, 161, 161, 161); -} - -class DrawerTextConstants { - static const String admin = "Administration"; - static const String androidAppLink = - "https://play.google.com/store/apps/details?id=fr.myecl.titan"; - static const String copied = "Copié !"; - static const String downloadAppOnMobileDevice = - "Ce site est la version Web de l'application MyECL. Nous vous invitons à télécharger l'application. N'utilisez ce site qu'en cas de problème avec l'application.\n"; - static const String iosAppLink = - "https://apps.apple.com/fr/app/myecl/id6444443430"; - static const String loginOut = "Voulez-vous vous déconnecter ?"; - static const String logOut = "Déconnexion"; - static const String or = " ou "; - static const String settings = "Paramètres"; -} diff --git a/lib/drawer/ui/bottom_bar.dart b/lib/drawer/ui/bottom_bar.dart deleted file mode 100644 index 94d74e5942..0000000000 --- a/lib/drawer/ui/bottom_bar.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:titan/drawer/providers/display_quit_popup.dart'; -import 'package:titan/drawer/tools/constants.dart'; - -class BottomBar extends ConsumerWidget { - const BottomBar({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final displayQuitNotifier = ref.watch(displayQuitProvider.notifier); - return Column( - children: [ - SizedBox( - height: 30, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - displayQuitNotifier.setDisplay(true); - }, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox(width: 25), - HeroIcon( - HeroIcons.arrowRightOnRectangle, - color: DrawerColorConstants.lightText, - size: 27, - ), - const SizedBox(width: 15), - Text( - DrawerTextConstants.logOut, - style: TextStyle( - color: DrawerColorConstants.lightText, - fontSize: 18, - ), - ), - ], - ), - ), - ], - ), - ), - const SizedBox(height: 30), - ], - ); - } -} diff --git a/lib/drawer/ui/custom_drawer.dart b/lib/drawer/ui/custom_drawer.dart deleted file mode 100644 index 6fd148196b..0000000000 --- a/lib/drawer/ui/custom_drawer.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/drawer/providers/is_web_format_provider.dart'; -import 'package:titan/drawer/tools/constants.dart'; -import 'package:titan/drawer/ui/bottom_bar.dart'; -import 'package:titan/drawer/ui/fake_page.dart'; -import 'package:titan/drawer/ui/list_module.dart'; -import 'package:titan/drawer/ui/drawer_top_bar.dart'; - -class CustomDrawer extends HookConsumerWidget { - const CustomDrawer({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isWebFormat = ref.watch(isWebFormatProvider); - return Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - DrawerColorConstants.lightBlue, - DrawerColorConstants.darkBlue, - ], - ), - ), - child: SafeArea( - child: Stack( - children: [ - const Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [DrawerTopBar(), BottomBar()], - ), - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded( - child: SizedBox( - width: 200, - height: MediaQuery.of(context).size.height * 4.4 / 10, - child: const ListModule(), - ), - ), - isWebFormat - ? Container( - width: MediaQuery.of(context).size.width - 220, - ) - : const FakePage(), - ], - ), - ], - ), - ], - ), - ), - ); - } -} diff --git a/lib/drawer/ui/drawer_template.dart b/lib/drawer/ui/drawer_template.dart deleted file mode 100644 index eaa11ebe05..0000000000 --- a/lib/drawer/ui/drawer_template.dart +++ /dev/null @@ -1,136 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter/foundation.dart' show kIsWeb; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/auth/providers/openid_provider.dart'; -import 'package:titan/drawer/providers/animation_provider.dart'; -import 'package:titan/drawer/providers/display_quit_popup.dart'; -import 'package:titan/drawer/providers/is_web_format_provider.dart'; -import 'package:titan/drawer/providers/should_setup_provider.dart'; -import 'package:titan/drawer/providers/swipe_provider.dart'; -import 'package:titan/drawer/ui/custom_drawer.dart'; -import 'package:titan/service/tools/setup.dart'; -import 'package:titan/drawer/providers/already_displayed_popup.dart'; -import 'package:titan/drawer/ui/quit_dialog.dart'; -import 'package:titan/drawer/ui/email_change_popup.dart'; -import 'package:titan/tools/providers/should_notify_provider.dart'; -import 'package:titan/user/providers/user_provider.dart'; -import 'package:qlevar_router/qlevar_router.dart'; - -class DrawerTemplate extends HookConsumerWidget { - static Duration duration = const Duration(milliseconds: 200); - static const double maxSlide = 255; - static const dragRightStartVal = 60; - static const dragLeftStartVal = maxSlide - 20; - static bool shouldDrag = false; - final Widget child; - - const DrawerTemplate({super.key, required this.child}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - // We are logged in, so we can set up the notification - final user = ref.watch(userProvider); - final animationController = useAnimationController( - duration: duration, - initialValue: 1, - ); - final animationNotifier = ref.read(animationProvider.notifier); - final controller = ref.watch(swipeControllerProvider(animationController)); - final controllerNotifier = ref.watch( - swipeControllerProvider(animationController).notifier, - ); - final isWebFormat = ref.watch(isWebFormatProvider); - final alreadyDisplayed = ref.watch(alreadyDisplayedProvider); - final displayQuit = ref.watch(displayQuitProvider); - final shouldNotify = ref.watch(shouldNotifyProvider); - final isLoggedIn = ref.watch(isLoggedInProvider); - final shouldSetup = ref.watch(shouldSetupProvider); - final shouldSetupNotifier = ref.read(shouldSetupProvider.notifier); - if (isWebFormat) { - controllerNotifier.close(); - } - - Future(() { - animationNotifier.setController(animationController); - if (!kIsWeb && user.id != "" && shouldSetup) { - setUpNotification(ref); - shouldSetupNotifier.setShouldSetup(); - } - }); - - return Scaffold( - body: Stack( - children: [ - GestureDetector( - onHorizontalDragStart: controllerNotifier.onDragStart, - onHorizontalDragUpdate: controllerNotifier.onDragUpdate, - onHorizontalDragEnd: (details) => controllerNotifier.onDragEnd( - details, - MediaQuery.of(context).size.width, - ), - onTap: () {}, - child: AnimatedBuilder( - animation: controller, - builder: (BuildContext context, _) { - double animationVal = controller.value; - double translateVal = animationVal * maxSlide; - double scaleVal = 1 - (isWebFormat ? 0 : (animationVal * 0.3)); - double cornerVal = isWebFormat ? 0 : 30.0 * animationVal; - return Stack( - children: [ - const CustomDrawer(), - Transform( - alignment: Alignment.centerLeft, - transform: Matrix4.identity() - ..translate(translateVal) - ..scale(scaleVal), - child: GestureDetector( - onTap: () { - if (controller.isCompleted) { - controllerNotifier.close(); - } - }, - child: ClipRRect( - borderRadius: BorderRadius.circular(cornerVal), - child: Stack( - children: [ - Container( - color: Colors.white, - child: IgnorePointer( - ignoring: controller.isCompleted, - child: child, - ), - ), - MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (event) { - controllerNotifier.toggle(); - }, - child: Container( - color: Colors.transparent, - width: 20, - height: double.infinity, - ), - ), - ], - ), - ), - ), - ), - ], - ); - }, - ), - ), - if (isLoggedIn && - shouldNotify && - QR.context != null && - !alreadyDisplayed) - const EmailChangeDialog(), - if (displayQuit) const QuitDialog(), - ], - ), - ); - } -} diff --git a/lib/drawer/ui/drawer_top_bar.dart b/lib/drawer/ui/drawer_top_bar.dart deleted file mode 100644 index 2c9447b638..0000000000 --- a/lib/drawer/ui/drawer_top_bar.dart +++ /dev/null @@ -1,279 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/providers/is_admin_provider.dart'; -import 'package:titan/admin/router.dart'; -import 'package:titan/auth/providers/is_connected_provider.dart'; -import 'package:titan/drawer/providers/animation_provider.dart'; -import 'package:titan/drawer/providers/swipe_provider.dart'; -import 'package:titan/drawer/tools/constants.dart'; -import 'package:titan/home/providers/scrolled_provider.dart'; -import 'package:titan/settings/router.dart'; -import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:titan/tools/providers/path_forwarding_provider.dart'; -import 'package:titan/tools/ui/builders/async_child.dart'; -import 'package:titan/user/providers/user_provider.dart'; -import 'package:titan/user/providers/profile_picture_provider.dart'; -import 'package:qlevar_router/qlevar_router.dart'; - -class DrawerTopBar extends HookConsumerWidget { - const DrawerTopBar({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final pathForwardingNotifier = ref.read(pathForwardingProvider.notifier); - final pathForwarding = ref.watch(pathForwardingProvider); - final user = ref.watch(userProvider); - final profilePicture = ref.watch(profilePictureProvider); - final hasScrolled = ref.watch(hasScrolledProvider.notifier); - final isAdmin = ref.watch(isAdminProvider); - final isConnected = ref.watch(isConnectedProvider); - final animation = ref.watch(animationProvider); - final dropDownAnimation = useAnimationController( - duration: const Duration(milliseconds: 250), - initialValue: 0.0, - ); - - void onBack(String path) { - if (animation != null) { - final controllerNotifier = ref.watch( - swipeControllerProvider(animation).notifier, - ); - controllerNotifier.toggle(); - } - QR.to(path); - pathForwardingNotifier.forward(path); - hasScrolled.setHasScrolled(false); - } - - return Column( - children: [ - Container(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container(width: 25), - GestureDetector( - onTap: () { - if (isAdmin) { - if (dropDownAnimation.isDismissed) { - dropDownAnimation.forward(); - } else { - dropDownAnimation.reverse(); - } - } else { - onBack(SettingsRouter.root); - } - }, - behavior: HitTestBehavior.opaque, - child: Row( - children: [ - AsyncChild( - value: profilePicture, - builder: (context, file) => Row( - children: [ - Stack( - children: [ - Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withValues( - alpha: 0.1, - ), - spreadRadius: 5, - blurRadius: 10, - offset: const Offset(0, 3), - ), - ], - ), - child: CircleAvatar( - radius: 25, - backgroundImage: file.isEmpty - ? AssetImage(getTitanLogo()) - : Image.memory(file).image, - ), - ), - if (isAdmin) - Positioned( - bottom: 0, - right: 0, - child: GestureDetector( - onTap: () async {}, - child: Container( - height: 18, - width: 18, - padding: const EdgeInsets.all(3), - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: const LinearGradient( - colors: [ - ColorConstants.gradient1, - ColorConstants.gradient2, - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - boxShadow: [ - BoxShadow( - color: ColorConstants.gradient2 - .withValues(alpha: 0.3), - spreadRadius: 1, - blurRadius: 2, - offset: const Offset(1, 2), - ), - ], - ), - child: const HeroIcon( - HeroIcons.bolt, - color: Colors.white, - ), - ), - ), - ), - ], - ), - const SizedBox(width: 15), - ], - ), - ), - Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - SizedBox( - width: 200, - child: Text( - user.nickname ?? user.firstname, - style: TextStyle( - color: Colors.grey.shade100, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - ), - Container(height: 3), - SizedBox( - width: 200, - child: Text( - user.nickname != null - ? "${user.firstname} ${user.name}" - : user.name, - style: TextStyle( - color: Colors.grey.shade100, - fontSize: 15, - ), - ), - ), - ], - ), - ], - ), - ), - ], - ), - if (!isConnected) - Container( - margin: const EdgeInsets.only(right: 20), - child: const HeroIcon( - HeroIcons.signalSlash, - color: Colors.white, - size: 40, - ), - ), - ], - ), - AnimatedBuilder( - builder: (context, child) { - return Opacity( - opacity: dropDownAnimation.value, - child: Padding( - padding: const EdgeInsets.only(left: 25, top: 15), - child: Column( - children: [ - Transform.translate( - offset: Offset(0, -10 * (1 - dropDownAnimation.value)), - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () => onBack(SettingsRouter.root), - child: Row( - children: [ - HeroIcon( - HeroIcons.cog, - color: - pathForwarding.path.startsWith( - SettingsRouter.root, - ) - ? DrawerColorConstants.selectedText - : DrawerColorConstants.lightText, - size: 25, - ), - Container(width: 15), - Text( - DrawerTextConstants.settings, - style: TextStyle( - color: - pathForwarding.path.startsWith( - SettingsRouter.root, - ) - ? DrawerColorConstants.selectedText - : DrawerColorConstants.lightText, - fontSize: 15, - ), - ), - ], - ), - ), - ), - const SizedBox(height: 10), - if (isAdmin) - Transform.translate( - offset: Offset(0, -15 * (1 - dropDownAnimation.value)), - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () => onBack(AdminRouter.root), - child: Row( - children: [ - HeroIcon( - HeroIcons.userGroup, - color: - pathForwarding.path.startsWith( - AdminRouter.root, - ) - ? DrawerColorConstants.selectedText - : DrawerColorConstants.lightText, - size: 25, - ), - Container(width: 15), - Text( - DrawerTextConstants.admin, - style: TextStyle( - color: - pathForwarding.path.startsWith( - AdminRouter.root, - ) - ? DrawerColorConstants.selectedText - : DrawerColorConstants.lightText, - fontSize: 15, - ), - ), - Container(width: 25), - ], - ), - ), - ), - ], - ), - ), - ); - }, - animation: dropDownAnimation, - ), - ], - ); - } -} diff --git a/lib/drawer/ui/email_change_popup.dart b/lib/drawer/ui/email_change_popup.dart deleted file mode 100644 index 2344a06dbe..0000000000 --- a/lib/drawer/ui/email_change_popup.dart +++ /dev/null @@ -1,414 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/drawer/providers/already_displayed_popup.dart'; -import 'package:titan/loan/tools/constants.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:titan/tools/providers/should_notify_provider.dart'; -import 'package:titan/tools/ui/builders/waiting_button.dart'; -import 'package:titan/user/providers/user_provider.dart'; - -class Consts { - Consts._(); - - static const double padding = 20.0; - static const double avatarRadius = 50.0; - static const Color greenGradient1 = Color(0xff79a400); - static const Color greenGradient2 = Color(0xff387200); - static const Color redGradient1 = Color(0xFF9E131F); - static const Color redGradient2 = Color(0xFF590512); - static const String description = - "L'administration a décidé de changer les adresses mails des étudiants.\nPour être sur de recevoir les mails en cas de perte du mot de passe, merci de renseigner la nouvelle (normalement elle est déjà préremplie 😉)."; - static const String descriptionMigration = - "Vous avez créé un compte avec une adresse qui n'est pas une adresse centralienne.\nPour pouvoir accéder à cette application, vous devez changer cette adresse (normalement elle est déjà préremplie, on vous laisse vérifier et valider 😉)."; -} - -class EmailChangeDialog extends HookConsumerWidget { - const EmailChangeDialog({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final user = ref.watch(userProvider); - final shouldBeUser = ref.watch(shouldNotifyProvider); - final userNotifier = ref.watch(asyncUserProvider.notifier); - final alreadyDisplayedNotifier = ref.watch( - alreadyDisplayedProvider.notifier, - ); - final newEmail = shouldBeUser - ? '${user.firstname.toLowerCase()}.${user.name.toLowerCase()}@etu.ec-lyon.fr' - : '${user.email.split('@')[0]}@etu.ec-lyon.fr'; - final emailController = useTextEditingController(text: newEmail); - final formKey = GlobalKey(); - final checkAnimationController = useAnimationController( - duration: const Duration(milliseconds: 500), - initialValue: 0, - ); - final checkAnimation = CurvedAnimation( - parent: checkAnimationController, - curve: Curves.bounceOut, - ); - final ValueNotifier currentState = useState( - AsyncError("", StackTrace.current), - ); - final displayForm = useState(true); - - useEffect(() { - if (shouldBeUser) { - emailController.text = newEmail; - } - return () {}; - }, [newEmail]); - - return GestureDetector( - onTap: alreadyDisplayedNotifier.setAlreadyDisplayed, - child: Container( - color: Colors.black54, - child: GestureDetector( - onTap: () {}, - child: Dialog( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(20.0)), - ), - elevation: 0.0, - insetPadding: const EdgeInsets.all(20.0), - backgroundColor: Colors.transparent, - child: Stack( - clipBehavior: Clip.none, - children: [ - Container( - padding: const EdgeInsets.only( - top: Consts.avatarRadius + Consts.padding, - bottom: Consts.padding, - left: Consts.padding, - right: Consts.padding, - ), - margin: const EdgeInsets.only(top: Consts.avatarRadius), - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.rectangle, - borderRadius: BorderRadius.circular(Consts.padding), - boxShadow: const [ - BoxShadow( - color: Colors.black26, - blurRadius: 10.0, - offset: Offset(0.0, 10.0), - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, // To make the card compact - children: [ - const Text( - "Changer d'adresse mail", - style: TextStyle( - fontSize: 24.0, - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(height: 12.0), - displayForm.value - ? Form( - key: formKey, - autovalidateMode: AutovalidateMode.always, - child: Column( - children: [ - Text( - shouldBeUser - ? Consts.descriptionMigration - : Consts.description, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 16.0), - ), - const SizedBox(height: 15.0), - TextFormField( - controller: emailController, - cursorColor: Colors.black, - decoration: const InputDecoration( - labelText: "Nouvelle adresse mail", - floatingLabelStyle: TextStyle( - color: Colors.black, - fontWeight: FontWeight.bold, - ), - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Colors.black, - width: 2.0, - ), - ), - ), - validator: (value) { - if (value == null) { - return LoanTextConstants.noValue; - } else if (value.isEmpty) { - return LoanTextConstants.noValue; - } else if (!isStudent(value)) { - return "Adresse mail invalide"; - } - return null; - }, - ), - const SizedBox(height: 30.0), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - onTap: alreadyDisplayedNotifier - .setAlreadyDisplayed, - child: Container( - width: 100, - padding: const EdgeInsets.symmetric( - vertical: 16, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - 10, - ), - gradient: const LinearGradient( - colors: [ - Colors.black87, - Colors.black, - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues( - alpha: 0.3, - ), - blurRadius: 2.0, - offset: const Offset(1.0, 2.0), - ), - ], - ), - child: const Center( - child: Text( - "Annuler", - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ), - WaitingButton( - onTap: () async { - if (formKey.currentState! - .validate()) { - currentState.value = - const AsyncLoading(); - final result = await userNotifier - .askMailMigration( - emailController.text, - ); - if (result) { - currentState.value = - const AsyncData(""); - checkAnimationController - .forward(); - displayForm.value = false; - } else { - currentState.value = AsyncError( - "Une erreur est survenue", - StackTrace.current, - ); - } - } - }, - waitingColor: Colors.black, - builder: (child) => Container( - width: 100, - padding: const EdgeInsets.symmetric( - vertical: 16, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - 10, - ), - color: Colors.grey.shade300, - boxShadow: [ - BoxShadow( - color: Colors.grey.withValues( - alpha: 0.3, - ), - blurRadius: 2.0, - offset: const Offset(1.0, 2.0), - ), - ], - ), - child: child, - ), - child: const Center( - child: Text( - "Confirmer", - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ], - ), - ], - ), - ) - : Expanded( - child: Center( - child: Column( - children: [ - const Text( - "Un mail de confirmation a été envoyé à l'adresse suivante, pour confirmer le changement :", - textAlign: TextAlign.center, - style: TextStyle(fontSize: 16.0), - ), - const SizedBox(height: 16.0), - Text( - emailController.text, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - ), - const Spacer(), - GestureDetector( - onTap: alreadyDisplayedNotifier - .setAlreadyDisplayed, - child: Container( - width: 100, - padding: const EdgeInsets.symmetric( - vertical: 16, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - 10, - ), - color: Colors.grey.shade300, - boxShadow: [ - BoxShadow( - color: Colors.grey.withValues( - alpha: 0.3, - ), - blurRadius: 2.0, - offset: const Offset(1.0, 2.0), - ), - ], - ), - child: const Center( - child: Text( - "Fermer", - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ), - ], - ), - ), - ), - ], - ), - ), - Positioned( - left: Consts.padding, - right: Consts.padding, - child: currentState.value.when( - data: (data) => AnimatedBuilder( - animation: checkAnimationController, - builder: (context, child) { - return Container( - width: Consts.avatarRadius * 2, - height: Consts.avatarRadius * 2, - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: const LinearGradient( - colors: [ - Consts.greenGradient1, - Consts.greenGradient2, - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - boxShadow: [ - BoxShadow( - color: Consts.greenGradient2.withValues( - alpha: 0.3, - ), - blurRadius: 10.0, - offset: const Offset(0.0, 10.0), - ), - ], - ), - child: Center( - child: Icon( - Icons.check, - color: Colors.white, - size: 60 * checkAnimation.value, - ), - ), - ); - }, - ), - loading: () => Container( - width: Consts.avatarRadius * 2, - height: Consts.avatarRadius * 2, - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: const LinearGradient( - colors: [Consts.redGradient1, Consts.redGradient2], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - boxShadow: [ - BoxShadow( - color: Consts.redGradient2.withValues(alpha: 0.3), - blurRadius: 10.0, - offset: const Offset(0.0, 10.0), - ), - ], - ), - child: const Center( - child: CircularProgressIndicator(color: Colors.white), - ), - ), - error: (error, stack) => Container( - width: Consts.avatarRadius * 2, - height: Consts.avatarRadius * 2, - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: const LinearGradient( - colors: [Consts.redGradient1, Consts.redGradient2], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - boxShadow: [ - BoxShadow( - color: Consts.redGradient2.withValues(alpha: 0.3), - blurRadius: 10.0, - offset: const Offset(0.0, 10.0), - ), - ], - ), - child: const Center( - child: HeroIcon( - HeroIcons.exclamationCircle, - size: 60, - color: Colors.white, - ), - ), - ), - ), - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/drawer/ui/fake_page.dart b/lib/drawer/ui/fake_page.dart deleted file mode 100644 index ba07cf20f5..0000000000 --- a/lib/drawer/ui/fake_page.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:titan/drawer/tools/constants.dart'; - -class FakePage extends StatelessWidget { - const FakePage({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.only(top: 10, bottom: 50), - width: MediaQuery.of(context).size.width - 220, - height: 420, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - boxShadow: const [ - BoxShadow( - color: DrawerColorConstants.fakePageShadow, - spreadRadius: 3, - blurRadius: 5, - offset: Offset(0, 3), - ), - ], - color: DrawerColorConstants.fakePageBlue, - ), - ); - } -} diff --git a/lib/drawer/ui/list_module.dart b/lib/drawer/ui/list_module.dart deleted file mode 100644 index b6f4009165..0000000000 --- a/lib/drawer/ui/list_module.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/drawer/providers/modules_provider.dart'; -import 'package:titan/drawer/ui/module.dart'; - -class ListModule extends HookConsumerWidget { - const ListModule({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final modules = ref.watch(listModuleProvider); - final scrollController = useScrollController(); - return Scrollbar( - controller: scrollController, - interactive: true, - radius: const Radius.circular(8), - thumbVisibility: true, - child: SingleChildScrollView( - controller: scrollController, - physics: const BouncingScrollPhysics(), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: modules.map((module) => ModuleUI(module: module)).toList(), - ), - ), - ); - } -} diff --git a/lib/drawer/ui/module.dart b/lib/drawer/ui/module.dart deleted file mode 100644 index a9b700850b..0000000000 --- a/lib/drawer/ui/module.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/drawer/class/module.dart'; -import 'package:titan/drawer/providers/animation_provider.dart'; -import 'package:titan/drawer/providers/swipe_provider.dart'; -import 'package:titan/drawer/tools/constants.dart'; -import 'package:titan/home/providers/scrolled_provider.dart'; -import 'package:titan/home/router.dart'; -import 'package:titan/tools/providers/path_forwarding_provider.dart'; -import 'package:qlevar_router/qlevar_router.dart'; - -class ModuleUI extends HookConsumerWidget { - const ModuleUI({super.key, required this.module}); - - static Duration duration = const Duration(milliseconds: 200); - final Module module; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final pathForwardingNotifier = ref.read(pathForwardingProvider.notifier); - final pathForwarding = ref.watch(pathForwardingProvider); - final hasScrolled = ref.watch(hasScrolledProvider.notifier); - final animation = ref.watch(animationProvider); - return GestureDetector( - behavior: HitTestBehavior.translucent, - key: ValueKey(module.root), - child: Container( - margin: const EdgeInsets.only(top: 8, bottom: 8), - width: 220, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Row( - children: [ - Container(width: 25), - Center( - child: module.getIcon( - module.root == pathForwarding.path - ? DrawerColorConstants.selectedText - : DrawerColorConstants.lightText, - ), - ), - Container(width: 20), - SizedBox( - height: 50, - child: Center( - child: Text( - module.name, - style: TextStyle( - color: module.root == pathForwarding.path - ? DrawerColorConstants.selectedText - : DrawerColorConstants.lightText, - fontSize: 18, - ), - ), - ), - ), - ], - ), - ], - ), - ), - onTap: () { - QR.to(module.root); - pathForwardingNotifier.forward(module.root); - if (animation != null) { - final controllerNotifier = ref.watch( - swipeControllerProvider(animation).notifier, - ); - controllerNotifier.toggle(); - } - if (pathForwarding.path != HomeRouter.root) { - hasScrolled.setHasScrolled(false); - } - }, - ); - } -} diff --git a/lib/drawer/ui/notification_badge.dart b/lib/drawer/ui/notification_badge.dart deleted file mode 100644 index ee6886cdab..0000000000 --- a/lib/drawer/ui/notification_badge.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:badges/badges.dart' as badges; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/tools/providers/should_notify_provider.dart'; - -class NotificationBadge extends HookConsumerWidget { - final Widget child; - - const NotificationBadge({super.key, required this.child}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final shouldNotify = ref.watch(shouldNotifyProvider); - return badges.Badge( - showBadge: shouldNotify, - position: badges.BadgePosition.topStart(top: -5, start: -10), - badgeStyle: badges.BadgeStyle( - shape: badges.BadgeShape.circle, - badgeGradient: badges.BadgeGradient.linear( - colors: [Colors.red.shade600, Colors.red.shade800], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - elevation: 0, - ), - child: child, - ); - } -} diff --git a/lib/event/providers/is_admin_provider.dart b/lib/event/providers/is_admin_provider.dart index 78f1998e95..393cc60a2b 100644 --- a/lib/event/providers/is_admin_provider.dart +++ b/lib/event/providers/is_admin_provider.dart @@ -5,5 +5,5 @@ final isEventAdminProvider = StateProvider((ref) { final me = ref.watch(userProvider); return me.groups .map((e) => e.id) - .contains("53a669d6-84b1-4352-8d7c-421c1fbd9c6a"); + .contains("b0357687-2211-410a-9e2a-144519eeaafa"); // admin_calendar }); diff --git a/lib/event/router.dart b/lib/event/router.dart index bd9d1656a0..fd655e1deb 100644 --- a/lib/event/router.dart +++ b/lib/event/router.dart @@ -1,7 +1,7 @@ -import 'package:either_dart/either.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:titan/drawer/class/module.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/class/module.dart'; import 'package:titan/event/providers/is_admin_provider.dart'; import 'package:titan/event/ui/pages/detail_page/detail_page.dart' deferred as detail_page; @@ -23,10 +23,10 @@ class EventRouter { static const String addEdit = '/add_edit'; static const String detail = '/detail'; static final Module module = Module( - name: "Évenements", - icon: const Left(HeroIcons.calendar), + getName: (context) => AppLocalizations.of(context)!.moduleEvent, + getDescription: (context) => + AppLocalizations.of(context)!.moduleEventDescription, root: EventRouter.root, - selected: false, ); EventRouter(this.ref); @@ -38,6 +38,10 @@ class EventRouter { AuthenticatedMiddleware(ref), DeferredLoadingMiddleware(main_page.loadLibrary), ], + pageType: QCustomPage( + transitionsBuilder: (_, animation, _, child) => + FadeTransition(opacity: animation, child: child), + ), children: [ QRoute( path: admin, diff --git a/lib/event/tools/constants.dart b/lib/event/tools/constants.dart index cd62124273..9152f84640 100644 --- a/lib/event/tools/constants.dart +++ b/lib/event/tools/constants.dart @@ -1,79 +1,9 @@ -class EventTextConstants { - static const String add = "Ajouter"; - static const String addEvent = "Ajouter un événement"; - static const String addedEvent = "Événement ajouté"; - static const String addingError = "Erreur lors de l'ajout"; - static const String allDay = "Toute la journée"; - static const String confirm = "Confirmer"; - static const String confirmEvent = "Confirmer l'événement ?"; - static const String confirmation = "Confirmation"; - static const String confirmed = "Confirmé"; - static const String dates = "Dates"; - static const String decline = "Refuser"; - static const String declineEvent = "Refuser l'événement ?"; - static const String declined = "Refusé"; - static const String delete = "Supprimer"; - static const String deletedEvent = "Événement supprimé"; - static const String deleting = "Suppression"; - static const String deletingError = "Erreur lors de la suppression"; - static const String deletingEvent = "Supprimer l'événement ?"; - static const String description = "Description"; - static const String edit = "Modifier"; - static const String editEvent = "Modifier un événement"; - static const String editedEvent = "Événement modifié"; - static const String editingError = "Erreur lors de la modification"; - static const String endDate = "Date de fin"; - static const String endHour = "Heure de fin"; - static const String error = "Erreur"; - static const String eventList = "Liste des événements"; - static const String eventType = "Type d'événement"; - static const String every = "Tous les"; - static const String history = "Historique"; - static const String incorrectOrMissingFields = - "Certains champs sont incorrects ou manquants"; - static const String interval = "Intervalle"; - static const String invalidDates = - "La date de fin doit être après la date de début"; - static const String invalidIntervalError = - "Veuillez entrer un intervalle valide"; - static const String location = "Lieu"; - static const String myEvents = "Mes événements"; - static const String name = "Nom"; - static const String next = "Suivant"; - static const String no = "Non"; - static const String noCurrentEvent = "Aucun événement en cours"; - static const String noDateError = "Veuillez entrer une date"; - static const String noDaySelected = "Aucun jour sélectionné"; - static const String noDescriptionError = "Veuillez entrer une description"; - static const String noEvent = "Aucun événement"; - static const String noNameError = "Veuillez entrer un nom"; - static const String noOrganizerError = "Veuillez entrer un organisateur"; - static const String noPlaceError = "Veuillez entrer un lieu"; - static const String noPhoneRegistered = "Numéro non renseigné"; - static const String noRuleError = "Veuillez entrer une règle de récurrence"; - static const String organizer = "Organisateur"; - static const String other = "Autre"; - static const String pending = "En attente"; - static const String previous = "Précédent"; - static const String recurrence = "Récurrence"; - static const String recurrenceDays = "Jours de récurrence"; - static const String recurrenceEndDate = "Date de fin de la récurrence"; - static const String recurrenceRule = "Règle de récurrence"; - static const String room = "Salle"; - static const String startDate = "Date de début"; - static const String startHour = "Heure de début"; - static const String title = "Événements"; - static const String yes = "Oui"; - static const String eventEvery = "Toutes les"; - static const String weeks = "semaines"; - - static const List dayList = [ - 'Lundi', - 'Mardi', - 'Mercredi', - 'Jeudi', - 'Vendredi', - 'Samedi', - 'Dimanche', - ]; -} +const eventDayKeys = [ + 'eventDayMon', + 'eventDayTue', + 'eventDayWed', + 'eventDayThu', + 'eventDayFri', + 'eventDaySat', + 'eventDaySun', +]; diff --git a/lib/event/tools/functions.dart b/lib/event/tools/functions.dart index d74bfa5ff3..b0c1d65d2c 100644 --- a/lib/event/tools/functions.dart +++ b/lib/event/tools/functions.dart @@ -1,17 +1,18 @@ import 'dart:math'; +import 'package:flutter/material.dart'; import 'package:titan/event/class/event.dart'; -import 'package:titan/event/tools/constants.dart'; import 'package:titan/tools/functions.dart'; +import 'package:titan/l10n/app_localizations.dart'; -String decisionToString(Decision d) { +String decisionToString(Decision d, BuildContext context) { switch (d) { case Decision.approved: - return EventTextConstants.confirmed; + return AppLocalizations.of(context)!.eventConfirmed; case Decision.declined: - return EventTextConstants.declined; + return AppLocalizations.of(context)!.eventDeclined; case Decision.pending: - return EventTextConstants.pending; + return AppLocalizations.of(context)!.eventPending; } } @@ -80,3 +81,25 @@ String formatDelayToToday(DateTime date, DateTime now) { } return "Dans ${date.year - now.year} ans"; } + +String getLocalizedEventDay(BuildContext context, String key) { + final loc = AppLocalizations.of(context)!; + switch (key) { + case 'eventDayMon': + return loc.eventDayMon; + case 'eventDayTue': + return loc.eventDayTue; + case 'eventDayWed': + return loc.eventDayWed; + case 'eventDayThu': + return loc.eventDayThu; + case 'eventDayFri': + return loc.eventDayFri; + case 'eventDaySat': + return loc.eventDaySat; + case 'eventDaySun': + return loc.eventDaySun; + default: + return key; + } +} diff --git a/lib/event/ui/components/event_ui.dart b/lib/event/ui/components/event_ui.dart index f5f0aafc92..3e57c99b7b 100644 --- a/lib/event/ui/components/event_ui.dart +++ b/lib/event/ui/components/event_ui.dart @@ -6,7 +6,6 @@ import 'package:titan/event/class/event.dart'; import 'package:titan/event/providers/event_provider.dart'; import 'package:titan/event/providers/user_event_list_provider.dart'; import 'package:titan/event/router.dart'; -import 'package:titan/event/tools/constants.dart'; import 'package:titan/event/tools/functions.dart'; import 'package:titan/event/ui/components/edit_delete_button.dart'; import 'package:titan/tools/constants.dart'; @@ -15,6 +14,7 @@ import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class EventUi extends ConsumerWidget { final Event event; @@ -34,6 +34,7 @@ class EventUi extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final locale = Localizations.localeOf(context); final now = DateTime.now(); final eventListNotifier = ref.watch(eventEventListProvider.notifier); final eventNotifier = ref.watch(eventProvider.notifier); @@ -127,6 +128,7 @@ class EventUi extends ConsumerWidget { event.end, event.recurrenceRule, event.allDay, + locale.toString(), ), style: TextStyle( color: textColor.withValues(alpha: 0.7), @@ -175,7 +177,7 @@ class EventUi extends ConsumerWidget { Align( alignment: Alignment.center, child: Text( - decisionToString(event.decision), + decisionToString(event.decision, context), overflow: TextOverflow.ellipsis, style: TextStyle( color: textColor, @@ -209,7 +211,7 @@ class EventUi extends ConsumerWidget { : Colors.grey.shade300, child: Center( child: Text( - EventTextConstants.edit, + AppLocalizations.of(context)!.eventEdit, style: TextStyle( color: textColor, fontSize: 15, @@ -228,24 +230,35 @@ class EventUi extends ConsumerWidget { context: context, builder: (BuildContext context) { return CustomDialogBox( - descriptions: - EventTextConstants.deletingEvent, + descriptions: AppLocalizations.of( + context, + )!.eventDeletingEvent, onYes: () async { + final deletedEventMsg = + AppLocalizations.of( + context, + )!.eventDeletedEvent; + final deletingErrorMsg = + AppLocalizations.of( + context, + )!.eventDeletingError; final value = await eventListNotifier .deleteEvent(event); if (value) { displayToastWithContext( TypeMsg.msg, - EventTextConstants.deletedEvent, + deletedEventMsg, ); } else { displayToastWithContext( TypeMsg.error, - EventTextConstants.deletingError, + deletingErrorMsg, ); } }, - title: EventTextConstants.deleting, + title: AppLocalizations.of( + context, + )!.eventDeleting, ); }, ); @@ -265,7 +278,7 @@ class EventUi extends ConsumerWidget { ), child: Center( child: Text( - EventTextConstants.delete, + AppLocalizations.of(context)!.eventDelete, style: TextStyle( color: textColor, fontSize: 15, diff --git a/lib/event/ui/event.dart b/lib/event/ui/event.dart index 7a1a7f962b..b8448f5ed8 100644 --- a/lib/event/ui/event.dart +++ b/lib/event/ui/event.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:titan/event/router.dart'; -import 'package:titan/event/tools/constants.dart'; import 'package:titan/tools/ui/widgets/top_bar.dart'; +import 'package:titan/tools/constants.dart'; class EventTemplate extends StatelessWidget { final Widget child; @@ -9,13 +9,18 @@ class EventTemplate extends StatelessWidget { @override Widget build(BuildContext context) { - return SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const TopBar(title: EventTextConstants.title, root: EventRouter.root), - Expanded(child: child), - ], + return Scaffold( + body: Container( + decoration: const BoxDecoration(color: ColorConstants.background), + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const TopBar(root: EventRouter.root), + Expanded(child: child), + ], + ), + ), ), ); } diff --git a/lib/event/ui/pages/admin_page/admin_page.dart b/lib/event/ui/pages/admin_page/admin_page.dart index e3d3e9df18..1f5682029e 100644 --- a/lib/event/ui/pages/admin_page/admin_page.dart +++ b/lib/event/ui/pages/admin_page/admin_page.dart @@ -3,12 +3,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/event/ui/event.dart'; import 'package:titan/event/class/event.dart'; import 'package:titan/event/providers/event_list_provider.dart'; -import 'package:titan/event/tools/constants.dart'; import 'package:titan/event/ui/pages/admin_page/list_event.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/ui/layouts/refresher.dart'; import 'package:titan/tools/ui/widgets/calendar.dart'; import 'package:syncfusion_flutter_calendar/calendar.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AdminPage extends HookConsumerWidget { const AdminPage({super.key}); @@ -73,6 +73,7 @@ class AdminPage extends HookConsumerWidget { }).toList(); return EventTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await ref.watch(eventListProvider.notifier).loadEventList(); }, @@ -90,10 +91,10 @@ class AdminPage extends HookConsumerWidget { if (pendingEvents.isEmpty && confirmedEvents.isEmpty && canceledEvents.isEmpty) - const Center( + Center( child: Text( - EventTextConstants.noCurrentEvent, - style: TextStyle( + AppLocalizations.of(context)!.eventNoCurrentEvent, + style: const TextStyle( color: Colors.black, fontSize: 20, fontWeight: FontWeight.bold, @@ -101,20 +102,20 @@ class AdminPage extends HookConsumerWidget { ), ), ListEvent( - title: EventTextConstants.pending, + title: AppLocalizations.of(context)!.eventPending, events: pendingEvents, canToggle: false, ), ListEvent( - title: EventTextConstants.confirmed, + title: AppLocalizations.of(context)!.eventConfirmed, events: confirmedEvents, ), ListEvent( - title: EventTextConstants.declined, + title: AppLocalizations.of(context)!.eventDeclined, events: canceledEvents, ), ListEvent( - title: EventTextConstants.history, + title: AppLocalizations.of(context)!.eventHistory, events: confirmedEvents + canceledEvents + pendingEvents, isHistory: true, ), diff --git a/lib/event/ui/pages/admin_page/list_event.dart b/lib/event/ui/pages/admin_page/list_event.dart index 3a5cdffd9b..7022e09598 100644 --- a/lib/event/ui/pages/admin_page/list_event.dart +++ b/lib/event/ui/pages/admin_page/list_event.dart @@ -7,7 +7,6 @@ import 'package:titan/event/providers/confirmed_event_list_provider.dart'; import 'package:titan/event/providers/event_list_provider.dart'; import 'package:titan/event/providers/event_provider.dart'; import 'package:titan/event/router.dart'; -import 'package:titan/event/tools/constants.dart'; import 'package:titan/event/ui/components/event_ui.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; @@ -15,6 +14,7 @@ import 'package:titan/tools/ui/widgets/align_left_text.dart'; import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class ListEvent extends HookConsumerWidget { final List events; @@ -102,8 +102,10 @@ class ListEvent extends HookConsumerWidget { context: context, builder: (context) { return CustomDialogBox( - title: EventTextConstants.confirm, - descriptions: EventTextConstants.confirmEvent, + title: AppLocalizations.of(context)!.eventConfirm, + descriptions: AppLocalizations.of( + context, + )!.eventConfirmEvent, onYes: () async { await tokenExpireWrapper(ref, () async { eventListNotifier @@ -126,8 +128,10 @@ class ListEvent extends HookConsumerWidget { context: context, builder: (context) { return CustomDialogBox( - title: EventTextConstants.decline, - descriptions: EventTextConstants.declineEvent, + title: AppLocalizations.of(context)!.eventDecline, + descriptions: AppLocalizations.of( + context, + )!.eventDeclineEvent, onYes: () async { await tokenExpireWrapper(ref, () async { eventListNotifier diff --git a/lib/event/ui/pages/detail_page/detail_page.dart b/lib/event/ui/pages/detail_page/detail_page.dart index d49673500a..b7f0dc08f0 100644 --- a/lib/event/ui/pages/detail_page/detail_page.dart +++ b/lib/event/ui/pages/detail_page/detail_page.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/event/providers/event_provider.dart'; -import 'package:titan/event/tools/constants.dart'; import 'package:titan/event/ui/event.dart'; import 'package:titan/event/ui/components/event_ui.dart'; import 'package:titan/tools/functions.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:titan/l10n/app_localizations.dart'; class DetailPage extends HookConsumerWidget { final bool isAdmin; @@ -97,7 +97,9 @@ class DetailPage extends HookConsumerWidget { const SizedBox(height: 30), Text( event.applicant.phone ?? - EventTextConstants.noPhoneRegistered, + AppLocalizations.of( + context, + )!.eventNoPhoneRegistered, style: const TextStyle(fontSize: 25), ), const SizedBox(height: 50), diff --git a/lib/event/ui/pages/event_pages/add_edit_event_page.dart b/lib/event/ui/pages/event_pages/add_edit_event_page.dart index 27c8055eb3..ea66e974aa 100644 --- a/lib/event/ui/pages/event_pages/add_edit_event_page.dart +++ b/lib/event/ui/pages/event_pages/add_edit_event_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:titan/service/providers/room_list_provider.dart'; import 'package:titan/event/ui/event.dart'; import 'package:titan/event/ui/pages/event_pages/checkbox_entry.dart'; @@ -23,6 +24,7 @@ import 'package:titan/tools/ui/widgets/text_entry.dart'; import 'package:titan/user/providers/user_provider.dart'; import 'package:qlevar_router/qlevar_router.dart'; import 'package:syncfusion_flutter_calendar/calendar.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AddEditEventPage extends HookConsumerWidget { final eventTypeScrollKey = GlobalKey(); @@ -31,6 +33,7 @@ class AddEditEventPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final locale = Localizations.localeOf(context); final now = DateTime.now(); final user = ref.watch(userProvider); final event = ref.watch(eventProvider); @@ -59,8 +62,8 @@ class AddEditEventPage extends HookConsumerWidget { ? "" : processDateOnlyHour(event.start) : allDay.value - ? processDate(event.start) - : processDateWithHour(event.start) + ? DateFormat.yMd(locale).format(event.start) + : DateFormat.yMd(locale).add_Hm().format(event.start) : "", ); final end = useTextEditingController( @@ -70,8 +73,8 @@ class AddEditEventPage extends HookConsumerWidget { ? "" : processDateOnlyHour(event.end) : allDay.value - ? processDate(event.end) - : processDateWithHour(event.end) + ? DateFormat.yMd(locale).format(event.end) + : DateFormat.yMd(locale).add_Hm().format(event.end) : "", ); final interval = useTextEditingController( @@ -81,7 +84,7 @@ class AddEditEventPage extends HookConsumerWidget { ); final recurrenceEndDate = useTextEditingController( text: event.recurrenceRule != "" - ? processDate( + ? DateFormat.yMd(locale).format( DateTime.parse( event.recurrenceRule.split(";UNTIL=")[1].split(";")[0], ), @@ -105,8 +108,8 @@ class AddEditEventPage extends HookConsumerWidget { const SizedBox(height: 40), AlignLeftText( isEdit - ? EventTextConstants.editEvent - : EventTextConstants.addEvent, + ? AppLocalizations.of(context)!.eventEditEvent + : AppLocalizations.of(context)!.eventAddEvent, padding: const EdgeInsets.symmetric(horizontal: 30), color: Colors.grey, ), @@ -141,16 +144,16 @@ class AddEditEventPage extends HookConsumerWidget { children: [ TextEntry( controller: name, - label: EventTextConstants.name, + label: AppLocalizations.of(context)!.eventName, ), const SizedBox(height: 30), TextEntry( controller: organizer, - label: EventTextConstants.organizer, + label: AppLocalizations.of(context)!.eventOrganizer, ), const SizedBox(height: 30), CheckBoxEntry( - title: EventTextConstants.recurrence, + title: AppLocalizations.of(context)!.eventRecurrence, valueNotifier: recurrent, onChanged: () { start.text = ""; @@ -160,7 +163,7 @@ class AddEditEventPage extends HookConsumerWidget { ), const SizedBox(height: 20), CheckBoxEntry( - title: EventTextConstants.allDay, + title: AppLocalizations.of(context)!.eventAllDay, valueNotifier: allDay, onChanged: () { start.text = ""; @@ -174,21 +177,33 @@ class AddEditEventPage extends HookConsumerWidget { children: [ Column( children: [ - const Text( - EventTextConstants.recurrenceDays, - style: TextStyle(color: Colors.black), + Text( + AppLocalizations.of( + context, + )!.eventRecurrenceDays, + style: const TextStyle( + color: Colors.black, + ), ), const SizedBox(height: 10), Column( - children: EventTextConstants.dayList - .map( - (e) => GestureDetector( - onTap: () { - selectedDaysNotifier.toggle( - EventTextConstants.dayList - .indexOf(e), + children: eventDayKeys + .asMap() + .entries + .map((entry) { + final index = entry.key; + final key = entry.value; + final localizedLabel = + getLocalizedEventDay( + context, + key, ); - }, + + return GestureDetector( + onTap: () => + selectedDaysNotifier.toggle( + index, + ), behavior: HitTestBehavior.opaque, child: Row( @@ -197,7 +212,7 @@ class AddEditEventPage extends HookConsumerWidget { .spaceBetween, children: [ Text( - e, + localizedLabel, style: TextStyle( color: Colors .grey @@ -209,35 +224,38 @@ class AddEditEventPage extends HookConsumerWidget { checkColor: Colors.white, activeColor: Colors.black, value: - selectedDays[EventTextConstants - .dayList - .indexOf(e)], - onChanged: (value) { - selectedDaysNotifier - .toggle( - EventTextConstants - .dayList - .indexOf(e), - ); - }, + selectedDays[index], + onChanged: (_) => + selectedDaysNotifier + .toggle(index), ), ], ), - ), - ) + ); + }) .toList(), ), const SizedBox(height: 20), - const Text( - EventTextConstants.interval, - style: TextStyle(color: Colors.black), + Text( + AppLocalizations.of( + context, + )!.eventInterval, + style: const TextStyle( + color: Colors.black, + ), ), const SizedBox(height: 10), TextEntry( - label: EventTextConstants.interval, + label: AppLocalizations.of( + context, + )!.eventInterval, controller: interval, - prefix: EventTextConstants.eventEvery, - suffix: EventTextConstants.weeks, + prefix: AppLocalizations.of( + context, + )!.eventEventEvery, + suffix: AppLocalizations.of( + context, + )!.eventWeeks, isInt: true, keyboardType: TextInputType.number, ), @@ -251,15 +269,18 @@ class AddEditEventPage extends HookConsumerWidget { start, ), controller: start, - label: - EventTextConstants.startHour, + label: AppLocalizations.of( + context, + )!.eventStartHour, ), const SizedBox(height: 30), DateEntry( onTap: () => getOnlyHourDate(context, end), controller: end, - label: EventTextConstants.endHour, + label: AppLocalizations.of( + context, + )!.eventEndHour, ), const SizedBox(height: 30), ], @@ -270,8 +291,9 @@ class AddEditEventPage extends HookConsumerWidget { recurrenceEndDate, ), controller: recurrenceEndDate, - label: EventTextConstants - .recurrenceEndDate, + label: AppLocalizations.of( + context, + )!.eventRecurrenceEndDate, ), ], ), @@ -284,7 +306,9 @@ class AddEditEventPage extends HookConsumerWidget { ? getOnlyDayDate(context, start) : getFullDate(context, start), controller: start, - label: EventTextConstants.startDate, + label: AppLocalizations.of( + context, + )!.eventStartDate, ), const SizedBox(height: 30), DateEntry( @@ -292,7 +316,9 @@ class AddEditEventPage extends HookConsumerWidget { ? getOnlyDayDate(context, end) : getFullDate(context, end), controller: end, - label: EventTextConstants.endDate, + label: AppLocalizations.of( + context, + )!.eventEndDate, ), ], ), @@ -309,7 +335,7 @@ class AddEditEventPage extends HookConsumerWidget { }, selected: isRoom.value, child: Text( - EventTextConstants.room, + AppLocalizations.of(context)!.eventRoom, style: TextStyle( color: isRoom.value ? Colors.white : Colors.black, fontWeight: FontWeight.bold, @@ -322,7 +348,7 @@ class AddEditEventPage extends HookConsumerWidget { }, selected: !isRoom.value, child: Text( - EventTextConstants.other, + AppLocalizations.of(context)!.eventOther, style: TextStyle( color: isRoom.value ? Colors.black : Colors.white, fontWeight: FontWeight.bold, @@ -369,7 +395,7 @@ class AddEditEventPage extends HookConsumerWidget { padding: const EdgeInsets.symmetric(horizontal: 30), child: TextEntry( controller: location, - label: EventTextConstants.location, + label: AppLocalizations.of(context)!.eventLocation, ), ), const SizedBox(height: 30), @@ -379,7 +405,7 @@ class AddEditEventPage extends HookConsumerWidget { children: [ TextEntry( controller: description, - label: EventTextConstants.description, + label: AppLocalizations.of(context)!.eventDescription, keyboardType: TextInputType.multiline, ), const SizedBox(height: 50), @@ -389,6 +415,18 @@ class AddEditEventPage extends HookConsumerWidget { if (key.currentState == null) { return; } + final editedEventMsg = AppLocalizations.of( + context, + )!.eventEditedEvent; + final addedEventMsg = AppLocalizations.of( + context, + )!.eventAddedEvent; + final editingErrorMsg = AppLocalizations.of( + context, + )!.eventEditingError; + final addingErrorMsg = AppLocalizations.of( + context, + )!.eventAddingError; if (key.currentState!.validate()) { if (allDay.value) { start.text = @@ -398,13 +436,21 @@ class AddEditEventPage extends HookConsumerWidget { } if (end.text.contains("/") && isDateBefore( - processDateBack(end.text), - processDateBack(start.text), + processDateBack( + end.text, + locale.toString(), + ), + processDateBack( + start.text, + locale.toString(), + ), )) { displayToast( context, TypeMsg.error, - EventTextConstants.invalidDates, + AppLocalizations.of( + context, + )!.eventInvalidDates, ); } else if (recurrent.value && selectedDays @@ -413,7 +459,9 @@ class AddEditEventPage extends HookConsumerWidget { displayToast( context, TypeMsg.error, - EventTextConstants.noDaySelected, + AppLocalizations.of( + context, + )!.eventNoDaySelected, ); } else { await tokenExpireWrapper(ref, () async { @@ -421,12 +469,12 @@ class AddEditEventPage extends HookConsumerWidget { String startString = start.text; if (!startString.contains("/")) { startString = - "${processDate(now)} $startString"; + "${DateFormat.yMd(locale).format(now)} $startString"; } String endString = end.text; if (!endString.contains("/")) { endString = - "${processDate(now)} $endString"; + "${DateFormat.yMd(locale).format(now)} $endString"; } if (recurrent.value) { RecurrenceProperties recurrence = @@ -436,7 +484,10 @@ class AddEditEventPage extends HookConsumerWidget { recurrence.recurrenceRange = RecurrenceRange.endDate; recurrence.endDate = DateTime.parse( - processDateBack(recurrenceEndDate.text), + processDateBack( + recurrenceEndDate.text, + locale.toString(), + ), ); recurrence.weekDays = WeekDays.values .where( @@ -453,10 +504,16 @@ class AddEditEventPage extends HookConsumerWidget { recurrenceRule = SfCalendar.generateRRule( recurrence, DateTime.parse( - processDateBackWithHour(startString), + processDateBackWithHour( + startString, + locale.toString(), + ), ), DateTime.parse( - processDateBackWithHour(endString), + processDateBackWithHour( + endString, + locale.toString(), + ), ), ); } @@ -464,14 +521,20 @@ class AddEditEventPage extends HookConsumerWidget { id: isEdit ? event.id : "", description: description.text, end: DateTime.parse( - processDateBack(endString), + processDateBack( + endString, + locale.toString(), + ), ), name: name.text, organizer: organizer.text, allDay: allDay.value, location: location.text, start: DateTime.parse( - processDateBack(startString), + processDateBack( + startString, + locale.toString(), + ), ), type: eventType.value, recurrenceRule: recurrenceRule, @@ -491,24 +554,24 @@ class AddEditEventPage extends HookConsumerWidget { if (isEdit) { displayToastWithContext( TypeMsg.msg, - EventTextConstants.editedEvent, + editedEventMsg, ); } else { displayToastWithContext( TypeMsg.msg, - EventTextConstants.addedEvent, + addedEventMsg, ); } } else { if (isEdit) { displayToastWithContext( TypeMsg.error, - EventTextConstants.editingError, + editingErrorMsg, ); } else { displayToastWithContext( TypeMsg.error, - EventTextConstants.addingError, + addingErrorMsg, ); } } @@ -518,8 +581,8 @@ class AddEditEventPage extends HookConsumerWidget { }, child: Text( isEdit - ? EventTextConstants.edit - : EventTextConstants.add, + ? AppLocalizations.of(context)!.eventEdit + : AppLocalizations.of(context)!.eventAdd, style: const TextStyle( color: Colors.white, fontSize: 25, diff --git a/lib/event/ui/pages/event_pages/checkbox_entry.dart b/lib/event/ui/pages/event_pages/checkbox_entry.dart index 88d70206ca..118b51c6c1 100644 --- a/lib/event/ui/pages/event_pages/checkbox_entry.dart +++ b/lib/event/ui/pages/event_pages/checkbox_entry.dart @@ -30,7 +30,6 @@ class CheckBoxEntry extends StatelessWidget { activeColor: Colors.black, value: valueNotifier.value, onChanged: (value) { - valueNotifier.value = value!; onChanged(); }, ), diff --git a/lib/event/ui/pages/main_page/main_page.dart b/lib/event/ui/pages/main_page/main_page.dart index 384ef3df81..7b6a6898ad 100644 --- a/lib/event/ui/pages/main_page/main_page.dart +++ b/lib/event/ui/pages/main_page/main_page.dart @@ -6,7 +6,6 @@ import 'package:titan/event/providers/event_provider.dart'; import 'package:titan/event/providers/is_admin_provider.dart'; import 'package:titan/event/providers/user_event_list_provider.dart'; import 'package:titan/event/router.dart'; -import 'package:titan/event/tools/constants.dart'; import 'package:titan/event/ui/event.dart'; import 'package:titan/event/ui/components/event_ui.dart'; import 'package:titan/tools/ui/layouts/column_refresher.dart'; @@ -14,6 +13,7 @@ import 'package:titan/tools/ui/widgets/admin_button.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/layouts/card_layout.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class EventMainPage extends HookConsumerWidget { const EventMainPage({super.key}); @@ -30,6 +30,7 @@ class EventMainPage extends HookConsumerWidget { builder: (context, eventList) { eventList.sort((a, b) => b.start.compareTo(a.start)); return ColumnRefresher( + controller: ScrollController(), onRefresh: () async { await eventListNotifier.loadConfirmedEvent(); }, @@ -44,8 +45,8 @@ class EventMainPage extends HookConsumerWidget { children: [ Text( eventList.isEmpty - ? EventTextConstants.noEvent - : EventTextConstants.myEvents, + ? AppLocalizations.of(context)!.eventNoEvent + : AppLocalizations.of(context)!.eventMyEvents, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, diff --git a/lib/feed/class/event.dart b/lib/feed/class/event.dart new file mode 100644 index 0000000000..ce4a655f21 --- /dev/null +++ b/lib/feed/class/event.dart @@ -0,0 +1,112 @@ +import 'package:titan/tools/functions.dart'; + +class Event { + late final String id; + late final String name; + late final DateTime start; + late final DateTime end; + late final bool allDay; + late final String location; + late final String recurrenceRule; + late final DateTime? ticketUrlOpening; + late final String associationId; + late final String? ticketUrl; + late final bool notification; + + Event({ + required this.id, + required this.name, + required this.start, + required this.end, + required this.allDay, + required this.location, + required this.recurrenceRule, + this.ticketUrlOpening, + required this.associationId, + this.ticketUrl, + required this.notification, + }); + + Event.fromJson(Map json) { + id = json['id']; + name = json['name']; + start = processDateFromAPI(json['start']); + end = processDateFromAPI(json['end']); + allDay = json['all_day']; + location = json['location']; + recurrenceRule = json['recurrence_rule'] ?? ""; + ticketUrlOpening = json['ticket_url_opening'] != null + ? processDateFromAPI(json['ticket_url_opening']) + : null; + associationId = json['association_id']; + ticketUrl = json['ticket_url']; + notification = json['notification'] ?? true; + } + + Map toJson() { + final Map data = {}; + + data['name'] = name; + data['start'] = processDateToAPI(start); + data['end'] = processDateToAPI(end); + data['all_day'] = allDay; + data['location'] = location; + data['recurrence_rule'] = recurrenceRule; + if (ticketUrlOpening != null) { + data['ticket_url_opening'] = processDateToAPI(ticketUrlOpening!); + } + data['association_id'] = associationId; + if (ticketUrl != null) { + data['ticket_url'] = ticketUrl; + } + data['notification'] = notification; + return data; + } + + Event copyWith({ + String? name, + DateTime? start, + DateTime? end, + String? location, + bool? allDay, + String? recurrenceRule, + DateTime? ticketUrlOpening, + String? associationId, + String? ticketUrl, + bool? hasRoom, + bool? notification, + }) { + return Event( + id: id, + name: name ?? this.name, + start: start ?? this.start, + end: end ?? this.end, + location: location ?? this.location, + recurrenceRule: recurrenceRule ?? this.recurrenceRule, + allDay: allDay ?? this.allDay, + ticketUrlOpening: ticketUrlOpening ?? this.ticketUrlOpening, + associationId: associationId ?? this.associationId, + ticketUrl: ticketUrl ?? this.ticketUrl, + notification: notification ?? this.notification, + ); + } + + Event.empty() { + id = ''; + name = ''; + start = DateTime.now(); + end = DateTime.now(); + allDay = false; + location = ''; + recurrenceRule = ''; + ticketUrlOpening = null; + associationId = ''; + ticketUrl = null; + notification = true; + } + + @override + String toString() { + return 'Event{name: $name, start: $start, end: $end, allDay: $allDay, location: $location, recurrenceRule: $recurrenceRule, ticketUrlOpening: $ticketUrlOpening, associationId: $associationId, ticketUrl: $ticketUrl, notification: $notification}'; + } +} diff --git a/lib/feed/class/filter_state.dart b/lib/feed/class/filter_state.dart new file mode 100644 index 0000000000..c430e711da --- /dev/null +++ b/lib/feed/class/filter_state.dart @@ -0,0 +1,20 @@ +class FilterState { + final List selectedEntities; + final List selectedModules; + + FilterState({required this.selectedEntities, required this.selectedModules}); + + FilterState copyWith({ + List? selectedEntities, + List? selectedModules, + }) { + return FilterState( + selectedEntities: selectedEntities ?? this.selectedEntities, + selectedModules: selectedModules ?? this.selectedModules, + ); + } + + factory FilterState.empty() { + return FilterState(selectedEntities: [], selectedModules: []); + } +} diff --git a/lib/feed/class/news.dart b/lib/feed/class/news.dart new file mode 100644 index 0000000000..a7d8176007 --- /dev/null +++ b/lib/feed/class/news.dart @@ -0,0 +1,102 @@ +import 'package:titan/feed/tools/function.dart'; +import 'package:titan/tools/functions.dart'; + +class News { + final String id; + final String title; + final DateTime start; + final DateTime? end; + final String entity; + final String? location; + final DateTime? actionStart; + final String module; + final String moduleObjectId; + final NewsStatus status; + + const News({ + required this.id, + required this.title, + required this.start, + this.end, + required this.entity, + this.location, + this.actionStart, + required this.module, + required this.moduleObjectId, + required this.status, + }); + + News.fromJson(Map json) + : id = json['id'], + title = json['title'], + start = processDateFromAPI(json['start']), + end = json['end'] != null ? processDateFromAPI(json['end']) : null, + entity = json['entity'], + location = json['location'], + actionStart = json['action_start'] != null + ? processDateFromAPI(json['action_start']) + : null, + module = json['module'], + moduleObjectId = json['module_object_id'], + status = stringToNewsStatus(json['status']); + + Map toJson() { + return { + 'id': id, + 'title': title, + 'start': processDateToAPI(start), + 'end': end != null ? processDateToAPI(end!) : null, + 'entity': entity, + 'location': location, + 'action_start': actionStart != null + ? processDateToAPI(actionStart!) + : null, + 'module': module, + 'module_object_id': moduleObjectId, + 'status': status.toString().split('.').last, + }; + } + + News copyWith({ + String? id, + String? title, + DateTime? start, + DateTime? end, + String? entity, + String? location, + DateTime? actionStart, + String? module, + String? moduleObjectId, + NewsStatus? status, + }) { + return News( + id: id ?? this.id, + title: title ?? this.title, + start: start ?? this.start, + end: end ?? this.end, + entity: entity ?? this.entity, + location: location ?? this.location, + actionStart: actionStart ?? this.actionStart, + module: module ?? this.module, + moduleObjectId: moduleObjectId ?? this.moduleObjectId, + status: status ?? this.status, + ); + } + + @override + String toString() { + return 'News(id: $id, title: $title, start: $start, end: $end, entity: $entity, location: $location, actionStart: $actionStart, module: $module, moduleObjectId: $moduleObjectId, status: $status)'; + } + + News.empty() + : id = '', + title = '', + start = DateTime.now(), + end = null, + entity = '', + location = null, + actionStart = null, + module = '', + moduleObjectId = '', + status = NewsStatus.waitingApproval; +} diff --git a/lib/feed/class/ticket_url.dart b/lib/feed/class/ticket_url.dart new file mode 100644 index 0000000000..f9c7842736 --- /dev/null +++ b/lib/feed/class/ticket_url.dart @@ -0,0 +1,27 @@ +class TicketUrl { + late final String ticketUrl; + TicketUrl({required this.ticketUrl}); + + TicketUrl.fromJson(Map json) { + ticketUrl = json['ticket_url']; + } + + Map toJson() { + final Map data = {}; + data['ticket_url'] = ticketUrl; + return data; + } + + TicketUrl copyWith({String? ticketUrl}) { + return TicketUrl(ticketUrl: ticketUrl ?? this.ticketUrl); + } + + TicketUrl.empty() { + ticketUrl = ''; + } + + @override + String toString() { + return 'TicketUrl{ticketUrl: $ticketUrl}'; + } +} diff --git a/lib/feed/providers/admin_news_list_provider.dart b/lib/feed/providers/admin_news_list_provider.dart new file mode 100644 index 0000000000..33561aa404 --- /dev/null +++ b/lib/feed/providers/admin_news_list_provider.dart @@ -0,0 +1,47 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/auth/providers/openid_provider.dart'; +import 'package:titan/feed/class/news.dart'; +import 'package:titan/feed/repositories/news_repository.dart'; +import 'package:titan/tools/providers/list_notifier.dart'; + +class AdminNewsListNotifier extends ListNotifier { + final NewsRepository newsRepository; + AdminNewsListNotifier({required this.newsRepository}) + : super(const AsyncValue.loading()); + + Future>> loadNewsList() async { + return await loadList(newsRepository.getAllNews); + } + + Future addNews(News news) async { + return await add(newsRepository.createNews, news); + } + + Future approveNews(News news) async { + return await update( + (news) => newsRepository.approveNews(news.id), + (newsList, news) => + newsList..[newsList.indexWhere((d) => d.id == news.id)] = news, + news, + ); + } + + Future rejectNews(News news) async { + return await update( + (news) => newsRepository.rejectNews(news.id), + (newsList, news) => + newsList..[newsList.indexWhere((d) => d.id == news.id)] = news, + news, + ); + } +} + +final adminNewsListProvider = + StateNotifierProvider>>((ref) { + final token = ref.watch(tokenProvider); + final newsRepository = NewsRepository()..setToken(token); + AdminNewsListNotifier newsListNotifier = AdminNewsListNotifier( + newsRepository: newsRepository, + )..loadNewsList(); + return newsListNotifier; + }); diff --git a/lib/feed/providers/association_event_list_provider.dart b/lib/feed/providers/association_event_list_provider.dart new file mode 100644 index 0000000000..cbaf277317 --- /dev/null +++ b/lib/feed/providers/association_event_list_provider.dart @@ -0,0 +1,49 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/auth/providers/openid_provider.dart'; +import 'package:titan/feed/class/event.dart'; +import 'package:titan/feed/repositories/event_repository.dart'; +import 'package:titan/tools/providers/list_notifier.dart'; + +class AssociationEventsListNotifier extends ListNotifier { + final EventRepository eventsRepository; + AsyncValue> allNews = const AsyncValue.loading(); + AssociationEventsListNotifier({required this.eventsRepository}) + : super(const AsyncValue.loading()); + + Future>> loadAssociationEventList( + String associationId, + ) async { + return allNews = await loadList( + () => eventsRepository.getAssociationEventList(associationId), + ); + } + + Future updateEvent(Event event) async { + return await update( + (event) => eventsRepository.updateEvent(event), + (eventList, event) => + eventList..[eventList.indexWhere((d) => d.id == event.id)] = event, + event, + ); + } + + Future deleteEvent(Event event) async { + return await update( + (event) => eventsRepository.deleteEvent(event.id), + (eventList, event) => eventList..removeWhere((d) => d.id == event.id), + event, + ); + } +} + +final associationEventsListProvider = + StateNotifierProvider< + AssociationEventsListNotifier, + AsyncValue> + >((ref) { + final token = ref.watch(tokenProvider); + final eventsRepository = EventRepository()..setToken(token); + AssociationEventsListNotifier newsListNotifier = + AssociationEventsListNotifier(eventsRepository: eventsRepository); + return newsListNotifier; + }); diff --git a/lib/feed/providers/event_image_provider.dart b/lib/feed/providers/event_image_provider.dart new file mode 100644 index 0000000000..1c754c343e --- /dev/null +++ b/lib/feed/providers/event_image_provider.dart @@ -0,0 +1,36 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/auth/providers/openid_provider.dart'; +import 'package:titan/feed/repositories/event_image_repository.dart'; +import 'package:titan/tools/providers/single_notifier.dart'; + +class EventImageNotifier extends SingleNotifier { + final eventImageRepository = EventImageRepository(); + EventImageNotifier({required String token}) + : super(const AsyncValue.loading()) { + eventImageRepository.setToken(token); + } + + Future addEventImage(String id, Uint8List bytes) async { + final image = await eventImageRepository.addEventImage(bytes, id); + if (image.toString() != "") { + state = AsyncData(image); + return true; + } + return false; + } + + void getEventImage(String id) async { + final image = await eventImageRepository.getEventImage(id); + state = AsyncData(image); + } +} + +final eventImageProvider = + StateNotifierProvider>((ref) { + final token = ref.watch(tokenProvider); + return EventImageNotifier(token: token); + }); diff --git a/lib/feed/providers/event_provider.dart b/lib/feed/providers/event_provider.dart new file mode 100644 index 0000000000..4cd01242da --- /dev/null +++ b/lib/feed/providers/event_provider.dart @@ -0,0 +1,34 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/auth/providers/openid_provider.dart'; +import 'package:titan/feed/class/event.dart'; +import 'package:titan/feed/repositories/event_repository.dart'; +import 'package:titan/tools/providers/single_notifier.dart'; + +class EventNotifier extends SingleNotifier { + final EventRepository eventRepository; + EventNotifier({required this.eventRepository}) + : super(const AsyncValue.loading()); + + Future addEvent(Event event) async { + return await eventRepository.createEvent(event); + } + + void fakeLoad() { + state = AsyncValue.data(Event.empty()); + } + + void setEvent(Event event) { + state = AsyncValue.data(event); + } +} + +final eventProvider = StateNotifierProvider>(( + ref, +) { + final token = ref.watch(tokenProvider); + final eventRepository = EventRepository()..setToken(token); + EventNotifier eventListNotifier = EventNotifier( + eventRepository: eventRepository, + )..fakeLoad(); + return eventListNotifier; +}); diff --git a/lib/feed/providers/event_ticket_url_provider.dart b/lib/feed/providers/event_ticket_url_provider.dart new file mode 100644 index 0000000000..1603e5e6a4 --- /dev/null +++ b/lib/feed/providers/event_ticket_url_provider.dart @@ -0,0 +1,23 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/feed/class/ticket_url.dart'; +import 'package:titan/feed/repositories/event_repository.dart'; +import 'package:titan/tools/providers/single_notifier.dart'; + +class TicketUrlNotifier extends SingleNotifier { + final EventRepository eventRepository; + TicketUrlNotifier({required this.eventRepository}) + : super(const AsyncValue.loading()); + + Future> getTicketUrl(String eventId) async { + return await load(() => eventRepository.getTicketUrl(eventId)); + } +} + +final ticketUrlProvider = + StateNotifierProvider>((ref) { + final eventRepository = ref.watch(eventRepositoryProvider); + TicketUrlNotifier notifier = TicketUrlNotifier( + eventRepository: eventRepository, + ); + return notifier; + }); diff --git a/lib/feed/providers/filter_state_provider.dart b/lib/feed/providers/filter_state_provider.dart new file mode 100644 index 0000000000..f0f479a701 --- /dev/null +++ b/lib/feed/providers/filter_state_provider.dart @@ -0,0 +1,15 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/feed/class/filter_state.dart'; + +class FilterStateNotifier extends StateNotifier { + FilterStateNotifier() : super(FilterState.empty()); + + void setFilterState(FilterState i) { + state = i; + } +} + +final filterStateProvider = + StateNotifierProvider((ref) { + return FilterStateNotifier(); + }); diff --git a/lib/feed/providers/is_feed_admin_provider.dart b/lib/feed/providers/is_feed_admin_provider.dart new file mode 100644 index 0000000000..b8d80b3f24 --- /dev/null +++ b/lib/feed/providers/is_feed_admin_provider.dart @@ -0,0 +1,9 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/user/providers/user_provider.dart'; + +final isFeedAdminProvider = StateProvider((ref) { + final me = ref.watch(userProvider); + return me.groups + .map((e) => e.id) + .contains("59e3c4c2-e60f-44b6-b0d2-fa1b248423bb"); // admin_feed +}); diff --git a/lib/feed/providers/is_user_a_member_of_an_association.dart b/lib/feed/providers/is_user_a_member_of_an_association.dart new file mode 100644 index 0000000000..3d8523e3f1 --- /dev/null +++ b/lib/feed/providers/is_user_a_member_of_an_association.dart @@ -0,0 +1,10 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/admin/providers/my_association_list_provider.dart'; + +final isUserAMemberOfAnAssociationProvider = Provider((ref) { + final myAssociation = ref.watch(asyncMyAssociationListProvider); + return myAssociation.maybeWhen( + data: (associations) => associations.isNotEmpty, + orElse: () => false, + ); +}); diff --git a/lib/feed/providers/news_image_provider.dart b/lib/feed/providers/news_image_provider.dart new file mode 100644 index 0000000000..d3152546b4 --- /dev/null +++ b/lib/feed/providers/news_image_provider.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/auth/providers/openid_provider.dart'; +import 'package:titan/feed/providers/news_images_provider.dart'; +import 'package:titan/feed/repositories/news_image_repository.dart'; +import 'package:titan/tools/providers/single_notifier.dart'; + +class NewsImageNotifier extends SingleNotifier { + final newsImageRepository = NewsImageRepository(); + final NewsImagesNotifier newsImagesNotifier; + NewsImageNotifier({required String token, required this.newsImagesNotifier}) + : super(const AsyncValue.loading()) { + newsImageRepository.setToken(token); + } + + Future getNewsImage(String id) async { + final image = await newsImageRepository.getNewsImage(id); + newsImagesNotifier.setTData(id, AsyncData([image])); + return image; + } +} + +final newsImageProvider = + StateNotifierProvider>((ref) { + final token = ref.watch(tokenProvider); + final newsImagesNotifier = ref.watch(newsImagesProvider.notifier); + return NewsImageNotifier( + token: token, + newsImagesNotifier: newsImagesNotifier, + ); + }); diff --git a/lib/feed/providers/news_images_provider.dart b/lib/feed/providers/news_images_provider.dart new file mode 100644 index 0000000000..7db05cd5d7 --- /dev/null +++ b/lib/feed/providers/news_images_provider.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/tools/providers/map_provider.dart'; + +class NewsImagesNotifier extends MapNotifier { + NewsImagesNotifier() : super(); +} + +final newsImagesProvider = + StateNotifierProvider< + NewsImagesNotifier, + Map>?> + >((ref) { + NewsImagesNotifier advertPosterNotifier = NewsImagesNotifier(); + return advertPosterNotifier; + }); diff --git a/lib/feed/providers/news_list_provider.dart b/lib/feed/providers/news_list_provider.dart new file mode 100644 index 0000000000..8056cdbd32 --- /dev/null +++ b/lib/feed/providers/news_list_provider.dart @@ -0,0 +1,41 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/auth/providers/openid_provider.dart'; +import 'package:titan/feed/class/news.dart'; +import 'package:titan/feed/repositories/news_repository.dart'; +import 'package:titan/tools/providers/list_notifier.dart'; + +class NewsListNotifier extends ListNotifier { + final NewsRepository newsRepository; + AsyncValue> allNews = const AsyncValue.loading(); + NewsListNotifier({required this.newsRepository}) + : super(const AsyncValue.loading()); + + Future>> loadNewsList() async { + return allNews = await loadList(newsRepository.getPublishedNews); + } + + void filterNews(List entities, List modules) { + state = AsyncValue.data( + (allNews.value ?? []).where((news) { + final matchesEntity = + entities.isEmpty || entities.contains(news.entity); + final matchesModule = modules.isEmpty || modules.contains(news.module); + return matchesEntity && matchesModule; + }).toList(), + ); + } + + void resetFilters() { + state = AsyncValue.data(allNews.value ?? []); + } +} + +final newsListProvider = + StateNotifierProvider>>((ref) { + final token = ref.watch(tokenProvider); + final newsRepository = NewsRepository()..setToken(token); + NewsListNotifier newsListNotifier = NewsListNotifier( + newsRepository: newsRepository, + )..loadNewsList(); + return newsListNotifier; + }); diff --git a/lib/feed/repositories/event_image_repository.dart b/lib/feed/repositories/event_image_repository.dart new file mode 100644 index 0000000000..0987c5c65a --- /dev/null +++ b/lib/feed/repositories/event_image_repository.dart @@ -0,0 +1,27 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/auth/providers/openid_provider.dart'; +import 'package:titan/tools/repository/logo_repository.dart'; + +class EventImageRepository extends LogoRepository { + @override + // ignore: overridden_fields + final ext = 'calendar/events/'; + + Future addEventImage(Uint8List bytes, String id) async { + final uint8List = await addLogo(bytes, id, suffix: "/image"); + return Image.memory(uint8List); + } + + Future getEventImage(String id) async { + final uint8List = await getLogo(id, suffix: "/image"); + return Image.memory(uint8List); + } +} + +final eventImageRepositoryProvider = Provider((ref) { + final token = ref.watch(tokenProvider); + return EventImageRepository()..setToken(token); +}); diff --git a/lib/feed/repositories/event_repository.dart b/lib/feed/repositories/event_repository.dart new file mode 100644 index 0000000000..0319ad0f2c --- /dev/null +++ b/lib/feed/repositories/event_repository.dart @@ -0,0 +1,42 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/auth/providers/openid_provider.dart'; +import 'package:titan/feed/class/event.dart'; +import 'package:titan/feed/class/ticket_url.dart'; +import 'package:titan/tools/repository/repository.dart'; + +class EventRepository extends Repository { + @override + // ignore: overridden_fields + final ext = "calendar/events/"; + + Future createEvent(Event event) async { + return Event.fromJson(await create(event.toJson())); + } + + Future> getEventList() async { + return List.from((await getList()).map((e) => Event.fromJson(e))); + } + + Future> getAssociationEventList(String id) async { + return List.from( + (await getList(suffix: "associations/$id")).map((e) => Event.fromJson(e)), + ); + } + + Future getTicketUrl(String id) async { + return TicketUrl.fromJson(await getOne(id, suffix: "/ticket-url")); + } + + Future updateEvent(Event event) async { + return await update(event.toJson(), event.id); + } + + Future deleteEvent(String id) async { + return await delete(id); + } +} + +final eventRepositoryProvider = Provider((ref) { + final token = ref.watch(tokenProvider); + return EventRepository()..setToken(token); +}); diff --git a/lib/feed/repositories/news_image_repository.dart b/lib/feed/repositories/news_image_repository.dart new file mode 100644 index 0000000000..aadb1dffdf --- /dev/null +++ b/lib/feed/repositories/news_image_repository.dart @@ -0,0 +1,30 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/auth/providers/openid_provider.dart'; +import 'package:titan/tools/repository/logo_repository.dart'; + +class NewsImageRepository extends LogoRepository { + @override + // ignore: overridden_fields + final ext = 'feed/news/'; + + Future getNewsImage(String id) async { + final uint8List = await getLogo(id, suffix: "/image"); + if (uint8List.isEmpty) { + throw Exception("No image found"); + } + return Image.memory(uint8List); + } + + Future addNewsImage(Uint8List bytes, String id) async { + final uint8List = await addLogo(bytes, id, suffix: "/image"); + return Image.memory(uint8List); + } +} + +final newsImageRepositoryProvider = Provider((ref) { + final token = ref.watch(tokenProvider); + return NewsImageRepository()..setToken(token); +}); diff --git a/lib/feed/repositories/news_repository.dart b/lib/feed/repositories/news_repository.dart new file mode 100644 index 0000000000..6713073eca --- /dev/null +++ b/lib/feed/repositories/news_repository.dart @@ -0,0 +1,32 @@ +import 'package:titan/feed/class/news.dart'; +import 'package:titan/tools/repository/repository.dart'; + +class NewsRepository extends Repository { + @override + // ignore: overridden_fields + final ext = "feed/"; + + Future> getPublishedNews() async { + return List.from( + (await getList(suffix: "news")).map((e) => News.fromJson(e)), + ); + } + + Future createNews(News news) async { + return News.fromJson(await create(news.toJson(), suffix: "news")); + } + + Future> getAllNews() async { + return List.from( + (await getList(suffix: "admin/news")).map((e) => News.fromJson(e)), + ); + } + + Future approveNews(String id) async { + return await create({}, suffix: "admin/news/$id/approve"); + } + + Future rejectNews(String id) async { + return await create({}, suffix: "admin/news/$id/reject"); + } +} diff --git a/lib/feed/router.dart b/lib/feed/router.dart new file mode 100644 index 0000000000..8a61ac1d30 --- /dev/null +++ b/lib/feed/router.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/feed/providers/is_feed_admin_provider.dart'; +import 'package:titan/feed/providers/is_user_a_member_of_an_association.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/class/module.dart'; +import 'package:titan/feed/ui/pages/association_events_page/association_events_page.dart' + deferred as association_events_page; +import 'package:titan/feed/ui/pages/add_event_page/add_event_page.dart' + deferred as add_edit_event_page; +import 'package:titan/feed/ui/pages/event_handling_page/event_handling_page.dart' + deferred as event_handling_page; +import 'package:titan/feed/ui/pages/main_page/main_page.dart' + deferred as main_page; +import 'package:titan/tools/middlewares/admin_middleware.dart'; +import 'package:titan/tools/middlewares/authenticated_middleware.dart'; +import 'package:titan/tools/middlewares/deferred_middleware.dart'; +import 'package:qlevar_router/qlevar_router.dart'; + +class FeedRouter { + final Ref ref; + + static const String root = '/feed'; + static const String addEditEvent = '/add_edit_event'; + static const String associationEvents = '/association_events'; + static const String eventHandling = '/event_handling'; + static final Module module = Module( + getName: (context) => AppLocalizations.of(context)!.moduleFeed, + getDescription: (context) => + AppLocalizations.of(context)!.moduleFeedDescription, + root: FeedRouter.root, + ); + + FeedRouter(this.ref); + + QRoute route() => QRoute( + name: "feed", + path: FeedRouter.root, + builder: () => main_page.FeedMainPage(), + middleware: [ + AuthenticatedMiddleware(ref), + DeferredLoadingMiddleware(main_page.loadLibrary), + ], + pageType: QCustomPage( + transitionsBuilder: (_, animation, _, child) => + FadeTransition(opacity: animation, child: child), + ), + children: [ + QRoute( + path: addEditEvent, + builder: () => add_edit_event_page.AddEditEventPage(), + middleware: [ + AdminMiddleware(ref, isUserAMemberOfAnAssociationProvider), + DeferredLoadingMiddleware(add_edit_event_page.loadLibrary), + ], + ), + QRoute( + path: eventHandling, + builder: () => event_handling_page.EventHandlingPage(), + middleware: [ + AdminMiddleware(ref, isFeedAdminProvider), + DeferredLoadingMiddleware(event_handling_page.loadLibrary), + ], + ), + QRoute( + path: associationEvents, + builder: () => association_events_page.ManageAssociationEventPage(), + middleware: [ + AdminMiddleware(ref, isUserAMemberOfAnAssociationProvider), + DeferredLoadingMiddleware(association_events_page.loadLibrary), + ], + ), + ], + ); +} diff --git a/lib/feed/tools/function.dart b/lib/feed/tools/function.dart new file mode 100644 index 0000000000..b1c5b57df1 --- /dev/null +++ b/lib/feed/tools/function.dart @@ -0,0 +1,25 @@ +enum NewsStatus { waitingApproval, rejected, published } + +String newsStatusToString(NewsStatus status) { + switch (status) { + case NewsStatus.waitingApproval: + return 'waiting_approval'; + case NewsStatus.rejected: + return 'rejected'; + case NewsStatus.published: + return 'published'; + } +} + +NewsStatus stringToNewsStatus(String status) { + switch (status) { + case 'waiting_approval': + return NewsStatus.waitingApproval; + case 'rejected': + return NewsStatus.rejected; + case 'published': + return NewsStatus.published; + default: + return NewsStatus.waitingApproval; // Default case + } +} diff --git a/lib/feed/tools/image_color_utils.dart b/lib/feed/tools/image_color_utils.dart new file mode 100644 index 0000000000..4dcebccec1 --- /dev/null +++ b/lib/feed/tools/image_color_utils.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; +import 'dart:ui' as ui; +import 'package:titan/tools/constants.dart'; + +/// Determines if a color is considered dark or light +bool isColorDark(Color color) { + // Calculate luminance using the standard formula + final luminance = (0.299 * color.r + 0.587 * color.g + 0.114 * color.b); + return luminance < 0.5; +} + +/// Gets the appropriate text color based on background color +Color getTextColor(Color backgroundColor) { + return isColorDark(backgroundColor) + ? ColorConstants.background + : ColorConstants.onTertiary; +} + +/// Extracts dominant color from an ImageProvider +Future getDominantColor(ImageProvider imageProvider) async { + final completer = Completer(); + final imageStream = imageProvider.resolve(const ImageConfiguration()); + + imageStream.addListener( + ImageStreamListener((ImageInfo imageInfo, bool synchronousCall) async { + final image = imageInfo.image; + final bytes = await image.toByteData(format: ui.ImageByteFormat.rawRgba); + + if (bytes == null) { + completer.complete(ColorConstants.main); + return; + } + + int redSum = 0, greenSum = 0, blueSum = 0; + int pixelCount = 0; + + // Sample pixels from the bottom portion of the image where text appears + final int width = image.width; + final int height = image.height; + final int startY = (height * 0.6).round(); // Bottom 40% of image + final buffer = bytes.buffer.asUint8List(); + + for (int y = startY; y < height; y += 4) { + // Sample every 4th pixel for performance + for (int x = 0; x < width; x += 4) { + final int pixelIndex = (y * width + x) * 4; + if (pixelIndex + 2 < bytes.lengthInBytes) { + redSum += buffer[pixelIndex]; + greenSum += buffer[pixelIndex + 1]; + blueSum += buffer[pixelIndex + 2]; + pixelCount++; + } + } + } + + if (pixelCount > 0) { + final avgColor = Color.fromRGBO( + (redSum / pixelCount).round(), + (greenSum / pixelCount).round(), + (blueSum / pixelCount).round(), + 1.0, + ); + completer.complete(avgColor); + } else { + completer.complete(ColorConstants.main); + } + }), + ); + + return completer.future; +} diff --git a/lib/feed/tools/news_filter_type.dart b/lib/feed/tools/news_filter_type.dart new file mode 100644 index 0000000000..91d369cb09 --- /dev/null +++ b/lib/feed/tools/news_filter_type.dart @@ -0,0 +1,16 @@ +enum NewsFilterType { all, pending, approved, rejected } + +extension NewsFilterTypeExtension on NewsFilterType { + String getKey() { + switch (this) { + case NewsFilterType.all: + return 'feedFilterAll'; + case NewsFilterType.pending: + return 'feedFilterPending'; + case NewsFilterType.approved: + return 'feedFilterApproved'; + case NewsFilterType.rejected: + return 'feedFilterRejected'; + } + } +} diff --git a/lib/feed/tools/news_helper.dart b/lib/feed/tools/news_helper.dart new file mode 100644 index 0000000000..c799437546 --- /dev/null +++ b/lib/feed/tools/news_helper.dart @@ -0,0 +1,232 @@ +import 'package:flutter/widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/advert/router.dart'; +import 'package:titan/feed/class/news.dart'; +import 'package:intl/intl.dart'; +import 'package:titan/feed/providers/event_ticket_url_provider.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:titan/vote/router.dart'; +import 'package:url_launcher/url_launcher.dart'; + +String _capitalize(String text) { + if (text.isEmpty) return text; + return text[0].toUpperCase() + text.substring(1); +} + +bool isNewsTerminated(News news) { + final now = DateTime.now(); + if (news.end != null && news.end!.isBefore(now)) { + return true; + } + return false; +} + +bool isNewsOngoing(News news) { + final now = DateTime.now(); + if (news.start.isBefore(now) && + (news.end == null || news.end!.isAfter(now))) { + return true; + } + return false; +} + +String formatUserFriendlyDate(DateTime date, {required BuildContext context}) { + final locale = Localizations.localeOf(context).toString(); + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final dateDay = DateTime(date.year, date.month, date.day); + + final timeFormat = DateFormat.Hm(locale); + final time = timeFormat.format(date); + + final connector = AppLocalizations.of(context)!.dateAt; + + final difference = dateDay.difference(today).inDays; + + if (difference == 0) { + return "${_capitalize(AppLocalizations.of(context)!.dateToday)} $connector $time"; + } else if (difference == -1) { + return "${_capitalize(AppLocalizations.of(context)!.dateYesterday)} $connector $time"; + } else if (difference == 1) { + return "${_capitalize(AppLocalizations.of(context)!.dateTomorrow)} $connector $time"; + } else if (difference > 1 && difference < 7) { + final dayName = _capitalize(DateFormat.EEEE(locale).format(date)); + return "$dayName $connector $time"; + } else if (difference < 0 && difference > -7) { + final dayName = _capitalize(DateFormat.EEEE(locale).format(date)); + final prefix = AppLocalizations.of(context)!.dateLast; + + final prefixWithSpace = prefix.isEmpty ? '' : _capitalize('$prefix '); + return "$prefixWithSpace$dayName $connector $time"; + } else { + if (date.year == now.year) { + final monthDay = _capitalize(DateFormat.MMMd(locale).format(date)); + return "$monthDay $connector $time"; + } else { + final monthDayYear = _capitalize(DateFormat.yMMMMd(locale).format(date)); + return "$monthDayYear $connector $time"; + } + } +} + +String getNewsSubtitle(News news, {required BuildContext context}) { + final locale = Localizations.localeOf(context).toString(); + String subtitle = ''; + + final startDate = news.start.toLocal(); + + if (isNewsOngoing(news) && news.end != null) { + final untilText = _capitalize(AppLocalizations.of(context)!.dateUntil); + subtitle = + "$untilText ${formatUserFriendlyDate(news.end!.toLocal(), context: context)}"; + } else if (news.end == null) { + subtitle = formatUserFriendlyDate(startDate, context: context); + } else { + final endDate = news.end!.toLocal(); + bool sameDay = + startDate.year == endDate.year && + startDate.month == endDate.month && + startDate.day == endDate.day; + + if (sameDay) { + final connector = AppLocalizations.of(context)!.dateAt; + + String dateStr = formatUserFriendlyDate( + startDate, + context: context, + ).split(' $connector ')[0]; + + final startTime = DateFormat.Hm(locale).format(startDate); + final endTime = DateFormat.Hm(locale).format(endDate); + + final fromWord = AppLocalizations.of(context)!.dateFrom; + final toWord = AppLocalizations.of(context)!.dateTo; + + subtitle = '$dateStr $fromWord $startTime $toWord $endTime'; + } else { + final fromWord = _capitalize(AppLocalizations.of(context)!.dateFrom); + + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final endDateTime = DateTime(endDate.year, endDate.month, endDate.day); + final difference = endDateTime.difference(today).inDays; + + final toWord = (difference >= -1 && difference <= 1) + ? (AppLocalizations.of(context)!.dateTo) + : (AppLocalizations.of(context)!.dateBetweenDays); + + subtitle = + '$fromWord ${formatUserFriendlyDate(startDate, context: context)} $toWord ${formatUserFriendlyDate(endDate, context: context)}'; + } + } + + if (subtitle.isEmpty) { + subtitle = news.entity; + } + + return subtitle; +} + +String getActionTitle(News news, BuildContext context) { + final module = news.module; + + if (module == "campagne") { + return AppLocalizations.of(context)!.eventActionCampaign; + } else if (module == "event") { + return AppLocalizations.of(context)!.eventActionEvent; + } + return ''; +} + +String getWaitingTitle( + News news, + BuildContext context, { + required String timeToGo, +}) { + final module = news.module; + final localizeWithContext = AppLocalizations.of(context)!; + + if (module == "campagne") { + return localizeWithContext.feedVoteIn(timeToGo); + } else if (module == "event") { + return localizeWithContext.feedShotgunIn(timeToGo); + } + return ''; +} + +String getActionSubtitle(News news, BuildContext context) { + final module = news.module; + + if (module == "campagne") { + return AppLocalizations.of(context)!.eventActionCampaignSubtitle; + } else if (module == "event") { + return AppLocalizations.of(context)!.eventActionEventSubtitle; + } + return ''; +} + +String getActionEnableButtonText(News news, BuildContext context) { + final module = news.module; + + if (module == "campagne") { + return AppLocalizations.of(context)!.eventActionCampaignButton; + } else if (module == "event") { + return AppLocalizations.of(context)!.eventActionEventButton; + } + return ''; +} + +String getActionValidatedButtonText(News news, BuildContext context) { + final module = news.module; + final localizeWithContext = AppLocalizations.of(context)!; + + if (module == "campagne") { + return localizeWithContext.eventActionCampaignValidated; + } else if (module == "event") { + return localizeWithContext.eventActionEventValidated; + } + return ''; +} + +void getActionButtonAction( + News news, + BuildContext context, + WidgetRef ref, +) async { + final module = news.module; + final localizeWithContext = AppLocalizations.of(context)!; + + if (module == "campagne") { + QR.to(VoteRouter.root); + } else if (module == "event") { + final ticketUrlNotifier = ref.watch(ticketUrlProvider.notifier); + final ticketUrl = await ticketUrlNotifier.getTicketUrl(news.moduleObjectId); + ticketUrl.when( + data: (ticketUrl) async { + if (await canLaunchUrl(Uri.parse(ticketUrl.ticketUrl))) { + await launchUrl( + Uri.parse(ticketUrl.ticketUrl), + mode: LaunchMode.externalApplication, + ); + } else { + if (!context.mounted) return; + displayToast( + context, + TypeMsg.error, + localizeWithContext.feedCantOpenLink, + ); + } + }, + error: (e, stackTrace) { + displayToast(context, TypeMsg.error, e.toString()); + }, + loading: () {}, + ); + } else if (module == "advert") { + // TODO : set id + QR.to(AdvertRouter.root); + } + return; +} diff --git a/lib/feed/ui/feed.dart b/lib/feed/ui/feed.dart new file mode 100644 index 0000000000..89642ef535 --- /dev/null +++ b/lib/feed/ui/feed.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/feed/providers/news_list_provider.dart'; +import 'package:titan/feed/router.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/ui/widgets/top_bar.dart'; + +class FeedTemplate extends ConsumerWidget { + final Widget child; + const FeedTemplate({super.key, required this.child}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Container( + color: ColorConstants.background, + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + TopBar( + root: FeedRouter.root, + onBack: () { + if (QR.currentPath == + FeedRouter.root + FeedRouter.eventHandling) { + final newsListNotifier = ref.watch(newsListProvider.notifier); + newsListNotifier.loadNewsList(); + } + }, + ), + Expanded(child: child), + ], + ), + ), + ); + } +} diff --git a/lib/feed/ui/pages/add_event_page/add_event_page.dart b/lib/feed/ui/pages/add_event_page/add_event_page.dart new file mode 100644 index 0000000000..31f946f461 --- /dev/null +++ b/lib/feed/ui/pages/add_event_page/add_event_page.dart @@ -0,0 +1,710 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:intl/intl.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +// import 'package:syncfusion_flutter_calendar/calendar.dart'; +import 'package:titan/admin/class/assocation.dart'; +import 'package:titan/admin/providers/my_association_list_provider.dart'; +// import 'package:titan/event/providers/selected_days_provider.dart'; +// import 'package:titan/event/tools/constants.dart'; +// import 'package:titan/event/tools/functions.dart'; +import 'package:titan/event/ui/pages/event_pages/checkbox_entry.dart'; +import 'package:titan/feed/class/event.dart'; +import 'package:titan/feed/providers/association_event_list_provider.dart'; +import 'package:titan/feed/providers/event_image_provider.dart'; +import 'package:titan/feed/providers/event_provider.dart'; +import 'package:titan/feed/providers/news_list_provider.dart'; +import 'package:titan/feed/ui/feed.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/ui/scroll_to_hide_navbar.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; +import 'package:titan/tools/ui/builders/waiting_button.dart'; +import 'package:titan/tools/ui/styleguide/horizontal_multi_select.dart'; +import 'package:titan/tools/ui/styleguide/text_entry.dart'; +import 'package:titan/tools/ui/widgets/date_entry.dart'; +import 'package:titan/tools/ui/widgets/image_picker_on_tap.dart'; + +class AddEditEventPage extends HookConsumerWidget { + const AddEditEventPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final locale = Localizations.localeOf(context); + final key = GlobalKey(); + // final recurrentController = useState(false); + // final recurrenceEndDateController = useTextEditingController(); + + final myAssociations = ref.watch(myAssociationListProvider); + final event = ref.watch(eventProvider); + final eventCreationNotifier = ref.watch(eventProvider.notifier); + final eventListNotifier = ref.watch(associationEventsListProvider.notifier); + final eventImageNotifier = ref.watch(eventImageProvider.notifier); + final newsListNotifier = ref.watch(newsListProvider.notifier); + final image = ref.watch(eventImageProvider); + // final interval = useTextEditingController(); + // final recurrenceEndDate = useTextEditingController(); + // final selectedDays = ref.watch(selectedDaysProvider); + // final selectedDaysNotifier = ref.watch(selectedDaysProvider.notifier); + // final now = DateTime.now(); + final selectedAssociation = useState( + myAssociations.length == 1 ? myAssociations.first : null, + ); + + final ImagePicker picker = ImagePicker(); + + void displayToastWithContext(TypeMsg type, String msg) { + displayToast(context, type, msg); + } + + final syncEvent = event.maybeWhen( + data: (event) => event, + orElse: () => Event.empty(), + ); + final poster = useState(null); + final posterFile = useState(null); + final allDay = useState(syncEvent.allDay); + final notification = useState( + syncEvent.id != "" ? syncEvent.notification : true, + ); + final titleController = useTextEditingController(text: syncEvent.name); + final locationController = useTextEditingController( + text: syncEvent.location, + ); + final shotgunDateController = useTextEditingController( + text: syncEvent.ticketUrlOpening != null + ? DateFormat.yMd( + locale.toString(), + ).add_Hm().format(syncEvent.ticketUrlOpening!) + : "", + ); + final externalLinkController = useTextEditingController( + text: syncEvent.ticketUrl, + ); + final startDateController = useTextEditingController( + text: syncEvent.id != "" + ? (allDay.value + ? DateFormat.yMd(locale.toString()).format(syncEvent.start) + : DateFormat.yMd( + locale.toString(), + ).add_Hm().format(syncEvent.start)) + : "", + ); + final endDateController = useTextEditingController( + text: syncEvent.id != "" + ? (allDay.value + ? DateFormat.yMd(locale.toString()).format(syncEvent.end) + : DateFormat.yMd( + locale.toString(), + ).add_Hm().format(syncEvent.end)) + : "", + ); + image.maybeWhen( + data: (image) => posterFile.value = image, + orElse: () => null, + ); + + final localizeWithContext = AppLocalizations.of(context)!; + + return FeedTemplate( + child: Expanded( + child: Form( + key: key, + child: ScrollToHideNavbar( + controller: ScrollController(), + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20.0, + vertical: 16.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + syncEvent.id == "" + ? SizedBox( + height: 50, + child: HorizontalMultiSelect( + items: myAssociations, + selectedItem: selectedAssociation.value, + onItemSelected: (association) { + selectedAssociation.value = association; + }, + itemBuilder: + (context, association, index, selected) => + Text( + association.name, + style: TextStyle( + color: selected + ? ColorConstants.background + : ColorConstants.tertiary, + fontSize: 16, + ), + ), + ), + ) + : Text( + localizeWithContext.feedAssociationEvent( + myAssociations + .firstWhere( + (element) => + element.id == syncEvent.associationId, + ) + .name, + ), + ), + const SizedBox(height: 10), + TextEntry( + label: localizeWithContext.feedTitle, + controller: titleController, + maxLength: 30, + ), + const SizedBox(height: 10), + CheckBoxEntry( + title: localizeWithContext.eventAllDay, + valueNotifier: allDay, + onChanged: () { + allDay.value = !allDay.value; + startDateController.text = ""; + endDateController.text = ""; + // recurrenceEndDateController.text = ""; + }, + ), + + // const SizedBox(height: 10), + // CheckBoxEntry( + // title: localizeWithContext.eventRecurrence, + // valueNotifier: recurrentController, + // onChanged: () { + // startDateController.text = ""; + // endDateController.text = ""; + // recurrenceEndDateController.text = ""; + // }, + // ), + // const SizedBox(height: 0), + // recurrentController.value + // ? Column( + // children: [ + // Column( + // children: [ + // Text( + // AppLocalizations.of( + // context, + // )!.eventRecurrenceDays, + // style: const TextStyle(color: Colors.black), + // ), + // const SizedBox(height: 10), + // Column( + // children: eventDayKeys.asMap().entries.map(( + // entry, + // ) { + // final index = entry.key; + // final key = entry.value; + // final localizedLabel = getLocalizedEventDay( + // context, + // key, + // ); + + // return GestureDetector( + // onTap: () => + // selectedDaysNotifier.toggle(index), + // behavior: HitTestBehavior.opaque, + // child: Row( + // mainAxisAlignment: + // MainAxisAlignment.spaceBetween, + // children: [ + // Text( + // localizedLabel, + // style: TextStyle( + // color: Colors.grey.shade700, + // fontSize: 16, + // ), + // ), + // Checkbox( + // checkColor: Colors.white, + // activeColor: Colors.black, + // value: selectedDays[index], + // onChanged: (_) => + // selectedDaysNotifier.toggle( + // index, + // ), + // ), + // ], + // ), + // ); + // }).toList(), + // ), + // const SizedBox(height: 20), + // Text( + // localizeWithContext.eventInterval, + // style: const TextStyle(color: Colors.black), + // ), + // const SizedBox(height: 10), + // TextEntry( + // label: AppLocalizations.of( + // context, + // )!.eventInterval, + // controller: interval, + // prefix: AppLocalizations.of( + // context, + // )!.eventEventEvery, + // suffix: AppLocalizations.of( + // context, + // )!.eventWeeks, + // isInt: true, + // keyboardType: TextInputType.number, + // ), + // const SizedBox(height: 30), + // if (!allDay.value) + // Column( + // children: [ + // DateEntry( + // onTap: () => getOnlyHourDate( + // context, + // startDateController, + // ), + // controller: startDateController, + // label: AppLocalizations.of( + // context, + // )!.eventStartHour, + // ), + // const SizedBox(height: 30), + // DateEntry( + // onTap: () => getOnlyHourDate( + // context, + // endDateController, + // ), + // controller: endDateController, + // label: AppLocalizations.of( + // context, + // )!.eventEndHour, + // ), + // const SizedBox(height: 30), + // ], + // ), + // DateEntry( + // onTap: () => getOnlyDayDate( + // context, + // recurrenceEndDate, + // ), + // controller: recurrenceEndDate, + // label: AppLocalizations.of( + // context, + // )!.eventRecurrenceEndDate, + // ), + // ], + // ), + // ], + // ) + // : + DateEntry( + onTap: () => allDay.value + ? getOnlyDayDate(context, startDateController) + : getFullDate(context, startDateController), + controller: startDateController, + label: localizeWithContext.eventStartDate, + ), + const SizedBox(height: 10), + DateEntry( + onTap: () => allDay.value + ? getOnlyDayDate(context, endDateController) + : getFullDate(context, endDateController), + controller: endDateController, + label: localizeWithContext.eventEndDate, + ), + SizedBox(height: 10), + TextEntry( + label: localizeWithContext.feedLocation, + controller: locationController, + ), + SizedBox(height: 10), + DateEntry( + onTap: () => getFullDate(context, shotgunDateController), + controller: shotgunDateController, + label: localizeWithContext.feedSGDate, + canBeEmpty: true, + ), + SizedBox(height: 10), + TextEntry( + label: localizeWithContext.feedSGExternalLink, + controller: externalLinkController, + canBeEmpty: true, + ), + const SizedBox(height: 10), + CheckBoxEntry( + title: localizeWithContext.feedNotification, + valueNotifier: notification, + onChanged: () { + notification.value = !notification.value; + }, + ), + const SizedBox(height: 10), + FormField( + builder: (formFieldState) => Center( + child: Stack( + clipBehavior: Clip.none, + children: [ + ImagePickerOnTap( + picker: picker, + imageBytesNotifier: poster, + imageNotifier: posterFile, + displayToastWithContext: displayToastWithContext, + child: AspectRatio( + aspectRatio: 851 / 315, + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(5), + ), + color: Colors.white, + boxShadow: [ + BoxShadow( + color: formFieldState.hasError + ? Colors.red + : Colors.black.withValues( + alpha: 0.1, + ), + spreadRadius: 5, + blurRadius: 10, + offset: const Offset(2, 3), + ), + ], + ), + child: posterFile.value != null + ? Stack( + children: [ + AspectRatio( + aspectRatio: 851 / 315, + child: Container( + decoration: BoxDecoration( + borderRadius: + const BorderRadius.all( + Radius.circular(5), + ), + image: DecorationImage( + image: poster.value != null + ? Image.memory( + poster.value!, + fit: BoxFit.cover, + ).image + : posterFile + .value! + .image, + fit: BoxFit.cover, + ), + ), + child: Center( + child: Container( + width: 50, + height: 50, + decoration: BoxDecoration( + borderRadius: + const BorderRadius.all( + Radius.circular(5), + ), + color: Colors.white + .withValues( + alpha: 0.4, + ), + ), + child: HeroIcon( + HeroIcons.photo, + size: 40, + color: Colors.black + .withValues( + alpha: 0.5, + ), + ), + ), + ), + ), + ), + ], + ) + : Container( + decoration: BoxDecoration( + borderRadius: + const BorderRadius.all( + Radius.circular(5), + ), + ), + child: Column( + mainAxisAlignment: + MainAxisAlignment.start, + children: [ + const HeroIcon( + HeroIcons.photo, + size: 100, + color: Colors.grey, + ), + Text("(851/315)"), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 40), + WaitingButton( + child: Text( + syncEvent.id == "" + ? localizeWithContext.feedCreateEvent + : localizeWithContext.feedEditEvent, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Color.fromARGB(255, 255, 255, 255), + ), + ), + builder: (child) => Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 10, + ), + decoration: BoxDecoration( + color: ColorConstants.tertiary, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: ColorConstants.onTertiary), + ), + child: Center(child: child), + ), + onTap: () async { + if (key.currentState == null) { + return; + } + if (syncEvent.id == "" && + selectedAssociation.value == null) { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.feedPleaseSelectAnAssociation, + ); + return; + } + if (externalLinkController.text.isEmpty && + shotgunDateController.text.isNotEmpty) { + displayToastWithContext( + TypeMsg.error, + localizeWithContext + .feedPleaseProvideASGExternalLink, + ); + return; + } + if (externalLinkController.text.isNotEmpty && + shotgunDateController.text.isEmpty) { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.feedPleaseProvideASGDate, + ); + return; + } + if (key.currentState!.validate()) { + // if (allDay.value) { + // startDateController.text = + // "${!recurrentController.value ? "${startDateController.text} " : ""}00:00"; + // endDateController.text = + // "${!recurrentController.value ? "${endDateController.text} " : ""}23:59"; + // } + if (endDateController.text.contains("/") && + isDateBefore( + processDateBackWithHourMaybe( + endDateController.text, + locale.toString(), + ), + processDateBackWithHourMaybe( + startDateController.text, + locale.toString(), + ), + )) { + displayToast( + context, + TypeMsg.error, + localizeWithContext.eventInvalidDates, + ); + // } else if (recurrentController.value && + // selectedDays.where((element) => element).isEmpty) { + // displayToast( + // context, + // TypeMsg.error, + // localizeWithContext.eventNoDaySelected, + // ); + } else { + await tokenExpireWrapper(ref, () async { + // String recurrenceRule = ""; + // String startString = startDateController.text; + // if (!startString.contains("/")) { + // startString = "${DateFormat.yMd(locale).format(now)} $startString"; + // } + // String endString = endDateController.text; + // if (!endString.contains("/")) { + // endString = "${DateFormat.yMd(locale).format(now)} $endString"; + // } + // if (recurrentController.value) { + // RecurrenceProperties recurrence = + // RecurrenceProperties(startDate: now); + // recurrence.recurrenceType = RecurrenceType.weekly; + // recurrence.recurrenceRange = + // RecurrenceRange.endDate; + // recurrence.endDate = DateTime.parse( + // processDateBack(recurrenceEndDate.text), + // ); + // recurrence.weekDays = WeekDays.values + // .where( + // (element) => + // selectedDays[(WeekDays.values.indexOf( + // element, + // ) - + // 1) % + // 7], + // ) + // .toList(); + // recurrence.interval = int.parse(interval.text); + // recurrenceRule = SfCalendar.generateRRule( + // recurrence, + // DateTime.parse( + // processDateBackWithHour(startString), + // ), + // DateTime.parse( + // processDateBackWithHour(endString), + // ), + // ); + // } + final newEvent = Event( + id: syncEvent.id, + start: DateTime.parse( + processDateBackWithHourMaybe( + startDateController.text, + locale.toString(), + ), + ), + end: DateTime.parse( + processDateBackWithHourMaybe( + endDateController.text, + locale.toString(), + ), + ), + location: locationController.text, + ticketUrlOpening: + shotgunDateController.text != "" + ? DateTime.parse( + processDateBackWithHourMaybe( + shotgunDateController.text, + locale.toString(), + ), + ) + : null, + name: titleController.text, + allDay: allDay.value, + // recurrenceRule: recurrenceRule, + recurrenceRule: "", + associationId: syncEvent.id != "" + ? syncEvent.associationId + : selectedAssociation.value!.id, + ticketUrl: externalLinkController.text, + notification: notification.value, + ); + try { + if (syncEvent.id != "") { + final value = await eventListNotifier + .updateEvent(newEvent); + if (value) { + if (poster.value == null) { + QR.back(); + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.eventModifiedEvent, + ); + newsListNotifier.loadNewsList(); + return; + } + final imageUploaded = + await eventImageNotifier.addEventImage( + syncEvent.id, + poster.value!, + ); + if (imageUploaded) { + QR.back(); + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.eventModifiedEvent, + ); + newsListNotifier.loadNewsList(); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.eventModifyingError, + ); + } + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.eventModifyingError, + ); + } + } else { + final eventCreated = + await eventCreationNotifier.addEvent( + newEvent, + ); + if (poster.value == null) { + QR.back(); + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.eventAddedEvent, + ); + newsListNotifier.loadNewsList(); + return; + } + final value = await eventImageNotifier + .addEventImage( + eventCreated.id, + poster.value!, + ); + if (value) { + QR.back(); + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.eventAddedEvent, + ); + newsListNotifier.loadNewsList(); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.eventAddingError, + ); + } + } + } catch (e) { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.eventAddingError, + ); + } + }); + } + } + }, + ), + const SizedBox(height: 80), + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/feed/ui/pages/association_events_page/association_event_card.dart b/lib/feed/ui/pages/association_events_page/association_event_card.dart new file mode 100644 index 0000000000..a80c3cf427 --- /dev/null +++ b/lib/feed/ui/pages/association_events_page/association_event_card.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/feed/class/event.dart'; +import 'package:titan/feed/providers/association_event_list_provider.dart'; +import 'package:titan/feed/providers/event_image_provider.dart'; +import 'package:titan/feed/providers/event_provider.dart'; +import 'package:titan/feed/router.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; +import 'package:titan/tools/ui/styleguide/confirm_modal.dart'; +import 'package:titan/tools/ui/styleguide/list_item.dart'; + +class AssociationEventCard extends ConsumerWidget { + final Event event; + const AssociationEventCard({super.key, required this.event}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final eventNotifier = ref.watch(eventProvider.notifier); + final associationEventsListNotifier = ref.watch( + associationEventsListProvider.notifier, + ); + final eventImageNotifier = ref.watch(eventImageProvider.notifier); + + final localizeWithContext = AppLocalizations.of(context)!; + + return ListItem( + title: event.name, + subtitle: event.location, + onTap: () => showCustomBottomModal( + context: context, + modal: BottomModalTemplate( + title: event.name, + child: Column( + children: [ + Button( + text: localizeWithContext.eventEdit, + onPressed: () { + eventNotifier.setEvent(event); + eventImageNotifier.getEventImage(event.id); + QR.to(FeedRouter.root + FeedRouter.addEditEvent); + Navigator.of(context).pop(); + }, + ), + const SizedBox(height: 10), + Button.danger( + text: localizeWithContext.eventDelete, + onPressed: () async { + Navigator.of(context).pop(); + showCustomBottomModal( + context: context, + modal: ConfirmModal( + title: localizeWithContext.eventDeleteConfirm(event.name), + description: localizeWithContext.globalIrreversibleAction, + onYes: () async { + await associationEventsListNotifier.deleteEvent(event); + }, + ), + ref: ref, + ); + }, + ), + ], + ), + ), + ref: ref, + ), + ); + } +} diff --git a/lib/feed/ui/pages/association_events_page/association_events_page.dart b/lib/feed/ui/pages/association_events_page/association_events_page.dart new file mode 100644 index 0000000000..4cf98e8247 --- /dev/null +++ b/lib/feed/ui/pages/association_events_page/association_events_page.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/admin/class/assocation.dart'; +import 'package:titan/admin/providers/my_association_list_provider.dart'; +import 'package:titan/feed/providers/association_event_list_provider.dart'; +import 'package:titan/feed/ui/feed.dart'; +import 'package:titan/feed/ui/pages/association_events_page/association_event_card.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/ui/scroll_to_hide_navbar.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/tools/ui/layouts/refresher.dart'; +import 'package:titan/tools/ui/styleguide/horizontal_multi_select.dart'; + +class ManageAssociationEventPage extends HookConsumerWidget { + const ManageAssociationEventPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final myAssociations = ref.watch(myAssociationListProvider); + final associationEventsList = ref.watch(associationEventsListProvider); + final associationEventsListNotifier = ref.watch( + associationEventsListProvider.notifier, + ); + final selectedAssociation = useState( + myAssociations.isNotEmpty ? myAssociations.first : null, + ); + + final localizeWithContext = AppLocalizations.of(context)!; + + return FeedTemplate( + child: Refresher( + onRefresh: () { + if (selectedAssociation.value == null) { + return Future.value(); + } + return associationEventsListNotifier.loadAssociationEventList( + selectedAssociation.value!.id, + ); + }, + controller: ScrollController(), + child: Column( + children: [ + const SizedBox(height: 16), + SizedBox( + height: 50, + child: HorizontalMultiSelect( + items: myAssociations, + selectedItem: selectedAssociation.value, + onItemSelected: (association) { + selectedAssociation.value = association; + associationEventsListNotifier.loadAssociationEventList( + association.id, + ); + }, + itemBuilder: (context, association, index, selected) => Text( + association.name, + style: TextStyle( + color: selected + ? ColorConstants.background + : ColorConstants.tertiary, + fontSize: 16, + ), + ), + ), + ), + const SizedBox(height: 16), + Text( + localizeWithContext.feedManageAssociationEvents, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: ColorConstants.title, + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: AsyncChild( + value: associationEventsList, + builder: (context, eventList) { + if (eventList.isEmpty) { + return Center( + child: Text( + localizeWithContext.feedNoAssociationEvents, + style: const TextStyle(color: ColorConstants.tertiary), + ), + ); + } + return ScrollToHideNavbar( + controller: ScrollController(), + child: SingleChildScrollView( + child: Column( + children: eventList + .map((event) => AssociationEventCard(event: event)) + .toList(), + ), + ), + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/feed/ui/pages/event_handling_page/admin_event_card.dart b/lib/feed/ui/pages/event_handling_page/admin_event_card.dart new file mode 100644 index 0000000000..ea01bd87c7 --- /dev/null +++ b/lib/feed/ui/pages/event_handling_page/admin_event_card.dart @@ -0,0 +1,206 @@ +import 'package:flutter/material.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/feed/class/news.dart'; +import 'package:titan/feed/providers/admin_news_list_provider.dart'; +import 'package:titan/feed/providers/news_list_provider.dart'; +import 'package:titan/feed/tools/function.dart'; +import 'package:titan/feed/tools/news_helper.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/ui/builders/waiting_button.dart'; + +class AdminEventCard extends ConsumerWidget { + final News news; + const AdminEventCard({super.key, required this.news}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final newsAdminNotifier = ref.watch(adminNewsListProvider.notifier); + final newsNotifier = ref.watch(newsListProvider.notifier); + + final localizeWithContext = AppLocalizations.of(context)!; + + return Container( + decoration: BoxDecoration( + color: ColorConstants.background, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + margin: const EdgeInsets.symmetric(vertical: 10), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + news.title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: ColorConstants.title, + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: ColorConstants.secondary.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + news.entity, + style: const TextStyle( + fontSize: 12, + color: ColorConstants.secondary, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + + const SizedBox(height: 8), + + Row( + children: [ + const HeroIcon( + HeroIcons.calendar, + size: 16, + color: ColorConstants.tertiary, + ), + const SizedBox(width: 4), + Expanded( + child: Text( + getNewsSubtitle(news, context: context), + style: const TextStyle( + fontSize: 14, + color: ColorConstants.tertiary, + ), + ), + ), + ], + ), + + if (news.location != null && news.location!.isNotEmpty) ...[ + const SizedBox(height: 4), + Row( + children: [ + const HeroIcon( + HeroIcons.mapPin, + size: 16, + color: ColorConstants.tertiary, + ), + const SizedBox(width: 4), + Text( + news.location!, + style: const TextStyle( + fontSize: 14, + color: ColorConstants.tertiary, + ), + ), + ], + ), + ], + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (news.status != NewsStatus.rejected) + WaitingButton( + onTap: () async { + final newNews = news.copyWith( + status: NewsStatus.rejected, + ); + await newsAdminNotifier.rejectNews(newNews); + await newsNotifier.loadNewsList(); + }, + builder: (child) => Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 5, + ), + width: 100, + height: 30, + decoration: BoxDecoration( + color: ColorConstants.main, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: ColorConstants.onMain, + width: 2, + ), + ), + child: child, + ), + waitingColor: ColorConstants.background, + child: Center( + child: Text( + localizeWithContext.feedReject, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: ColorConstants.background, + ), + ), + ), + ), + if (news.status == NewsStatus.waitingApproval) + SizedBox(width: 10), + if (news.status != NewsStatus.published) + WaitingButton( + onTap: () async { + final newNews = news.copyWith( + status: NewsStatus.published, + ); + await newsAdminNotifier.approveNews(newNews); + await newsNotifier.loadNewsList(); + }, + builder: (child) => Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 5, + ), + width: 100, + height: 30, + decoration: BoxDecoration( + color: ColorConstants.tertiary, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: ColorConstants.onTertiary, + width: 2, + ), + ), + child: child, + ), + waitingColor: ColorConstants.background, + child: Center( + child: Text( + localizeWithContext.feedApprove, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: ColorConstants.background, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/feed/ui/pages/event_handling_page/event_handling_page.dart b/lib/feed/ui/pages/event_handling_page/event_handling_page.dart new file mode 100644 index 0000000000..289652670a --- /dev/null +++ b/lib/feed/ui/pages/event_handling_page/event_handling_page.dart @@ -0,0 +1,163 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/feed/class/news.dart'; +import 'package:titan/feed/providers/admin_news_list_provider.dart'; +import 'package:titan/feed/tools/function.dart'; +import 'package:titan/feed/tools/news_filter_type.dart'; +import 'package:titan/feed/ui/feed.dart'; +import 'package:titan/feed/ui/pages/event_handling_page/admin_event_card.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/ui/scroll_to_hide_navbar.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/tools/ui/layouts/refresher.dart'; +import 'package:titan/tools/ui/styleguide/horizontal_multi_select.dart'; + +class EventHandlingPage extends HookConsumerWidget { + const EventHandlingPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final newsListAsync = ref.watch(adminNewsListProvider); + final newsListNotifier = ref.watch(adminNewsListProvider.notifier); + final selectedFilter = useState(NewsFilterType.pending); + + final localizeWithContext = AppLocalizations.of(context)!; + + return FeedTemplate( + child: Refresher( + onRefresh: () { + return newsListNotifier.loadNewsList(); + }, + controller: ScrollController(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Text( + localizeWithContext.feedEventManagement, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorConstants.title, + ), + ), + ), + + const SizedBox(height: 16), + + SizedBox( + height: 40, + child: HorizontalMultiSelect( + items: NewsFilterType.values, + selectedItem: selectedFilter.value, + itemBuilder: (context, item, _, selected) { + final filterName = _getFilterName(context, item); + return Text( + filterName, + style: TextStyle( + color: selected + ? ColorConstants.background + : ColorConstants.tertiary, + fontWeight: selected + ? FontWeight.bold + : FontWeight.normal, + ), + ); + }, + onItemSelected: (item) { + selectedFilter.value = item; + }, + ), + ), + + const SizedBox(height: 16), + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: AsyncChild( + value: newsListAsync, + builder: (context, newsList) { + final filteredNews = _getFilteredNews( + newsList, + selectedFilter.value, + ); + + if (filteredNews.isEmpty) { + return Center( + child: Text( + _getEmptyMessage(context, selectedFilter.value), + style: const TextStyle(color: ColorConstants.tertiary), + ), + ); + } + return ScrollToHideNavbar( + controller: ScrollController(), + child: SingleChildScrollView( + child: Column( + children: filteredNews + .map((news) => AdminEventCard(news: news)) + .toList(), + ), + ), + ); + }, + ), + ), + ], + ), + ), + ); + } + + String _getFilterName(BuildContext context, NewsFilterType filter) { + final localizeWithContext = AppLocalizations.of(context)!; + switch (filter) { + case NewsFilterType.all: + return localizeWithContext.feedFilterAll; + case NewsFilterType.pending: + return localizeWithContext.feedFilterPending; + case NewsFilterType.approved: + return localizeWithContext.feedFilterApproved; + case NewsFilterType.rejected: + return localizeWithContext.feedFilterRejected; + } + } + + List _getFilteredNews(List allNews, NewsFilterType filter) { + switch (filter) { + case NewsFilterType.all: + return allNews; + case NewsFilterType.pending: + return allNews + .where((news) => news.status == NewsStatus.waitingApproval) + .toList(); + case NewsFilterType.approved: + return allNews + .where((news) => news.status == NewsStatus.published) + .toList(); + case NewsFilterType.rejected: + return allNews + .where((news) => news.status == NewsStatus.rejected) + .toList(); + } + } + + String _getEmptyMessage(BuildContext context, NewsFilterType filter) { + final localizeWithContext = AppLocalizations.of(context)!; + switch (filter) { + case NewsFilterType.all: + return localizeWithContext.feedEmptyAll; + case NewsFilterType.pending: + return localizeWithContext.feedEmptyPending; + case NewsFilterType.approved: + return localizeWithContext.feedEmptyApproved; + case NewsFilterType.rejected: + return localizeWithContext.feedEmptyRejected; + } + } +} diff --git a/lib/feed/ui/pages/main_page/dotted_vertical_line.dart b/lib/feed/ui/pages/main_page/dotted_vertical_line.dart new file mode 100644 index 0000000000..dc967dab7a --- /dev/null +++ b/lib/feed/ui/pages/main_page/dotted_vertical_line.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:titan/tools/constants.dart'; + +/// A widget that displays a vertical line of evenly spaced dots. +/// The number of dots is calculated based on the height of the widget. +class DottedVerticalLine extends StatelessWidget { + /// The spacing between each dot in logical pixels. + final double dotSpacing; + + /// The diameter of each dot in logical pixels. + final double dotSize; + + /// The color of the dots. Defaults to [ColorConstants.secondary]. + final Color? dotColor; + + const DottedVerticalLine({ + super.key, + this.dotSpacing = 6.0, + this.dotSize = 2.0, + this.dotColor, + }); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final height = constraints.maxHeight; + + // Calculate how many dots we can fit in the height + // We add 1 to dotSpacing to account for the space between dots plus the dot itself + final dotsCount = (height / (dotSize + dotSpacing)).floor(); + + return CustomPaint( + size: Size(dotSize, height), + painter: _DottedLinePainter( + dotSpacing: dotSpacing, + dotSize: dotSize, + dotsCount: dotsCount, + dotColor: dotColor ?? ColorConstants.secondary, + ), + ); + }, + ); + } +} + +class _DottedLinePainter extends CustomPainter { + final double dotSpacing; + final double dotSize; + final int dotsCount; + final Color dotColor; + + _DottedLinePainter({ + required this.dotSpacing, + required this.dotSize, + required this.dotsCount, + required this.dotColor, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = dotColor + ..strokeCap = StrokeCap.round + ..strokeWidth = dotSize; + + // Start position + double startY = 0; + + // Draw dots + for (int i = 0; i < dotsCount; i++) { + // Calculate y position for this dot + double yPosition = startY + (i * (dotSize + dotSpacing)); + + // Draw the dot + canvas.drawCircle(Offset(size.width / 2, yPosition), dotSize / 2, paint); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/lib/feed/ui/pages/main_page/event_action.dart b/lib/feed/ui/pages/main_page/event_action.dart new file mode 100644 index 0000000000..2b128a55e5 --- /dev/null +++ b/lib/feed/ui/pages/main_page/event_action.dart @@ -0,0 +1,140 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:timeago_flutter/timeago_flutter.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/constants.dart'; + +class EventAction extends HookWidget { + final String title, + subtitle, + actionEnableButtonText, + actionValidatedButtonText; + final String Function(String timeToGo) waitingTitle; + final DateTime? timeOpening, eventEnd; + final VoidCallback? onActionPressed; + final bool isActionValidated; + + const EventAction({ + super.key, + required this.title, + required this.subtitle, + this.onActionPressed, + required this.actionEnableButtonText, + required this.actionValidatedButtonText, + required this.isActionValidated, + required this.timeOpening, + required this.eventEnd, + required this.waitingTitle, + }); + + @override + Widget build(BuildContext context) { + final now = useState(DateTime.now()); + final locale = Localizations.localeOf(context); + + useEffect(() { + final timer = Timer.periodic(const Duration(seconds: 1), (_) { + now.value = DateTime.now(); + }); + + return timer.cancel; + }, []); + + final isActionEnabled = + timeOpening != null && + timeOpening!.isBefore(now.value) && + eventEnd != null && + eventEnd!.isAfter(now.value); + + final isWaiting = timeOpening != null && timeOpening!.isAfter(now.value); + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isWaiting ? AppLocalizations.of(context)!.feedGetReady : title, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: ColorConstants.onTertiary, + ), + overflow: TextOverflow.ellipsis, + ), + timeOpening != null && + eventEnd != null && + eventEnd!.isAfter(now.value) && + timeOpening!.isAfter(now.value) + ? Timeago( + date: timeOpening!, + locale: '${locale.languageCode}_short', + allowFromNow: true, + refreshRate: const Duration(seconds: 1), + builder: (context, str) => Text( + waitingTitle(str), + style: const TextStyle( + fontSize: 11, + color: ColorConstants.secondary, + ), + overflow: TextOverflow.ellipsis, + ), + ) + : Text( + subtitle, + style: const TextStyle( + fontSize: 11, + color: ColorConstants.secondary, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + const SizedBox(width: 10), + GestureDetector( + onTap: () { + if (isActionEnabled && !isActionValidated) onActionPressed!.call(); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), + width: 100, + decoration: BoxDecoration( + color: isActionValidated + ? ColorConstants.tertiary + : ColorConstants.background, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: ColorConstants.tertiary.withValues( + alpha: isActionEnabled && !isActionValidated ? 1 : 0.5, + ), + width: 2, + ), + ), + child: Center( + child: Text( + isActionValidated + ? actionValidatedButtonText + : actionEnableButtonText, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: + (isActionValidated + ? ColorConstants.background + : ColorConstants.tertiary) + .withValues( + alpha: isActionEnabled && !isActionValidated + ? 1 + : 0.5, + ), + ), + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/feed/ui/pages/main_page/event_action_admin.dart b/lib/feed/ui/pages/main_page/event_action_admin.dart new file mode 100644 index 0000000000..ddfb4b20d0 --- /dev/null +++ b/lib/feed/ui/pages/main_page/event_action_admin.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/feed/class/news.dart'; +import 'package:titan/feed/providers/admin_news_list_provider.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/ui/builders/waiting_button.dart'; + +class EventActionAdmin extends ConsumerWidget { + final News item; + const EventActionAdmin({super.key, required this.item}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final localizeWithContext = AppLocalizations.of(context)!; + final newsAdminNotifier = ref.watch(adminNewsListProvider.notifier); + return Align( + alignment: Alignment.centerRight, + child: WaitingButton( + onTap: () async => await newsAdminNotifier.rejectNews(item), + builder: (child) => Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), + width: 100, + decoration: BoxDecoration( + color: ColorConstants.main, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: ColorConstants.onMain, width: 2), + ), + child: child, + ), + waitingColor: ColorConstants.background, + child: Center( + child: Text( + localizeWithContext.eventDelete, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: ColorConstants.background, + ), + ), + ), + ), + ); + } +} diff --git a/lib/feed/ui/pages/main_page/event_card.dart b/lib/feed/ui/pages/main_page/event_card.dart new file mode 100644 index 0000000000..586470c624 --- /dev/null +++ b/lib/feed/ui/pages/main_page/event_card.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/advert/router.dart'; +import 'package:titan/feed/class/news.dart'; +import 'package:titan/feed/providers/news_image_provider.dart'; +import 'package:titan/feed/providers/news_images_provider.dart'; +import 'package:titan/feed/tools/news_helper.dart'; +import 'package:titan/feed/ui/widgets/adaptive_text_card.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/providers/path_forwarding_provider.dart'; +import 'package:titan/tools/ui/builders/auto_loader_child.dart'; + +class EventCard extends ConsumerWidget { + final News item; + + const EventCard({super.key, required this.item}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final images = ref.watch( + newsImagesProvider.select((newsImages) => newsImages[item.id]), + ); + final newsImagesNotifier = ref.watch(newsImagesProvider.notifier); + final imageNotifier = ref.watch(newsImageProvider.notifier); + final pathForwardingNotifier = ref.watch(pathForwardingProvider.notifier); + final localizeWithContext = AppLocalizations.of(context)!; + return GestureDetector( + onTap: () { + if (item.module == "advert") { + pathForwardingNotifier.forward(AdvertRouter.root); + QR.to(AdvertRouter.root); + } + }, + + child: AspectRatio( + aspectRatio: 851 / 315, + child: Stack( + children: [ + AutoLoaderChild( + group: images, + notifier: newsImagesNotifier, + mapKey: item.id, + loader: (itemId) => imageNotifier.getNewsImage(itemId), + orElseBuilder: (context, stack) => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + gradient: const LinearGradient( + colors: [ColorConstants.onMain, ColorConstants.main], + begin: Alignment.bottomLeft, + end: Alignment.topRight, + ), + ), + ), + dataBuilder: (context, value) => AdaptiveTextCard( + hasImage: value.isNotEmpty, + imageProvider: value.isNotEmpty ? value.first.image : null, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + image: value.isEmpty + ? null + : DecorationImage( + image: value.first.image, + fit: BoxFit.cover, + ), + gradient: value.isNotEmpty + ? null + : const LinearGradient( + colors: [ + ColorConstants.onMain, + ColorConstants.main, + ], + begin: Alignment.bottomLeft, + end: Alignment.topRight, + ), + ), + ), + ), + ), + if (isNewsTerminated(item) && item.module != "advert") + Positioned( + bottom: 53, + left: 15, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 7, + vertical: 3, + ), + decoration: const BoxDecoration( + color: ColorConstants.main, + borderRadius: BorderRadius.all(Radius.circular(5)), + ), + child: Text( + localizeWithContext.feedEnded, + style: TextStyle( + color: ColorConstants.background, + fontSize: 10, + ), + ), + ), + ), + if (isNewsOngoing(item) && item.module != "advert") + Positioned( + bottom: 53, + left: 15, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 7, + vertical: 3, + ), + decoration: const BoxDecoration( + color: ColorConstants.background, + borderRadius: BorderRadius.all(Radius.circular(5)), + ), + child: Text( + localizeWithContext.feedOngoing, + style: TextStyle(color: ColorConstants.main, fontSize: 10), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/feed/ui/pages/main_page/feed_timeline.dart b/lib/feed/ui/pages/main_page/feed_timeline.dart new file mode 100644 index 0000000000..2db88064d5 --- /dev/null +++ b/lib/feed/ui/pages/main_page/feed_timeline.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:titan/feed/class/news.dart'; +import 'package:titan/feed/ui/pages/main_page/time_line_item.dart'; + +class FeedTimeline extends StatelessWidget { + final List items; + final Function(News item)? onItemTap; + final bool isAdmin; + + const FeedTimeline({ + super.key, + required this.items, + this.onItemTap, + required this.isAdmin, + }); + + @override + Widget build(BuildContext context) { + items.sort((a, b) { + if (a.start == b.start) { + if (a.end == null && b.end == null) return 0; + if (a.end == null) return -1; + if (b.end == null) return 1; + return a.end!.compareTo(b.end!); + } + return a.start.compareTo(b.start); + }); + return Column( + children: [ + ...items.map( + (item) => TimelineItem( + item: item, + onTap: onItemTap != null ? () => onItemTap!(item) : null, + ), + ), + SizedBox(height: 80), + ], + ); + } +} diff --git a/lib/feed/ui/pages/main_page/filter_news.dart b/lib/feed/ui/pages/main_page/filter_news.dart new file mode 100644 index 0000000000..38ddaa35f5 --- /dev/null +++ b/lib/feed/ui/pages/main_page/filter_news.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/feed/providers/filter_state_provider.dart'; +import 'package:titan/feed/providers/news_list_provider.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; +import 'package:titan/tools/ui/styleguide/item_chip.dart'; + +class FilterNewsModal extends HookWidget { + final List entities, modules; + const FilterNewsModal({ + super.key, + required this.entities, + required this.modules, + }); + + @override + Widget build(BuildContext context) { + final localizeWithContext = AppLocalizations.of(context)!; + return HookConsumer( + builder: (context, ref, child) { + final newsListNotifier = ref.watch(newsListProvider.notifier); + final filterState = ref.watch(filterStateProvider); + final filterStateNotifier = ref.watch(filterStateProvider.notifier); + return BottomModalTemplate( + title: localizeWithContext.feedFilter, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(localizeWithContext.feedAssociation), + SizedBox(height: 10), + HorizontalListView( + height: 50, + children: entities + .map( + (entity) => ItemChip( + selected: filterState.selectedEntities.contains(entity), + onTap: () { + if (filterState.selectedEntities.contains(entity)) { + filterStateNotifier.setFilterState( + filterState.copyWith( + selectedEntities: filterState.selectedEntities + ..remove(entity), + ), + ); + } else { + filterStateNotifier.setFilterState( + filterState.copyWith( + selectedEntities: filterState.selectedEntities + ..add(entity), + ), + ); + } + if (filterState.selectedEntities.isEmpty && + filterState.selectedModules.isEmpty) { + newsListNotifier.resetFilters(); + } else { + newsListNotifier.filterNews( + filterState.selectedEntities, + filterState.selectedModules, + ); + } + newsListNotifier.filterNews( + filterState.selectedEntities, + filterState.selectedModules, + ); + }, + child: Text( + entity, + style: TextStyle( + color: filterState.selectedEntities.contains(entity) + ? ColorConstants.background + : ColorConstants.onTertiary, + ), + ), + ), + ) + .toList(), + ), + SizedBox(height: 30), + Text(localizeWithContext.feedNewsType), + SizedBox(height: 10), + HorizontalListView( + height: 50, + children: modules + .map( + (module) => ItemChip( + selected: filterState.selectedModules.contains(module), + onTap: () { + if (filterState.selectedModules.contains(module)) { + filterStateNotifier.setFilterState( + filterState.copyWith( + selectedModules: filterState.selectedModules + ..remove(module), + ), + ); + } else { + filterStateNotifier.setFilterState( + filterState.copyWith( + selectedModules: filterState.selectedModules + ..add(module), + ), + ); + } + if (filterState.selectedEntities.isEmpty && + filterState.selectedModules.isEmpty) { + newsListNotifier.resetFilters(); + } else { + newsListNotifier.filterNews( + filterState.selectedEntities, + filterState.selectedModules, + ); + } + }, + child: Text( + module, + style: TextStyle( + color: filterState.selectedModules.contains(module) + ? ColorConstants.background + : ColorConstants.onTertiary, + ), + ), + ), + ) + .toList(), + ), + SizedBox(height: 40), + Button( + text: localizeWithContext.feedApply, + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/feed/ui/pages/main_page/main_page.dart b/lib/feed/ui/pages/main_page/main_page.dart new file mode 100644 index 0000000000..3077d34d8e --- /dev/null +++ b/lib/feed/ui/pages/main_page/main_page.dart @@ -0,0 +1,248 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/admin/providers/my_association_list_provider.dart'; +import 'package:titan/feed/class/news.dart'; +import 'package:titan/feed/providers/association_event_list_provider.dart'; +import 'package:titan/feed/providers/is_feed_admin_provider.dart'; +import 'package:titan/feed/providers/is_user_a_member_of_an_association.dart'; +import 'package:titan/feed/providers/news_list_provider.dart'; +import 'package:titan/feed/router.dart'; +import 'package:titan/feed/ui/feed.dart'; +import 'package:titan/feed/ui/pages/main_page/feed_timeline.dart'; +import 'package:titan/feed/ui/pages/main_page/filter_news.dart'; +import 'package:titan/feed/ui/pages/main_page/scroll_with_refresh_button.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; +import 'package:titan/tools/ui/styleguide/icon_button.dart'; + +class FeedMainPage extends HookConsumerWidget { + const FeedMainPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final news = ref.watch(newsListProvider); + final newsListNotifier = ref.watch(newsListProvider.notifier); + final isUserAMemberOfAnAssociation = ref.watch( + isUserAMemberOfAnAssociationProvider, + ); + final isFeedAdmin = ref.watch(isFeedAdminProvider); + final scrollController = useScrollController(); + final associationEventsListNotifier = ref.watch( + associationEventsListProvider.notifier, + ); + final myAssociations = ref.watch(myAssociationListProvider); + + final localizeWithContext = AppLocalizations.of(context)!; + + Future onRefresh() async { + await newsListNotifier.loadNewsList(); + } + + useEffect(() { + if (news.hasValue && news.value!.isNotEmpty) { + WidgetsBinding.instance.addPostFrameCallback((_) { + final now = DateTime.now(); + final newsList = news.value!; + + newsList.sort((a, b) { + if (a.start == b.start) { + if (a.end == null && b.end == null) return 0; + if (a.end == null) return -1; + if (b.end == null) return 1; + return a.end!.compareTo(b.end!); + } + return a.start.compareTo(b.start); + }); + + final upcomingIndex = newsList.indexWhere( + (item) => + item.start.isAfter(now) || + (item.end != null && item.end!.isAfter(now)), + ); + + if (upcomingIndex != -1 && scrollController.hasClients) { + double scrollPosition = 0.0; + for (int i = 0; i < upcomingIndex; i++) { + final currentItem = newsList[i]; + + final itemHeight = + (currentItem.actionStart != null || + isUserAMemberOfAnAssociation) + ? 200.0 + : 160.0; + scrollPosition += itemHeight; + } + + scrollController.jumpTo(scrollPosition); + } + }); + } + return null; + }, [news]); + + return FeedTemplate( + child: Stack( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 5), + + Row( + children: [ + Text( + localizeWithContext.feedNews, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorConstants.title, + ), + ), + Spacer(), + IconButton( + icon: HeroIcon( + HeroIcons.adjustmentsHorizontal, + color: ColorConstants.tertiary, + size: 20, + ), + onPressed: () async { + final syncNews = newsListNotifier.allNews.maybeWhen( + orElse: () => [], + data: (loaded) => loaded, + ); + final entities = syncNews + .map((e) => e.entity) + .toSet() + .toList(); + final modules = syncNews + .map((e) => e.module) + .toSet() + .toList(); + await showCustomBottomModal( + modal: FilterNewsModal( + entities: entities, + modules: modules, + ), + context: context, + ref: ref, + ); + }, + splashRadius: 20, + ), + if (isUserAMemberOfAnAssociation || isFeedAdmin) + CustomIconButton( + icon: HeroIcon( + !isFeedAdmin && isUserAMemberOfAnAssociation + ? HeroIcons.pencil + : HeroIcons.userGroup, + color: ColorConstants.background, + ), + onPressed: () { + if (isFeedAdmin && !isUserAMemberOfAnAssociation) { + QR.to(FeedRouter.root + FeedRouter.eventHandling); + } else { + showCustomBottomModal( + modal: BottomModalTemplate( + title: localizeWithContext.feedAdmin, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Button( + text: + localizeWithContext.feedCreateAnEvent, + onPressed: () { + Navigator.of(context).pop(); + QR.to( + FeedRouter.root + + FeedRouter.addEditEvent, + ); + }, + ), + const SizedBox(height: 20), + Button( + text: localizeWithContext + .feedManageAssociationEvents, + onPressed: () { + Navigator.of(context).pop(); + associationEventsListNotifier + .loadAssociationEventList( + myAssociations.first.id, + ); + QR.to( + FeedRouter.root + + FeedRouter.associationEvents, + ); + }, + ), + const SizedBox(height: 20), + if (isFeedAdmin) + Button( + text: localizeWithContext + .feedManageRequests, + onPressed: () { + Navigator.of(context).pop(); + newsListNotifier.loadNewsList(); + QR.to( + FeedRouter.root + + FeedRouter.eventHandling, + ); + }, + ), + ], + ), + ), + context: context, + ref: ref, + ); + } + }, + ), + ], + ), + + const SizedBox(height: 10), + + Expanded( + child: SingleChildScrollView( + controller: scrollController, + physics: const BouncingScrollPhysics(), + child: AsyncChild( + value: news, + builder: (context, news) => news.isEmpty + ? Center( + child: Text( + localizeWithContext.feedNoNewsAvailable, + style: TextStyle( + fontSize: 16, + color: ColorConstants.tertiary, + ), + ), + ) + : FeedTimeline( + isAdmin: isFeedAdmin, + items: news, + onItemTap: (item) {}, + ), + ), + ), + ), + ], + ), + ), + ScrollWithRefreshButton( + controller: scrollController, + onRefresh: onRefresh, + ), + ], + ), + ); + } +} diff --git a/lib/feed/ui/pages/main_page/scroll_with_refresh_button.dart b/lib/feed/ui/pages/main_page/scroll_with_refresh_button.dart new file mode 100644 index 0000000000..5086312d92 --- /dev/null +++ b/lib/feed/ui/pages/main_page/scroll_with_refresh_button.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/providers/navbar_visibility_provider.dart'; +import 'package:titan/tools/constants.dart'; + +class ScrollWithRefreshButton extends HookConsumerWidget { + final ScrollController controller; + final Future Function() onRefresh; + + const ScrollWithRefreshButton({ + super.key, + required this.controller, + required this.onRefresh, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final showRefreshButton = useState(false); + final lastScrollPosition = useState(0.0); + final hasScrolledEnough = useState(false); + + final lastUserScrollTime = useState(DateTime.now()); + final consecutiveUpwardScrolls = useState(0); + + final localizeWithContext = AppLocalizations.of(context)!; + + useEffect(() { + void scrollListener() { + if (!controller.hasClients) return; + + final navbarVisibilityNotifier = ref.read( + navbarVisibilityProvider.notifier, + ); + final position = controller.position; + final currentScrollPosition = position.pixels; + final scrollDirection = + currentScrollPosition - lastScrollPosition.value; + final maxScrollExtent = position.maxScrollExtent; + + if (currentScrollPosition <= 0) { + navbarVisibilityNotifier.show(); + } else if (currentScrollPosition >= maxScrollExtent) { + } else if (scrollDirection > 0) { + navbarVisibilityNotifier.hide(); + } else if (scrollDirection < 0) { + navbarVisibilityNotifier.show(); + } + + final now = DateTime.now(); + + if (scrollDirection.abs() < 3) return; + + final isAtTop = currentScrollPosition <= position.minScrollExtent; + final isAtBottom = currentScrollPosition >= position.maxScrollExtent; + final isInBounceZone = isAtTop || isAtBottom; + + if (currentScrollPosition > 200 && !hasScrolledEnough.value) { + hasScrolledEnough.value = true; + } + + if (scrollDirection < -15) { + final timeSinceLastScroll = now + .difference(lastUserScrollTime.value) + .inMilliseconds; + + if (!isInBounceZone && timeSinceLastScroll > 50) { + consecutiveUpwardScrolls.value++; + lastUserScrollTime.value = now; + + if (hasScrolledEnough.value && + consecutiveUpwardScrolls.value >= 2 && + !showRefreshButton.value) { + showRefreshButton.value = true; + } + } + } else if (scrollDirection > 5) { + consecutiveUpwardScrolls.value = 0; + lastUserScrollTime.value = now; + + if (showRefreshButton.value && currentScrollPosition > 50) { + showRefreshButton.value = false; + } + } + + lastScrollPosition.value = currentScrollPosition; + } + + controller.addListener(scrollListener); + return () => controller.removeListener(scrollListener); + }, []); + + Future handleRefresh() async { + showRefreshButton.value = false; + await onRefresh(); + } + + return AnimatedPositioned( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + top: showRefreshButton.value ? 10 : -10, + left: 0, + right: 0, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + opacity: showRefreshButton.value ? 1.0 : 0.0, + child: Center( + child: GestureDetector( + onTap: handleRefresh, + child: Container( + decoration: BoxDecoration( + color: ColorConstants.main, + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: ColorConstants.onMain.withValues(alpha: 0.4), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], + ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + HeroIcon( + HeroIcons.arrowPath, + size: 16, + color: ColorConstants.background, + ), + const SizedBox(width: 8), + Text( + localizeWithContext.feedRefresh, + style: TextStyle( + color: ColorConstants.background, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/feed/ui/pages/main_page/time_line_item.dart b/lib/feed/ui/pages/main_page/time_line_item.dart new file mode 100644 index 0000000000..95a786046c --- /dev/null +++ b/lib/feed/ui/pages/main_page/time_line_item.dart @@ -0,0 +1,152 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:titan/feed/class/news.dart'; +import 'package:titan/feed/tools/news_helper.dart'; +import 'package:titan/feed/ui/pages/main_page/event_action.dart'; +import 'package:titan/feed/ui/pages/main_page/event_card.dart'; +import 'package:titan/feed/ui/widgets/event_card_text_content.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/feed/ui/pages/main_page/dotted_vertical_line.dart'; + +class TimelineItem extends ConsumerWidget { + final News item; + final VoidCallback? onTap; + + const TimelineItem({super.key, required this.item, this.onTap}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final locale = Localizations.localeOf(context); + final localizeWithContext = AppLocalizations.of(context)!; + + return LayoutBuilder( + builder: (context, constraints) { + final eventCardWidth = constraints.maxWidth - 70; + final eventCardHeight = eventCardWidth / (851 / 315); + + final baseHeight = 30 + eventCardHeight + 20; + + final totalHeight = item.actionStart != null + ? baseHeight + 40 + : baseHeight; + + return SizedBox( + height: totalHeight, + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.only(left: 20), + child: DottedVerticalLine(), + ), + Padding( + padding: const EdgeInsets.only(top: 5), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.only(left: 10, right: 30), + color: ColorConstants.background, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + DateFormat.d( + locale.toString(), + ).format(item.start), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: ColorConstants.main, + ), + ), + Text( + DateFormat.MMM( + locale.toString(), + ).format(item.start).toUpperCase(), + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: ColorConstants.onTertiary, + ), + ), + ], + ), + ), + Expanded( + child: GestureDetector( + onTap: onTap, + child: EventCard(item: item), + ), + ), + ], + ), + Padding( + padding: const EdgeInsets.only(top: 5, left: 45), + child: EventCardTextContent( + item: item, + localizeWithContext: localizeWithContext, + ), + ), + if (item.actionStart != null) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only( + left: 11, + right: 37, + top: 3, + ), + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: ColorConstants.background, + border: Border.all( + color: ColorConstants.secondary, + width: 2, + ), + ), + ), + ), + Expanded( + child: EventAction( + title: getActionTitle(item, context), + waitingTitle: (timeToGo) => getWaitingTitle( + item, + context, + timeToGo: timeToGo, + ), + subtitle: getActionSubtitle(item, context), + onActionPressed: () => + getActionButtonAction(item, context, ref), + actionEnableButtonText: + getActionEnableButtonText(item, context), + actionValidatedButtonText: + getActionValidatedButtonText(item, context), + isActionValidated: false, + eventEnd: item.end, + timeOpening: item.actionStart, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/feed/ui/widgets/adaptive_text_card.dart b/lib/feed/ui/widgets/adaptive_text_card.dart new file mode 100644 index 0000000000..bd0593b885 --- /dev/null +++ b/lib/feed/ui/widgets/adaptive_text_card.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:titan/feed/tools/image_color_utils.dart' as image_color_utils; + +// Provider for managing dominant color state +final dominantColorProvider = + StateNotifierProvider.family< + DominantColorNotifier, + AsyncValue, + ImageProvider? + >((ref, imageProvider) => DominantColorNotifier(imageProvider)); + +class DominantColorNotifier extends StateNotifier> { + final ImageProvider? imageProvider; + + DominantColorNotifier(this.imageProvider) + : super(const AsyncValue.loading()) { + _analyzeDominantColor(); + } + + Future _analyzeDominantColor() async { + if (imageProvider == null) { + state = const AsyncValue.data(null); + return; + } + + try { + state = const AsyncValue.loading(); + final color = await image_color_utils.getDominantColor(imageProvider!); + state = AsyncValue.data(color); + } catch (error, stackTrace) { + state = AsyncValue.error(error, stackTrace); + } + } + + void refresh() { + _analyzeDominantColor(); + } +} + +class AdaptiveTextCard extends HookConsumerWidget { + final Widget child; + final bool hasImage; + final ImageProvider? imageProvider; + + const AdaptiveTextCard({ + super.key, + required this.child, + required this.hasImage, + this.imageProvider, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + // Use a memoized provider key to avoid unnecessary provider rebuilds + final providerKey = useMemoized(() => imageProvider, [imageProvider]); + + // Watch the dominant color state + final dominantColorState = ref.watch(dominantColorProvider(providerKey)); + + return AdaptiveTextProvider( + isAnalyzing: dominantColorState.isLoading, + hasImage: hasImage, + child: child, + ); + } +} + +class AdaptiveTextProvider extends InheritedWidget { + final bool isAnalyzing; + final bool hasImage; + + const AdaptiveTextProvider({ + super.key, + required this.isAnalyzing, + required this.hasImage, + required super.child, + }); + + static AdaptiveTextProvider? of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType(); + } + + @override + bool updateShouldNotify(AdaptiveTextProvider oldWidget) { + return isAnalyzing != oldWidget.isAnalyzing || + hasImage != oldWidget.hasImage; + } +} diff --git a/lib/feed/ui/widgets/event_card_text_content.dart b/lib/feed/ui/widgets/event_card_text_content.dart new file mode 100644 index 0000000000..29878173ca --- /dev/null +++ b/lib/feed/ui/widgets/event_card_text_content.dart @@ -0,0 +1,59 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/feed/class/news.dart'; +import 'package:titan/feed/tools/news_helper.dart'; + +class EventCardTextContent extends ConsumerWidget { + final News item; + final dynamic localizeWithContext; + + const EventCardTextContent({ + super.key, + required this.item, + required this.localizeWithContext, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SizedBox( + width: MediaQuery.of(context).size.width - 140, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + item.title.length > 30 + ? '${item.title.substring(0, 30)}...' + : item.title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w900, + color: Colors.black, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (item.location != null && item.location!.isNotEmpty) + Expanded( + child: Text( + ' | ${item.location}', + style: TextStyle(fontSize: 14, color: Colors.black), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + AutoSizeText( + minFontSize: 10, + maxLines: 1, + getNewsSubtitle(item, context: context), + style: TextStyle(fontSize: 12, color: Colors.black), + ), + ], + ), + ); + } +} diff --git a/lib/flappybird/router.dart b/lib/flappybird/router.dart index 8f771df3cc..c3ffc9fa2c 100644 --- a/lib/flappybird/router.dart +++ b/lib/flappybird/router.dart @@ -1,6 +1,7 @@ -import 'package:either_dart/either.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:titan/drawer/class/module.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/class/module.dart'; import 'package:titan/flappybird/ui/pages/game_page/game_page.dart' deferred as play_page; import 'package:titan/flappybird/ui/pages/leaderboard_page/leaderboard_page.dart' @@ -14,10 +15,10 @@ class FlappyBirdRouter { static const String root = '/flappybird'; static const String leaderBoard = '/leaderboard'; static final Module module = Module( - name: "FlappyBird", - icon: const Right("assets/images/logo_flappybird.svg"), + getName: (context) => AppLocalizations.of(context)!.moduleFlappyBird, + getDescription: (context) => + AppLocalizations.of(context)!.moduleFlappyBirdDescription, root: FlappyBirdRouter.root, - selected: false, ); FlappyBirdRouter(this.ref); @@ -29,6 +30,10 @@ class FlappyBirdRouter { AuthenticatedMiddleware(ref), DeferredLoadingMiddleware(play_page.loadLibrary), ], + pageType: QCustomPage( + transitionsBuilder: (_, animation, _, child) => + FadeTransition(opacity: animation, child: child), + ), children: [ QRoute( path: leaderBoard, diff --git a/lib/flappybird/ui/flappybird_template.dart b/lib/flappybird/ui/flappybird_template.dart index efab4dea41..50fb4e13d7 100644 --- a/lib/flappybird/ui/flappybird_template.dart +++ b/lib/flappybird/ui/flappybird_template.dart @@ -22,7 +22,6 @@ class FlappyBirdTemplate extends HookConsumerWidget { child: Column( children: [ TopBar( - title: "Flappy Bird", root: FlappyBirdRouter.root, textStyle: GoogleFonts.silkscreen( textStyle: const TextStyle( diff --git a/lib/home/router.dart b/lib/home/router.dart index a8fbc48718..f437f5f783 100644 --- a/lib/home/router.dart +++ b/lib/home/router.dart @@ -1,7 +1,7 @@ -import 'package:either_dart/either.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:titan/drawer/class/module.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/class/module.dart'; import 'package:titan/event/ui/pages/detail_page/detail_page.dart' deferred as detail_page; import 'package:titan/home/ui/home.dart' deferred as home_page; @@ -14,10 +14,10 @@ class HomeRouter { static const String root = '/home'; static const String detail = '/detail'; static final Module module = Module( - name: "Calendrier", - icon: const Left(HeroIcons.calendarDays), + getName: (context) => AppLocalizations.of(context)!.moduleCalendar, + getDescription: (context) => + AppLocalizations.of(context)!.moduleCalendarDescription, root: HomeRouter.root, - selected: false, ); HomeRouter(this.ref); @@ -29,6 +29,10 @@ class HomeRouter { AuthenticatedMiddleware(ref), DeferredLoadingMiddleware(home_page.loadLibrary), ], + pageType: QCustomPage( + transitionsBuilder: (_, animation, _, child) => + FadeTransition(opacity: animation, child: child), + ), children: [ QRoute( path: detail, diff --git a/lib/home/tools/constants.dart b/lib/home/tools/constants.dart index 828bc16e30..c4bbc3957c 100644 --- a/lib/home/tools/constants.dart +++ b/lib/home/tools/constants.dart @@ -6,21 +6,3 @@ class HomeColorConstants { static const Color gradient1 = Color(0xFFfb6d10); static const Color gradient2 = Color(0xffeb3e1b); } - -class HomeTextConstants { - static const String calendar = "Calendrier"; - static const String eventOf = "Évènements du"; - static const String incomingEvents = "Évènements à venir"; - static const String lastInfos = "Dernières annonces"; - static const String noEvents = "Aucun évènement"; - - static const Map translateDayShort = { - 'Mon': 'Lun', - 'Tue': 'Mar', - 'Wed': 'Mer', - 'Thu': 'Jeu', - 'Fri': 'Ven', - 'Sat': 'Sam', - 'Sun': 'Dim', - }; -} diff --git a/lib/home/ui/day_card.dart b/lib/home/ui/day_card.dart index 85a0468ea1..77f922d244 100644 --- a/lib/home/ui/day_card.dart +++ b/lib/home/ui/day_card.dart @@ -20,6 +20,7 @@ class DayCard extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final locale = Localizations.localeOf(context).toString(); final selectedDayNotifier = ref.watch(selectedDayProvider.notifier); final selectedDay = ref.watch(selectedDayProvider); return GestureDetector( @@ -81,9 +82,7 @@ class DayCard extends HookConsumerWidget { SizedBox( height: 15, child: Text( - HomeTextConstants.translateDayShort[DateFormat( - 'E', - ).format(day)]!, + DateFormat.E(locale).format(day), textAlign: TextAlign.center, style: TextStyle( color: isToday ? Colors.white : Colors.black, diff --git a/lib/home/ui/home.dart b/lib/home/ui/home.dart index 37a9e157c3..e81bebe4d7 100644 --- a/lib/home/ui/home.dart +++ b/lib/home/ui/home.dart @@ -4,10 +4,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/event/providers/confirmed_event_list_provider.dart'; import 'package:titan/event/providers/sorted_event_list_provider.dart'; import 'package:titan/home/router.dart'; -import 'package:titan/home/tools/constants.dart'; import 'package:titan/home/ui/day_list.dart'; import 'package:titan/home/ui/days_event.dart'; import 'package:titan/home/ui/month_bar.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/constants.dart'; import 'package:titan/tools/ui/widgets/align_left_text.dart'; import 'package:titan/tools/ui/layouts/refresher.dart'; import 'package:titan/tools/ui/widgets/top_bar.dart'; @@ -26,19 +27,17 @@ class HomePage extends HookConsumerWidget { final daysEventScrollController = useScrollController(); return Container( - color: Colors.white, + color: ColorConstants.background, child: SafeArea( child: Refresher( + controller: ScrollController(), onRefresh: () async { await confirmedEventListNotifier.loadConfirmedEvent(); now = DateTime.now(); }, child: Column( children: [ - const TopBar( - title: HomeTextConstants.calendar, - root: HomeRouter.root, - ), + const TopBar(root: HomeRouter.root), const SizedBox(height: 20), MonthBar( scrollController: scrollController, @@ -47,8 +46,8 @@ class HomePage extends HookConsumerWidget { const SizedBox(height: 10), DayList(scrollController, daysEventScrollController), const SizedBox(height: 15), - const AlignLeftText( - HomeTextConstants.incomingEvents, + AlignLeftText( + AppLocalizations.of(context)!.homeIncomingEvents, padding: EdgeInsets.symmetric(horizontal: 30.0), fontSize: 25, ), @@ -70,9 +69,9 @@ class HomePage extends HookConsumerWidget { .values .toList(), ) - : const Center( + : Center( child: Text( - HomeTextConstants.noEvents, + AppLocalizations.of(context)!.homeNoEvents, style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 0000000000..08a18c67c0 --- /dev/null +++ b/lib/l10n/app_en.arb @@ -0,0 +1,1648 @@ +{ + "@@locale": "en", + "dateToday": "Today", + "dateYesterday": "Yesterday", + "dateTomorrow": "Tomorrow", + "dateAt": "at", + "dateFrom": "from", + "dateTo": "to", + "dateBetweenDays": "to", + "dateStarting": "Starting", + "dateLast": "Last", + "dateUntil": "Until", + "feedFilterAll": "All", + "feedFilterPending": "Pending", + "feedFilterApproved": "Approved", + "feedFilterRejected": "Rejected", + "feedEmptyAll": "No events available", + "feedEmptyPending": "No events pending approval", + "feedEmptyApproved": "No approved events", + "feedEmptyRejected": "No rejected events", + "feedEventManagement": "Event Management", + "feedTitle": "Title", + "feedLocation": "Location", + "feedSGDate": "SG Date", + "feedSGExternalLink": "SG External link", + "feedCreateEvent": "Create an event", + "feedNotification": "Send a notification", + "feedPleaseSelectAnAssociation": "Please select an association", + "feedReject": "Reject", + "feedApprove": "Approve", + "feedEnded": "Ended", + "feedOngoing": "Ongoing", + "feedFilter": "Filter", + "feedAssociation": "Association", + "feedAssociationEvent": "{name} event", + "@feedAssociationEvent": { + "description": "Association event", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "feedEditEvent": "Edit event", + "feedManageAssociationEvents": "Manage association events", + "feedNews": "Calendar", + "feedNewsType": "News type", + "feedNoAssociationEvents": "No association events", + "feedApply": "Apply", + "feedAdmin": "Administration", + "feedCreateAnEvent": "Create an event", + "feedManageRequests": "Manage requests", + "feedNoNewsAvailable": "No news available", + "feedRefresh": "Refresh", + "feedPleaseProvideASGExternalLink": "Please provide a SG external link", + "feedPleaseProvideASGDate": "Please provide a SG date", + "feedShotgunIn": "Shotgun in {time}", + "@feedShotgunIn": { + "description": "Shotgun in time", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "feedVoteIn": "Vote in {time}", + "@feedVoteIn": { + "description": "Vote in time", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "feedCantOpenLink": "Can't open link", + "feedGetReady": "Get ready!", + "eventActionCampaign": "You can vote", + "eventActionEvent": "You are invited", + "eventActionCampaignSubtitle": "Vote now", + "eventActionEventSubtitle": "Answer the invitation", + "eventActionCampaignButton": "Vote", + "eventActionEventButton": "Reserve", + "eventActionCampaignValidated": "I voted!", + "eventActionEventValidated": "I'm coming!", + "adminAccountTypes": "Account types", + "adminAdd": "Add", + "adminAddGroup": "Add group", + "adminAddMember": "Add member", + "adminAddedGroup": "Group created", + "adminAddedLoaner": "Lender added", + "adminAddedMember": "Member added", + "adminAddingError": "Error while adding", + "adminAddingMember": "Adding a member", + "adminAddLoaningGroup": "Add loaning group", + "adminAddSchool": "Add school", + "adminAddStructure": "Add structure", + "adminAddedSchool": "School created", + "adminAddedStructure": "Structure added", + "adminEditedStructure": "Structure edited", + "adminAdministration": "Administration", + "adminAssociationMembership": "Membership", + "adminAssociationMembershipName": "Membership name", + "adminAssociationsMemberships": "Memberships", + "adminBankAccountHolder": "Bank account holder: {bankAccountHolder}", + "@adminBankAccountHolder": { + "description": "Displays the bank account holder's name", + "placeholders": { + "bankAccountHolder": { + "type": "String" + } + } + }, + "adminBankAccountHolderModified": "Bank account holder modified", + "adminBankDetails": "Bank details", + "adminBic": "BIC", + "adminBicError": "BIC must be 11 characters", + "adminCity": "City", + "adminClearFilters": "Clear filters", + "adminCountry": "Country", + "adminCreateAssociationMembership": "Create membership", + "adminCreatedAssociationMembership": "Membership created", + "adminCreationError": "Error during creation", + "adminDateError": "Start date must be before end date", + "adminDefineAsBankAccountHolder": "Define as bank account holder", + "adminDelete": "Delete", + "adminDeleteAssociationMember": "Delete member?", + "adminDeleteAssociationMemberConfirmation": "Are you sure you want to delete this member?", + "adminDeleteAssociationMembership": "Delete membership?", + "adminDeletedAssociationMembership": "Membership deleted", + "adminDeleteGroup": "Delete group?", + "adminDeletedGroup": "Group deleted", + "adminDeleteSchool": "Delete school?", + "adminDeletedSchool": "School deleted", + "adminDeleting": "Deleting", + "adminDeletingError": "Error while deleting", + "adminDescription": "Description", + "adminEdit": "Edit", + "adminEditStructure": "Edit structure", + "adminEditMembership": "Edit membership", + "adminEmptyDate": "Empty date", + "adminEmptyFieldError": "Name cannot be empty", + "adminEmailFailed": "Unable to send email to the following addresses", + "adminEmailRegex": "Email Regex", + "adminEmptyUser": "Empty user", + "adminEndDate": "End date", + "adminEndDateMaximal": "Maximum end date", + "adminEndDateMinimal": "Minimum end date", + "adminError": "Error", + "adminFilters": "Filters", + "adminGroup": "Group", + "adminGroups": "Groups", + "adminIban": "IBAN", + "adminIbanError": "IBAN must be 27 characters", + "adminLoaningGroup": "Loaning group", + "adminLooking": "Searching", + "adminManager": "Structure administrator", + "adminMaximum": "Maximum", + "adminMembers": "Members", + "adminMembershipAddingError": "Error while adding (likely due to overlapping dates)", + "adminMemberships": "Memberships", + "adminMembershipUpdatingError": "Error while updating (likely due to overlapping dates)", + "adminMinimum": "Minimum", + "adminModifyModuleVisibility": "Module visibility", + "adminName": "Name", + "adminNoGroup": "No group", + "adminNoManager": "No manager selected", + "adminNoMember": "No member", + "adminNoMoreLoaner": "No lender available", + "adminNoSchool": "No school", + "adminRemoveGroupMember": "Remove member from group?", + "adminResearch": "Search", + "adminSchools": "Schools", + "adminShortId": "Short ID (3 letters)", + "adminShortIdError": "Short ID must be 3 characters", + "adminSiegeAddress": "Head office address", + "adminSiret": "SIRET", + "adminSiretError": "SIRET must be 14 digits", + "adminStreet": "Street and number", + "adminStructures": "Structures", + "adminStartDate": "Start date", + "adminStartDateMaximal": "Maximum start date", + "adminStartDateMinimal": "Minimum start date", + "adminUndefinedBankAccountHolder": "Bank account holder not defined", + "adminUpdatedAssociationMembership": "Membership updated", + "adminUpdatedGroup": "Group updated", + "adminUpdatedMembership": "Membership updated", + "adminUpdatingError": "Error while updating", + "adminUser": "User", + "adminValidateFilters": "Apply filters", + "adminVisibilities": "Visibilities", + "adminZipcode": "Zip code", + "adminGroupNotification": "Group notifications", + "adminNotifyGroup": "Send a notification", + "adminTitle": "Title", + "adminContent": "Content", + "adminSend": "Send", + "adminNotificationSent": "Notification sent", + "adminFailedToSendNotification": "Failed to send notification", + "adminGroupsManagement": "Groups management", + "adminEditGroup": "Edit group", + "adminManageMembers": "Manage members", + "adminDeleteGroupConfirmation": "Are you sure you want to delete this group?", + "adminFailedToDeleteGroup": "Failed to delete group", + "adminUsersAndGroups": "Users and groups", + "adminUsersManagement": "Users management", + "adminUsersManagementDescription": "Manage users, groups, and associations", + "adminManageUserGroups": "Manage user groups", + "adminSendNotificationToGroup": "Send notification to group", + "adminPaiementModule": "Payment module", + "adminPaiement": "Payment", + "adminManagePaiementStructures": "Manage payment structures", + "adminManageUsersAssociationMemberships": "Manage users' association memberships", + "adminAssociationMembershipsManagement": "Association memberships management", + "adminChooseGroupManager": "Choose a group to manage this membership", + "adminSelectManager": "Select a manager", + "adminImportList": "Import a list", + "adminImportUsersDescription": "Import users from a CSV file. The CSV file must contain one email address per line.", + "adminFailedToInviteUsers": "Failed to invite users", + "adminDeleteUsers": "Delete users", + "adminAdmin": "Admin", + "adminAssociations": "Associations", + "adminManageAssociations": "Manage associations", + "adminAddAssociation": "Add association", + "adminAssociationName": "Association name", + "adminSelectGroupAssociationManager": "Select a group to manage this association", + "adminEditAssociation": "Edit association : {associationName}", + "@adminEditAssociation": { + "description": "Edit association", + "placeholders": { + "associationName": { + "type": "String" + } + } + }, + "adminManagerGroup": "Manager group : {groupName}", + "@adminManagerGroup": { + "description": "Manager group", + "placeholders": { + "groupName": { + "type": "String" + } + } + }, + "adminAssociationCreated": "Association created", + "adminAssociationUpdated": "Association updated", + "adminAssociationCreationError": "Error while creating association", + "adminAssociationUpdateError": "Error while updating association", + "adminInvite": "Invite", + "adminInvitedUsers": "Invited users", + "adminInviteUsers": "Invite users", + "adminInviteUsersCounter": "{count, plural, zero {No user} one {{count} user} other {{count} users}} in the CSV file", + "@adminInviteUsersCounter": { + "description": "Text with the number of users in the CSV file", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "adminUpdatedAssociationLogo": "Association logo updated", + "adminTooHeavyLogo": "Logo too heavy, maximum size is 4MB", + "adminFailedToUpdateAssociationLogo": "Failed to update association logo", + "adminChooseGroup": "Choose a group", + "adminChooseAssociationManagerGroup": "Choose a group to manage this association", + "advertAdd": "Add", + "advertAddedAdvert": "Advert published", + "advertAddedAnnouncer": "Announcer added", + "advertAddingError": "Error while adding", + "advertAdmin": "Admin", + "advertAdvert": "Advert", + "advertChoosingAnnouncer": "Please choose an announcer", + "advertChoosingPoster": "Please choose an image", + "advertContent": "Content", + "advertDeleteAdvert": "Delete ad?", + "advertDeleteAnnouncer": "Delete announcer?", + "advertDeleting": "Deleting", + "advertEdit": "Edit", + "advertEditedAdvert": "Advert edited", + "advertEditingError": "Error while editing", + "advertGroupAdvert": "Group", + "advertIncorrectOrMissingFields": "Incorrect or missing fields", + "advertInvalidNumber": "Please enter a number", + "advertManagement": "Management", + "advertModifyAnnouncingGroup": "Edit announcement group", + "advertNoMoreAnnouncer": "No more announcers available", + "advertNoValue": "Please enter a value", + "advertPositiveNumber": "Please enter a positive number", + "advertPublishToFeed": "Publish to feed?", + "advertNotification": "Send a notification", + "advertRemovedAnnouncer": "Announcer removed", + "advertRemovingError": "Error during removal", + "advertTags": "Tags", + "advertTitle": "Title", + "advertMonthJan": "Jan", + "advertMonthFeb": "Feb", + "advertMonthMar": "Mar", + "advertMonthApr": "Apr", + "advertMonthMay": "May", + "advertMonthJun": "Jun", + "advertMonthJul": "Jul", + "advertMonthAug": "Aug", + "advertMonthSep": "Sep", + "advertMonthOct": "Oct", + "advertMonthNov": "Nov", + "advertMonthDec": "Dec", + "amapAccounts": "Accounts", + "amapAdd": "Add", + "amapAddDelivery": "Add delivery", + "amapAddedCommand": "Order added", + "amapAddedOrder": "Order added", + "amapAddedProduct": "Product added", + "amapAddedUser": "User added", + "amapAddProduct": "Add product", + "amapAddUser": "Add user", + "amapAddingACommand": "Add an order", + "amapAddingCommand": "Add the order", + "amapAddingError": "Error while adding", + "amapAddingProduct": "Add a product", + "amapAddOrder": "Add an order", + "amapAdmin": "Admin", + "amapAlreadyExistCommand": "An order already exists for this date", + "amapAmap": "Amap", + "amapAmount": "Balance", + "amapArchive": "Archive", + "amapArchiveDelivery": "Archive", + "amapArchivingDelivery": "Archiving delivery", + "amapCategory": "Category", + "amapCloseDelivery": "Lock", + "amapCommandDate": "Order date", + "amapCommandProducts": "Order products", + "amapConfirm": "Confirm", + "amapContact": "Association contacts", + "amapCreateCategory": "Create category", + "amapDelete": "Delete", + "amapDeleteDelivery": "Delete delivery?", + "amapDeleteDeliveryDescription": "Are you sure you want to delete this delivery?", + "amapDeletedDelivery": "Delivery deleted", + "amapDeletedOrder": "Order deleted", + "amapDeletedProduct": "Product deleted", + "amapDeleteProduct": "Delete product?", + "amapDeleteProductDescription": "Are you sure you want to delete this product?", + "amapDeleting": "Deleting", + "amapDeletingDelivery": "Delete delivery?", + "amapDeletingError": "Error while deleting", + "amapDeletingOrder": "Delete order?", + "amapDeletingProduct": "Delete product?", + "amapDeliver": "Delivery completed?", + "amapDeliveries": "Deliveries", + "amapDeliveringDelivery": "Are all orders delivered?", + "amapDelivery": "Delivery", + "amapDeliveryArchived": "Delivery archived", + "amapDeliveryDate": "Delivery date", + "amapDeliveryDelivered": "Delivery completed", + "amapDeliveryHistory": "Delivery history", + "amapDeliveryList": "Delivery list", + "amapDeliveryLocked": "Delivery locked", + "amapDeliveryOn": "Delivery on", + "amapDeliveryOpened": "Delivery opened", + "amapDeliveryNotArchived": "Delivery not archived", + "amapDeliveryNotLocked": "Delivery not locked", + "amapDeliveryNotDelivered": "Delivery not completed", + "amapDeliveryNotOpened": "Delivery not opened", + "amapEditDelivery": "Edit delivery", + "amapEditedCommand": "Order edited", + "amapEditingError": "Error while editing", + "amapEditProduct": "Edit product", + "amapEndingDelivery": "End of delivery", + "amapError": "Error", + "amapErrorLink": "Error opening link", + "amapErrorLoadingUser": "Error loading users", + "amapEvening": "Evening", + "amapExpectingNumber": "Please enter a number", + "amapFillField": "Please fill out this field", + "amapHandlingAccount": "Manage accounts", + "amapLoading": "Loading...", + "amapLoadingError": "Loading error", + "amapLock": "Lock", + "amapLocked": "Locked", + "amapLockedDelivery": "Delivery locked", + "amapLockedOrder": "Order locked", + "amapLooking": "Search", + "amapLockingDelivery": "Lock delivery?", + "amapMidDay": "Midday", + "amapMyOrders": "My orders", + "amapName": "Name", + "amapNextStep": "Next step", + "amapNoProduct": "No product", + "amapNoCurrentOrder": "No current order", + "amapNoMoney": "Not enough money", + "amapNoOpennedDelivery": "No open delivery", + "amapNoOrder": "No order", + "amapNoSelectedDelivery": "No delivery selected", + "amapNotEnoughMoney": "Not enough money", + "amapNotPlannedDelivery": "No scheduled delivery", + "amapOneOrder": "order", + "amapOpenDelivery": "Open", + "amapOpened": "Opened", + "amapOpenningDelivery": "Open delivery?", + "amapOrder": "Order", + "amapOrders": "Orders", + "amapPickChooseCategory": "Please enter a value or choose an existing category", + "amapPickDeliveryMoment": "Choose a delivery time", + "amapPresentation": "Presentation", + "amapPresentation1": "The AMAP (association for the preservation of small-scale farming) is a service offered by the Planet&Co association of ECL. You can receive products (fruit and vegetable baskets, juices, jams...) directly on campus!\n\nOrders must be placed before Friday at 9 PM and are delivered on campus on Tuesday from 1:00 PM to 1:45 PM (or from 6:15 PM to 6:30 PM if you can't come at midday) in the M16 hall.\n\nYou can only order if your balance allows it. You can top up your balance via the Lydia collection or by cheque during office hours.\n\nLydia top-up link: ", + "amapPresentation2": "\n\nFeel free to contact us if you have any issues!", + "amapPrice": "Price", + "amapProduct": "product", + "amapProducts": "Products", + "amapProductInDelivery": "Product in an unfinished delivery", + "amapQuantity": "Quantity", + "amapRequiredDate": "Date is required", + "amapSeeMore": "See more", + "amapThe": "The", + "amapUnlock": "Unlock", + "amapUnlockedDelivery": "Delivery unlocked", + "amapUnlockingDelivery": "Unlock delivery?", + "amapUpdate": "Edit", + "amapUpdatedAmount": "Balance updated", + "amapUpdatedOrder": "Order updated", + "amapUpdatedProduct": "Product updated", + "amapUpdatingError": "Update failed", + "amapUsersNotFound": "No users found", + "amapWaiting": "Pending", + "bookingAdd": "Add", + "bookingAddBookingPage": "Request", + "bookingAddRoom": "Add room", + "bookingAddBooking": "Add booking", + "bookingAddedBooking": "Request added", + "bookingAddedRoom": "Room added", + "bookingAddedManager": "Manager added", + "bookingAddingError": "Error while adding", + "bookingAddManager": "Add manager", + "bookingAdminPage": "Admin", + "bookingAllDay": "All day", + "bookingBookedFor": "Booked for", + "bookingBooking": "Booking", + "bookingBookingCreated": "Booking created", + "bookingBookingDemand": "Booking request", + "bookingBookingNote": "Booking note", + "bookingBookingPage": "Booking", + "bookingBookingReason": "Booking reason", + "bookingBy": "by", + "bookingConfirm": "Confirm", + "bookingConfirmation": "Confirmation", + "bookingConfirmBooking": "Confirm the booking?", + "bookingConfirmed": "Confirmed", + "bookingDates": "Dates", + "bookingDecline": "Decline", + "bookingDeclineBooking": "Decline the booking?", + "bookingDeclined": "Declined", + "bookingDelete": "Delete", + "bookingDeleting": "Deleting", + "bookingDeleteBooking": "Deleting", + "bookingDeleteBookingConfirmation": "Are you sure you want to delete this booking?", + "bookingDeletedBooking": "Booking deleted", + "bookingDeletedRoom": "Room deleted", + "bookingDeletedManager": "Manager deleted", + "bookingDeleteRoomConfirmation": "Are you sure you want to delete this room?\n\nThe room must have no current or upcoming bookings to be deleted", + "bookingDeleteManagerConfirmation": "Are you sure you want to delete this manager?\n\nThe manager must not be associated with any room to be deleted", + "bookingDeletingBooking": "Delete the booking?", + "bookingDeletingError": "Error while deleting", + "bookingDeletingRoom": "Delete the room?", + "bookingEdit": "Edit", + "bookingEditBooking": "Edit a booking", + "bookingEditionError": "Error while editing", + "bookingEditedBooking": "Booking edited", + "bookingEditedRoom": "Room edited", + "bookingEditedManager": "Manager edited", + "bookingEditManager": "Edit or delete a manager", + "bookingEditRoom": "Edit or delete a room", + "bookingEndDate": "End date", + "bookingEndHour": "End hour", + "bookingEntity": "For whom?", + "bookingError": "Error", + "bookingEventEvery": "Every", + "bookingHistoryPage": "History", + "bookingIncorrectOrMissingFields": "Incorrect or missing fields", + "bookingInterval": "Interval", + "bookingInvalidIntervalError": "Invalid interval", + "bookingInvalidDates": "Invalid dates", + "bookingInvalidRoom": "Invalid room", + "bookingKeysRequested": "Keys requested", + "bookingManagement": "Management", + "bookingManager": "Manager", + "bookingManagerName": "Manager name", + "bookingMultipleDay": "Multiple days", + "bookingMyBookings": "My bookings", + "bookingNecessaryKey": "Key needed", + "bookingNext": "Next", + "bookingNo": "No", + "bookingNoCurrentBooking": "No current booking", + "bookingNoDateError": "Please choose a date", + "bookingNoAppointmentInReccurence": "No slot exists with these recurrence settings", + "bookingNoDaySelected": "No day selected", + "bookingNoDescriptionError": "Please enter a description", + "bookingNoKeys": "No keys", + "bookingNoNoteError": "Please enter a note", + "bookingNoPhoneRegistered": "Number not provided", + "bookingNoReasonError": "Please enter a reason", + "bookingNoRoomFoundError": "No room registered", + "bookingNoRoomFound": "No room found", + "bookingNote": "Note", + "bookingOther": "Other", + "bookingPending": "Pending", + "bookingPrevious": "Previous", + "bookingReason": "Reason", + "bookingRecurrence": "Recurrence", + "bookingRecurrenceDays": "Recurrence days", + "bookingRecurrenceEndDate": "Recurrence end date", + "bookingRecurrent": "Recurrent", + "bookingRegisteredRooms": "Registered rooms", + "bookingRoom": "Room", + "bookingRoomName": "Room name", + "bookingStartDate": "Start date", + "bookingStartHour": "Start hour", + "bookingWeeks": "Weeks", + "bookingYes": "Yes", + "bookingWeekDayMon": "Monday", + "bookingWeekDayTue": "Tuesday", + "bookingWeekDayWed": "Wednesday", + "bookingWeekDayThu": "Thursday", + "bookingWeekDayFri": "Friday", + "bookingWeekDaySat": "Saturday", + "bookingWeekDaySun": "Sunday", + "cinemaAdd": "Add", + "cinemaAddedSession": "Session added", + "cinemaAddingError": "Error while adding", + "cinemaAddSession": "Add a session", + "cinemaCinema": "Cinema", + "cinemaDeleteSession": "Delete the session?", + "cinemaDeleting": "Deleting", + "cinemaDuration": "Duration", + "cinemaEdit": "Edit", + "cinemaEditedSession": "Session edited", + "cinemaEditingError": "Error while editing", + "cinemaEditSession": "Edit the session", + "cinemaEmptyUrl": "Please enter a URL", + "cinemaImportFromTMDB": "Import from TMDB", + "cinemaIncomingSession": "Now showing", + "cinemaIncorrectOrMissingFields": "Incorrect or missing fields", + "cinemaInvalidUrl": "Invalid URL", + "cinemaGenre": "Genre", + "cinemaName": "Name", + "cinemaNoDateError": "Please enter a date", + "cinemaNoDuration": "Please enter a duration", + "cinemaNoOverview": "No synopsis", + "cinemaNoPoster": "No poster", + "cinemaNoSession": "No session", + "cinemaOverview": "Synopsis", + "cinemaPosterUrl": "Poster URL", + "cinemaSessionDate": "Session day", + "cinemaStartHour": "Start hour", + "cinemaTagline": "Tagline", + "cinemaThe": "The", + "drawerAdmin": "Administration", + "drawerAndroidAppLink": "https://play.google.com/store/apps/details?id=fr.myecl.titan", + "drawerCopied": "Copied!", + "drawerDownloadAppOnMobileDevice": "This site is the web version of the MyECL app. We invite you to download the app. Use this site only if you have problems with the app.\n", + "drawerIosAppLink": "https://apps.apple.com/fr/app/myecl/id6444443430", + "drawerLoginOut": "Do you want to log out?", + "drawerLogOut": "Log out", + "drawerOr": " or ", + "drawerSettings": "Settings", + "eventAdd": "Add", + "eventAddEvent": "Add an event", + "eventAddedEvent": "Event added", + "eventAddingError": "Error while adding", + "eventAllDay": "All day", + "eventConfirm": "Confirm", + "eventConfirmEvent": "Confirm the event?", + "eventConfirmation": "Confirmation", + "eventConfirmed": "Confirmed", + "eventDates": "Dates", + "eventDecline": "Decline", + "eventDeclineEvent": "Decline the event?", + "eventDeclined": "Declined", + "eventDelete": "Delete", + "eventDeleteConfirm": "Delete the event {name}?", + "@eventDeleteConfirm": { + "description": "Delete the event with its name", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "eventDeletedEvent": "Event deleted", + "eventDeleting": "Deleting", + "eventDeletingError": "Error while deleting", + "eventDeletingEvent": "Delete the event?", + "eventDescription": "Description", + "eventEdit": "Edit", + "eventEditEvent": "Edit an event", + "eventEditedEvent": "Event edited", + "eventEditingError": "Error while editing", + "eventEndDate": "End date", + "eventEndHour": "End hour", + "eventError": "Error", + "eventEventList": "Event list", + "eventEventType": "Event type", + "eventEvery": "Every", + "eventHistory": "History", + "eventIncorrectOrMissingFields": "Some fields are incorrect or missing", + "eventInterval": "Interval", + "eventInvalidDates": "End date must be after start date", + "eventInvalidIntervalError": "Please enter a valid interval", + "eventLocation": "Location", + "eventModifiedEvent": "Event modified", + "eventModifyingError": "Error while modifying", + "eventMyEvents": "My events", + "eventName": "Name", + "eventNext": "Next", + "eventNo": "No", + "eventNoCurrentEvent": "No current event", + "eventNoDateError": "Please enter a date", + "eventNoDaySelected": "No day selected", + "eventNoDescriptionError": "Please enter a description", + "eventNoEvent": "No event", + "eventNoNameError": "Please enter a name", + "eventNoOrganizerError": "Please enter an organizer", + "eventNoPlaceError": "Please enter a location", + "eventNoPhoneRegistered": "Number not provided", + "eventNoRuleError": "Please enter a recurrence rule", + "eventOrganizer": "Organizer", + "eventOther": "Other", + "eventPending": "Pending", + "eventPrevious": "Previous", + "eventRecurrence": "Recurrence", + "eventRecurrenceDays": "Recurrence days", + "eventRecurrenceEndDate": "Recurrence end date", + "eventRecurrenceRule": "Recurrence rule", + "eventRoom": "Room", + "eventStartDate": "Start date", + "eventStartHour": "Start hour", + "eventTitle": "Events", + "eventYes": "Yes", + "eventEventEvery": "Every", + "eventWeeks": "weeks", + "eventDayMon": "Monday", + "eventDayTue": "Tuesday", + "eventDayWed": "Wednesday", + "eventDayThu": "Thursday", + "eventDayFri": "Friday", + "eventDaySat": "Saturday", + "eventDaySun": "Sunday", + "globalConfirm": "Confirm", + "globalCancel": "Cancel", + "globalIrreversibleAction": "This action is irreversible", + "globalOptionnal": "{text} (Optional)", + "@globalOptionnal": { + "description": "Text with optional complement", + "placeholders": { + "text": { + "type": "String" + } + } + }, + "homeCalendar": "Calendar", + "homeEventOf": "Events of", + "homeIncomingEvents": "Upcoming events", + "homeLastInfos": "Latest announcements", + "homeNoEvents": "No events", + "homeTranslateDayShortMon": "Mon", + "homeTranslateDayShortTue": "Tue", + "homeTranslateDayShortWed": "Wed", + "homeTranslateDayShortThu": "Thu", + "homeTranslateDayShortFri": "Fri", + "homeTranslateDayShortSat": "Sat", + "homeTranslateDayShortSun": "Sun", + "loanAdd": "Add", + "loanAddLoan": "Add a loan", + "loanAddObject": "Add an object", + "loanAddedLoan": "Loan added", + "loanAddedObject": "Object added", + "loanAddedRoom": "Room added", + "loanAddingError": "Error while adding", + "loanAdmin": "Administrator", + "loanAvailable": "Available", + "loanAvailableMultiple": "Available", + "loanBorrowed": "Borrowed", + "loanBorrowedMultiple": "Borrowed", + "loanAnd": "and", + "loanAssociation": "Association", + "loanAvailableItems": "Available items", + "loanBeginDate": "Loan start date", + "loanBorrower": "Borrower", + "loanCaution": "Deposit", + "loanCancel": "Cancel", + "loanConfirm": "Confirm", + "loanConfirmation": "Confirmation", + "loanDates": "Dates", + "loanDays": "Days", + "loanDelay": "Extension delay", + "loanDelete": "Delete", + "loanDeletingLoan": "Delete the loan?", + "loanDeletedItem": "Object deleted", + "loanDeletedLoan": "Loan deleted", + "loanDeleting": "Deleting", + "loanDeletingError": "Error while deleting", + "loanDeletingItem": "Delete the object?", + "loanDuration": "Duration", + "loanEdit": "Edit", + "loanEditItem": "Edit the object", + "loanEditLoan": "Edit the loan", + "loanEditedRoom": "Room edited", + "loanEndDate": "Loan end date", + "loanEnded": "Ended", + "loanEnterDate": "Please enter a date", + "loanExtendedLoan": "Extended loan", + "loanExtendingError": "Error while extending", + "loanHistory": "History", + "loanIncorrectOrMissingFields": "Some fields are missing or incorrect", + "loanInvalidNumber": "Please enter a number", + "loanInvalidDates": "Dates are not valid", + "loanItem": "Item", + "loanItems": "Items", + "loanItemHandling": "Item management", + "loanItemSelected": "selected item", + "loanItemsSelected": "selected items", + "loanLendingDuration": "Possible loan duration", + "loanLoan": "Loan", + "loanLoanHandling": "Loan management", + "loanLooking": "Searching", + "loanName": "Name", + "loanNext": "Next", + "loanNo": "No", + "loanNoAssociationsFounded": "No associations found", + "loanNoAvailableItems": "No available items", + "loanNoBorrower": "No borrower", + "loanNoItems": "No items", + "loanNoItemSelected": "No item selected", + "loanNoLoan": "No loan", + "loanNoReturnedDate": "No return date", + "loanQuantity": "Quantity", + "loanNone": "None", + "loanNote": "Note", + "loanNoValue": "Please enter a value", + "loanOnGoing": "Ongoing", + "loanOnGoingLoan": "Ongoing loan", + "loanOthers": "others", + "loanPaidCaution": "Deposit paid", + "loanPositiveNumber": "Please enter a positive number", + "loanPrevious": "Previous", + "loanReturned": "Returned", + "loanReturnedLoan": "Returned loan", + "loanReturningError": "Error while returning", + "loanReturningLoan": "Return", + "loanReturnLoan": "Return the loan?", + "loanReturnLoanDescription": "Do you want to return this loan?", + "loanToReturn": "To return", + "loanUnavailable": "Unavailable", + "loanUpdate": "Edit", + "loanUpdatedItem": "Item updated", + "loanUpdatedLoan": "Loan updated", + "loanUpdatingError": "Error while updating", + "loanYes": "Yes", + "loginAppName": "MyECL", + "loginCreateAccount": "Create an account", + "loginForgotPassword": "Forgot password?", + "loginFruitVegetableOrders": "Fruit and vegetable orders", + "loginInterfaceCustomization": "Interface customization", + "loginLoginFailed": "Login failed", + "loginMadeBy": "Developped by ProximApp", + "loginMaterialLoans": "Material loans management", + "loginNewTermsElections": "New terms elections", + "loginRaffles": "Raffles", + "loginRegister": "Register", + "loginShortDescription": "The associative application", + "loginSignIn": "Sign in", + "loginUpcomingEvents": "Upcoming events", + "loginUpcomingScreenings": "Upcoming screenings", + "othersCheckInternetConnection": "Please check your internet connection", + "othersRetry": "Retry", + "othersTooOldVersion": "Your app version is too old.\n\nPlease update the app.", + "othersUnableToConnectToServer": "Unable to connect to the server", + "othersVersion": "Version", + "othersNoModule": "No modules available, please try again later 😢😢", + "othersAdmin": "Admin", + "othersError": "An error occurred", + "othersNoValue": "Please enter a value", + "othersInvalidNumber": "Please enter a number", + "othersNoDateError": "Please enter a date", + "othersImageSizeTooBig": "Image size must not exceed 4 MB", + "othersImageError": "Error adding the image", + "paiementAccept": "Accept", + "paiementAccessPage": "Access the page", + "paiementAdd": "Add", + "paiementAddedSeller": "Seller added", + "paiementAddingSellerError": "Error while adding seller", + "paiementAddingStoreError": "Error while adding the store", + "paiementAddSeller": "Add seller", + "paiementAddStore": "Add store", + "paiementAddThisDevice": "Add this device", + "paiementAdmin": "Administrator", + "paiementAmount": "Amount", + "paiementAskDeviceActivation": "Device activation request", + "paiementAStore": "a store", + "paiementAt": "at", + "paiementAuthenticationRequired": "Authentication required to pay", + "paiementAuthentificationFailed": "Authentication failed", + "paiementBalanceAfterTopUp": "Balance after top-up:", + "paiementBalanceAfterTransaction": "Balance after payment: ", + "paiementBank": "Collect", + "paiementBillingSpace": "Billing page", + "paiementCameraPermissionRequired": "Camera permission required", + "paiementCameraPerssionRequiredDescription": "To scan a QR Code, you must allow camera access.", + "paiementCanBank": "Can collect payments", + "paiementCanCancelTransaction": "Can cancel transactions", + "paiementCancel": "Cancel", + "paiementCancelled": "Cancelled", + "paiementCancelledTransaction": "Payment cancelled", + "paiementCancelTransaction": "Cancel transaction", + "paiementCancelTransactions": "Cancel transactions", + "paiementCanManageSellers": "Can manage sellers", + "paiementCanSeeHistory": "Can view history", + "paiementCantLaunchURL": "Can't open link", + "paiementClose": "Close", + "paiementCreate": "Create", + "paiementCreateInvoice": "Create new invoice", + "paiementDecline": "Decline", + "paiementDeletedSeller": "Seller deleted", + "paiementDeleteInvoice": "Delete invoice", + "paiementDeleteSeller": "Delete seller", + "paiementDeleteSellerDescription": "Are you sure you want to delete this seller?", + "paiementDeleteSuccessfully": "Successfully deleted", + "paiementDeleteStore": "Delete store", + "paiementDeleteStoreDescription": "Are you sure you want to delete this store?", + "paiementDeleteStoreError": "Unable to delete the store", + "paiementDeletingSellerError": "Error while deleting seller", + "paiementDeviceActivationReceived": "The activation request has been received, please check your email to finalize the process", + "paiementDeviceNotActivated": "Device not activated", + "paiementDeviceNotActivatedDescription": "Your device is not yet activated. \nTo activate it, please go to the devices page.", + "paiementDeviceNotRegistered": "Device not registered", + "paiementDeviceNotRegisteredDescription": "Your device is not registered yet. \nTo register it, please go to the devices page.", + "paiementDeviceRecoveryError": "Error while retrieving device", + "paiementDeviceRevoked": "Device revoked", + "paiementDeviceRevokingError": "Error while revoking device", + "paiementDevices": "Devices", + "paiementDoneTransaction": "Transaction completed", + "paiementDownload": "Download", + "paiementEditStore": "Edit store {store}", + "@paiementEditStore": { + "description": "Text to edit a store", + "placeholders": { + "store": { + "type": "String" + } + } + }, + "paiementErrorDeleting": "Error while deleting", + "paiementErrorUpdatingStatus": "Error while updating the status", + "paiementFromTo": "From {from} to {to}", + "@paiementFromTo": { + "description": "Text with a date range", + "placeholders": { + "from": { + "type": "DateTime", + "format": "yMd" + }, + "to": { + "type": "DateTime", + "format": "yMd" + } + } + }, + "paiementGetBalanceError": "Error while retrieving balance: ", + "paiementGetTransactionsError": "Error while retrieving transactions: ", + "paiementHandOver": "Handover", + "paiementHistory": "History", + "paiementInvoiceCreatedSuccessfully": "Invoice created successfully", + "paiementInvoices": "Invoices", + "paiementInvoicesPerPage": "{quantity} invoices/page", + "@paiementInvoicesPerPage": { + "description": "Text with the number of invoices per page", + "placeholders": { + "quantity": { + "type": "int" + } + } + }, + "paiementLastTransactions": "Latest transactions", + "paiementLimitedTo": "Limited to", + "paiementManagement": "Management", + "paiementManageSellers": "Manage sellers", + "paiementMarkPaid": "Mark as paid", + "paiementMarkReceived": "Mark as received", + "paiementMarkUnpaid": "Mark as unpaid", + "paiementMaxAmount": "The maximum wallet amount is", + "paiementMean": "Average: ", + "paiementModify": "Edit", + "paiementModifyingStoreError": "Error while updating the store", + "paiementModifySuccessfully": "Successfully modified", + "paiementNewCGU": "New Terms of Service", + "paiementNext": "Next", + "paiementNextAccountable": "Next responsible", + "paiementNoInvoiceToCreate": "No invoice to create", + "paiementNoMembership": "No membership", + "paiementNoMembershipDescription": "This product is not available to non-members. Confirm the payment?", + "paiementNoThanks": "No thanks", + "paiementNoTransaction": "No transaction", + "paiementNoTransactionForThisMonth": "No transactions for this month", + "paiementOf": "of", + "paiementPaid": "Paid", + "paiementPay": "Pay", + "paiementPayment": "Payment", + "paiementPayWithHA": "Pay with HelloAsso", + "paiementPending": "Pending", + "paiementPersonalBalance": "Personal balance", + "paiementAddFunds": "Add Funds", + "paiementInsufficientFunds": "Insufficient Funds", + "paiementTimeRemaining": "Time Remaining", + "paiementHurryUp": "Hurry up!", + "paiementCompletePayment": "Complete payment", + "paiementConfirmPayment": "Confirm Payment", + "paiementPleaseAcceptPopup": "Please allow popups", + "paiementPleaseAcceptTOS": "Please accept the Terms of Service.", + "paiementPleaseAddDevice": "Please add this device to pay", + "paiementPleaseAuthenticate": "Please authenticate", + "paiementPleaseEnterMinAmount": "Please enter an amount greater than 1", + "paiementPleaseEnterValidAmount": "Please enter a valid amount", + "paiementProceedSuccessfully": "Payment completed successfully", + "paiementQRCodeAlreadyUsed": "QR Code already used", + "paiementReactivateRevokedDeviceDescription": "Your device has been revoked. \nTo reactivate it, please go to the devices page.", + "paiementReceived": "Received", + "paiementRefund": "Refund", + "paiementRefundAction": "Refund", + "paiementRefundedThe": "Refunded on", + "paiementRevokeDevice": "Revoke device?", + "paiementRevokeDeviceDescription": "You will no longer be able to use this device for payments", + "paiementRightsOf": "Rights of", + "paiementRightsUpdated": "Rights updated", + "paiementRightsUpdateError": "Error while updating rights", + "paiementScan": "Scan", + "paiementScanAlreadyUsedQRCode": "QR Code already used", + "paiementScanCode": "Scan a code", + "paiementScanNoMembership": "No membership", + "paiementScanNoMembershipConfirmation": "This product is not available to non-members. Confirm the payment?", + "paiementSeeHistory": "View history", + "paiementSelectStructure": "Select a structure", + "paiementSellerError": "You are not a seller of this store", + "paiementSellerRigths": "Seller rights", + "paiementSellersOf": "Sellers of", + "paiementSettings": "Settings", + "paiementSpent": "Spent", + "paiementStats": "Stats", + "paiementStoreBalance": "Store balance", + "paiementStoreDeleted": "Store deleted", + "paiementStructureManagement": "{structure} management", + "@paiementStructureManagement": { + "description": "Gestion de la structure", + "placeholders": { + "structure": { + "type": "String" + } + } + }, + "paiementStoreName": "Store name", + "paiementStores": "Stores", + "paiementStructureAdmin": "Structure administrator", + "paiementSuccededTransaction": "Successful payment", + "paiementConfirmYourPurchase": "Confirm your purchase", + "paiementYourBalance": "Your balance", + "paiementPaymentSuccessful": "Payment successful!", + "paiementPaymentCanceled": "Payment canceled", + "paiementPaymentRequest": "Payment request", + "paiementPaymentRequestAccepted": "Payment request accepted", + "paiementPaymentRequestRefused": "Payment request refused", + "paiementPaymentRequestError": "Error processing payment request", + "paiementAccept": "Accept", + "paiementRefuse": "Refuse", + "paiementSuccessfullyAddedStore": "Store successfully added", + "paiementSuccessfullyModifiedStore": "Store successfully updated", + "paiementThe": "The", + "paiementThisDevice": "(this device)", + "paiementTopUp": "Top-up", + "paiementTopUpAction": "Top-up", + "paiementTotalDuringPeriod": "Total during the period", + "paiementTransaction": "Transaction", + "paiementTransactionCancelled": "Transaction cancelled", + "paiementTransactionCancelledDescription": "Are you sure you want to cancel the transaction of", + "paiementTransactionCancelledError": "Error while cancelling the transaction", + "paiementTransferStructure": "Structure transfer", + "paiementTransferStructureDescription": "The new manager will have access to all structure management features. You will receive an email to confirm this transfer. The link will only be active for 20 minutes. This action is irreversible. Are you sure you want to continue?", + "paiementTransferStructureError": "Error while transferring structure", + "paiementTransferStructureSuccess": "Structure transfer requested successfully", + "paiementUnknownDevice": "Unknown device", + "paiementValidUntil": "Valid until", + "paiementYouAreTransferingStructureTo": "You are about to transfer the structure to ", + "phAddNewJournal": "Add a new journal", + "phNameField": "Name: ", + "phDateField": "Date: ", + "phDelete": "Are you sure you want to delete this journal?", + "phIrreversibleAction": "This action is irreversible", + "phToHeavyFile": "File too large", + "phAddPdfFile": "Add a PDF file", + "phEditPdfFile": "Edit PDF file", + "phPhName": "PH name", + "phDate": "Date", + "phAdded": "Added", + "phEdited": "Edited", + "phAddingFileError": "Add error", + "phMissingInformatonsOrPdf": "Missing information or PDF file", + "phAdd": "Add", + "phEdit": "Edit", + "phSeePreviousJournal": "See previous journals", + "phNoJournalInDatabase": "No PH yet in database", + "phSuccesDowloading": "Successfully downloaded", + "phonebookAdd": "Add", + "phonebookAddAssociation": "Add an association", + "phonebookAddAssociationGroupement": "Add an association groupement", + "phonebookAddedAssociation": "Association added", + "phonebookAddedMember": "Member added", + "phonebookAddingError": "Error adding", + "phonebookAddMember": "Add a member", + "phonebookAddRole": "Add a role", + "phonebookAdmin": "Admin", + "phonebookAll": "All", + "phonebookApparentName": "Public role name:", + "phonebookAssociation": "Association", + "phonebookAssociationDetail": "Association details:", + "phonebookAssociationGroupement": "Association groupement", + "phonebookAssociationKind": "Type of association:", + "phonebookAssociationName": "Association name", + "phonebookAssociations": "Associations", + "phonebookCancel": "Cancel", + "phonebookChangeTermYear": "Switch to {year} term", + "@phonebookChangeTermYear": { + "description": "Change the term year of the association", + "placeholders": { + "year": { + "type": "int" + } + } + }, + "phonebookChangeTermConfirm": "Are you sure you want to change the entire term?\nThis action is irreversible!", + "phonebookClose": "Close", + "phonebookConfirm": "Confirm", + "phonebookCopied": "Copied to clipboard", + "phonebookDeactivateAssociation": "Deactivate association", + "phonebookDeactivatedAssociation": "Association deactivated", + "phonebookDeactivatedAssociationWarning": "Warning, this association is deactivated, you cannot modify it", + "phonebookDeactivateSelectedAssociation": "Désactiver l'association {association} ?", + "@phonebookDeactivateSelectedAssociation": { + "description": "Permet de désactiver une association", + "placeholders": { + "association": { + "type": "String" + } + } + }, + "phonebookDeactivatingError": "Error during deactivation", + "phonebookDetail": "Details:", + "phonebookDelete": "Delete", + "phonebookDeleteAssociation": "Delete association", + "phonebookDeleteSelectedAssociation": "Delete the association {association}?", + "@phonebookDeleteSelectedAssociation": { + "description": "Delete an association", + "placeholders": { + "association": { + "type": "String" + } + } + }, + "phonebookDeleteAssociationDescription": "This will erase all association history", + "phonebookDeletedAssociation": "Association deleted", + "phonebookDeletedMember": "Member deleted", + "phonebookDeleteRole": "Delete role", + "phonebookDeleteUserRole": "Delete the role of {name}?", + "@phonebookDeleteUserRole": { + "description": "Delete the role of a user", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "phonebookDeactivating": "Deactivate the association?", + "phonebookDeleting": "Deleting", + "phonebookDeletingError": "Error deleting", + "phonebookDescription": "Description", + "phonebookEdit": "Edit", + "phonebookEditAssociationGroupement": "Edit association groupement", + "phonebookEditAssociationGroups": "Manage groups", + "phonebookEditAssociationInfo": "Edit", + "phonebookEditAssociationMembers": "Manage members", + "phonebookEditRole": "Edit role", + "phonebookEditMembership": "Edit role", + "phonebookEmail": "Email:", + "phonebookEmailCopied": "Email copied to clipboard", + "phonebookEmptyApparentName": "Please enter a role name", + "phonebookEmptyFieldError": "A field is not filled", + "phonebookEmptyKindError": "Please choose an association type", + "phonebookEmptyMember": "No member selected", + "phonebookErrorAssociationLoading": "Error loading association", + "phonebookErrorAssociationNameEmpty": "Please enter an association name", + "phonebookErrorAssociationPicture": "Error editing association picture", + "phonebookErrorKindsLoading": "Error loading association types", + "phonebookErrorLoadAssociationList": "Error loading association list", + "phonebookErrorLoadAssociationMember": "Error loading association members", + "phonebookErrorLoadAssociationPicture": "Error loading association picture", + "phonebookErrorLoadProfilePicture": "Error", + "phonebookErrorRoleTagsLoading": "Error loading role tags", + "phonebookExistingMembership": "This member is already in the current term", + "phonebookFilter": "Filter", + "phonebookFilterDescription": "Filter the associations by their type", + "phonebookFirstname": "First name:", + "phonebookGroupementDeleted": "Association groupement deleted", + "phonebookGroupementDeleteError": "Error deleting association groupement", + "phonebookGroupementName": "Groupement name", + "phonebookGroups": "Manage {association} groups", + "@phonebookGroups": { + "description": "Manage the groups of an association", + "placeholders": { + "association": { + "type": "String" + } + } + }, + "phonebookTerm": "{year} term", + "@phonebookTerm": { + "description": "Term year of the association", + "placeholders": { + "year": { + "type": "int" + } + } + }, + "phonebookTermChangingError": "Error changing term", + "phonebookMember": "Member", + "phonebookMemberReordered": "Member reordered", + "phonebookMembers": "Manage {association} members", + "@phonebookMembers": { + "description": "Manage the members of an association", + "placeholders": { + "association": { + "type": "String" + } + } + }, + "phonebookMembershipAssociationError": "Please choose an association", + "phonebookMembershipRole": "Role:", + "phonebookMembershipRoleError": "Please choose a role", + "phonebookModifyMembership": "Modify {name}'s role", + "@phonebookModifyMembership": { + "description": "Modify the role of a member", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "phonebookName": "Last name:", + "phonebookNameCopied": "Name and first name copied to clipboard", + "phonebookNamePure": "Last name", + "phonebookNewTerm": "New term", + "phonebookNewTermConfirmed": "Term changed", + "phonebookNickname": "Nickname:", + "phonebookNicknameCopied": "Nickname copied to clipboard", + "phonebookNoAssociationFound": "No association found", + "phonebookNoMember": "No member", + "phonebookNoMemberRole": "No role found", + "phonebookNoRoleTags": "No role tags found", + "phonebookPhone": "Phone:", + "phonebookPhonebook": "Phonebook", + "phonebookPhonebookSearch": "Search", + "phonebookPhonebookSearchAssociation": "Association", + "phonebookPhonebookSearchField": "Search:", + "phonebookPhonebookSearchName": "Last name/First name/Nickname", + "phonebookPhonebookSearchRole": "Position", + "phonebookPresidentRoleTag": "Prez'", + "phonebookPromoNotGiven": "Promotion not provided", + "phonebookPromotion": "Promotion {year}", + "@phonebookPromotion": { + "description": "Promotion year of the member", + "placeholders": { + "year": { + "type": "int" + } + } + }, + "phonebookReorderingError": "Error during reordering", + "phonebookResearch": "Search", + "phonebookRolePure": "Role", + "phonebookSearchUser": "Search a user", + "phonebookTooHeavyAssociationPicture": "Image is too large (max 4MB)", + "phonebookUpdateGroups": "Update groups", + "phonebookUpdatedAssociation": "Association updated", + "phonebookUpdatedAssociationPicture": "Association picture has been changed", + "phonebookUpdatedGroups": "Groups updated", + "phonebookUpdatedMember": "Member updated", + "phonebookUpdatingError": "Error during update", + "phonebookValidation": "Validate", + "purchasesPurchases": "Purchases", + "purchasesResearch": "Search", + "purchasesNoPurchasesFound": "No purchases found", + "purchasesNoTickets": "No tickets", + "purchasesTicketsError": "Error loading tickets", + "purchasesPurchasesError": "Error loading purchases", + "purchasesNoPurchases": "No purchase", + "purchasesTimes": "times", + "purchasesAlreadyUsed": "Already used", + "purchasesNotPaid": "Not validated", + "purchasesPleaseSelectProduct": "Please select a product", + "purchasesProducts": "Products", + "purchasesCancel": "Cancel", + "purchasesValidate": "Validate", + "purchasesLeftScan": "Scans remaining", + "purchasesTag": "Tag", + "purchasesHistory": "History", + "purchasesPleaseSelectSeller": "Please select a seller", + "purchasesNoTagGiven": "Warning, no tag entered", + "purchasesTickets": "Tickets", + "purchasesNoScannableProducts": "No scannable products", + "purchasesLoading": "Waiting for scan", + "purchasesScan": "Scan", + "raffleRaffle": "Raffle", + "rafflePrize": "Prize", + "rafflePrizes": "Prizes", + "raffleActualRaffles": "Current raffles", + "rafflePastRaffles": "Past raffles", + "raffleYourTickets": "All your tickets", + "raffleCreateMenu": "Creation menu", + "raffleNextRaffles": "Upcoming raffles", + "raffleNoTicket": "You have no ticket", + "raffleSeeRaffleDetail": "View prizes/tickets", + "raffleActualPrize": "Current prizes", + "raffleMajorPrize": "Major prizes", + "raffleTakeTickets": "Take your tickets", + "raffleNoTicketBuyable": "You cannot buy tickets right now", + "raffleNoCurrentPrize": "There are no prizes currently", + "raffleModifTombola": "You can modify your raffles or create new ones, all decisions must then be approved by admins", + "raffleCreateYourRaffle": "Your raffle creation menu", + "rafflePossiblePrice": "Possible prize", + "raffleInformation": "Information and statistics", + "raffleAccounts": "Accounts", + "raffleAdd": "Add", + "raffleUpdatedAmount": "Amount updated", + "raffleUpdatingError": "Error during update", + "raffleDeletedPrize": "Prize deleted", + "raffleDeletingError": "Error during deletion", + "raffleQuantity": "Quantity", + "raffleClose": "Close", + "raffleOpen": "Open", + "raffleAddTypeTicketSimple": "Add", + "raffleAddingError": "Error during addition", + "raffleEditTypeTicketSimple": "Edit", + "raffleFillField": "Field cannot be empty", + "raffleWaiting": "Loading", + "raffleEditingError": "Error during editing", + "raffleAddedTicket": "Ticket added", + "raffleEditedTicket": "Ticket edited", + "raffleAlreadyExistTicket": "Ticket already exists", + "raffleNumberExpected": "An integer is expected", + "raffleDeletedTicket": "Ticket deleted", + "raffleAddPrize": "Add", + "raffleEditPrize": "Edit", + "raffleOpenRaffle": "Open raffle", + "raffleCloseRaffle": "Close raffle", + "raffleOpenRaffleDescription": "You are going to open the raffle, users will be able to buy tickets. You will no longer be able to modify the raffle. Are you sure you want to continue?", + "raffleCloseRaffleDescription": "You are going to close the raffle, users will no longer be able to buy tickets. Are you sure you want to continue?", + "raffleNoCurrentRaffle": "There is no ongoing raffle", + "raffleBoughtTicket": "Ticket purchased", + "raffleDrawingError": "Error during drawing", + "raffleInvalidPrice": "Price must be greater than 0", + "raffleMustBePositive": "Number must be strictly positive", + "raffleDraw": "Draw", + "raffleDrawn": "Drawn", + "raffleError": "Error", + "raffleGathered": "Collected", + "raffleTickets": "Tickets", + "raffleTicket": "ticket", + "raffleWinner": "Winner", + "raffleNoPrize": "No prize", + "raffleDeletePrize": "Delete prize", + "raffleDeletePrizeDescription": "You are going to delete the prize, are you sure you want to continue?", + "raffleDrawing": "Drawing", + "raffleDrawingDescription": "Draw the prize winner?", + "raffleDeleteTicket": "Delete ticket", + "raffleDeleteTicketDescription": "You are going to delete the ticket, are you sure you want to continue?", + "raffleWinningTickets": "Winning tickets", + "raffleNoWinningTicketYet": "Winning tickets will be displayed here", + "raffleName": "Name", + "raffleDescription": "Description", + "raffleBuyThisTicket": "Buy this ticket", + "raffleLockedRaffle": "Locked raffle", + "raffleUnavailableRaffle": "Unavailable raffle", + "raffleNotEnoughMoney": "You don't have enough money", + "raffleWinnable": "winnable", + "raffleNoDescription": "No description", + "raffleAmount": "Balance", + "raffleLoading": "Loading", + "raffleTicketNumber": "Number of tickets", + "rafflePrice": "Price", + "raffleEditRaffle": "Edit raffle", + "raffleEdit": "Edit", + "raffleAddPackTicket": "Add ticket pack", + "recommendationRecommendation": "Recommendation", + "recommendationTitle": "Title", + "recommendationLogo": "Logo", + "recommendationCode": "Code", + "recommendationSummary": "Short summary", + "recommendationDescription": "Description", + "recommendationAdd": "Add", + "recommendationEdit": "Edit", + "recommendationDelete": "Delete", + "recommendationAddImage": "Please add an image", + "recommendationAddedRecommendation": "Deal added", + "recommendationEditedRecommendation": "Deal updated", + "recommendationDeleteRecommendationConfirmation": "Are you sure you want to delete this deal?", + "recommendationDeleteRecommendation": "Delete", + "recommendationDeletingRecommendationError": "Error during deletion", + "recommendationDeletedRecommendation": "Deal deleted", + "recommendationIncorrectOrMissingFields": "Incorrect or missing fields", + "recommendationEditingError": "Edit failed", + "recommendationAddingError": "Add failed", + "recommendationCopiedCode": "Discount code copied", + "seedLibraryAdd": "Add", + "seedLibraryAddedPlant": "Plant added", + "seedLibraryAddedSpecies": "Species added", + "seedLibraryAddingError": "Error during addition", + "seedLibraryAddPlant": "Deposit a plant", + "seedLibraryAddSpecies": "Add a species", + "seedLibraryAll": "All", + "seedLibraryAncestor": "Ancestor", + "seedLibraryAround": "around", + "seedLibraryAutumn": "Autumn", + "seedLibraryBorrowedPlant": "Borrowed plant", + "seedLibraryBorrowingDate": "Borrowing date:", + "seedLibraryBorrowPlant": "Borrow plant", + "seedLibraryCard": "Card", + "seedLibraryChoosingAncestor": "Please choose an ancestor", + "seedLibraryChoosingSpecies": "Please choose a species", + "seedLibraryChoosingSpeciesOrAncestor": "Please choose a species or an ancestor", + "seedLibraryContact": "Contact:", + "seedLibraryDays": "days", + "seedLibraryDeadMsg": "Do you want to declare the plant dead?", + "seedLibraryDeadPlant": "Dead plant", + "seedLibraryDeathDate": "Date of death", + "seedLibraryDeletedSpecies": "Species deleted", + "seedLibraryDeleteSpecies": "Delete species?", + "seedLibraryDeleting": "Deleting", + "seedLibraryDeletingError": "Error during deletion", + "seedLibraryDepositNotAvailable": "Plant deposit is not possible without borrowing a plant first", + "seedLibraryDescription": "Description", + "seedLibraryDifficulty": "Difficulty:", + "seedLibraryEdit": "Edit", + "seedLibraryEditedPlant": "Plant updated", + "seedLibraryEditInformation": "Edit information", + "seedLibraryEditingError": "Error during editing", + "seedLibraryEditSpecies": "Edit species", + "seedLibraryEmptyDifficultyError": "Please choose a difficulty", + "seedLibraryEmptyFieldError": "Please fill all fields", + "seedLibraryEmptyTypeError": "Please choose a plant type", + "seedLibraryEndMonth": "End month:", + "seedLibraryFacebookUrl": "Facebook link", + "seedLibraryFilters": "Filters", + "seedLibraryForum": "Oskour mom I killed my plant - Help forum", + "seedLibraryForumUrl": "Forum link", + "seedLibraryHelpSheets": "Plant sheets", + "seedLibraryInformation": "Information:", + "seedLibraryMaturationTime": "Maturation time", + "seedLibraryMonthJan": "January", + "seedLibraryMonthFeb": "February", + "seedLibraryMonthMar": "March", + "seedLibraryMonthApr": "April", + "seedLibraryMonthMay": "May", + "seedLibraryMonthJun": "June", + "seedLibraryMonthJul": "July", + "seedLibraryMonthAug": "August", + "seedLibraryMonthSep": "September", + "seedLibraryMonthOct": "October", + "seedLibraryMonthNov": "November", + "seedLibraryMonthDec": "December", + "seedLibraryMyPlants": "My plants", + "seedLibraryName": "Name", + "seedLibraryNbSeedsRecommended": "Number of seeds recommended", + "seedLibraryNbSeedsRecommendedError": "Please enter a recommended seed number greater than 0", + "seedLibraryNoDateError": "Please enter a date", + "seedLibraryNoFilteredPlants": "No plants match your search. Try other filters.", + "seedLibraryNoMorePlant": "No plants available", + "seedLibraryNoPersonalPlants": "You don't have any plants yet in your seed library. You can add some in the stocks.", + "seedLibraryNoSpecies": "No species found", + "seedLibraryNoStockPlants": "No plants available in stock", + "seedLibraryNotes": "Notes", + "seedLibraryOk": "OK", + "seedLibraryPlantationPeriod": "Planting period:", + "seedLibraryPlantationType": "Plantation type:", + "seedLibraryPlantDetail": "Plant details", + "seedLibraryPlantingDate": "Planting date", + "seedLibraryPlantingNow": "I'm planting it now", + "seedLibraryPrefix": "Prefix", + "seedLibraryPrefixError": "Prefix already used", + "seedLibraryPrefixLengthError": "The prefix must be 3 characters", + "seedLibraryPropagationMethod": "Propagation method:", + "seedLibraryReference": "Reference:", + "seedLibraryRemovedPlant": "Plant removed", + "seedLibraryRemovingError": "Error removing plant", + "seedLibraryResearch": "Search", + "seedLibrarySaveChanges": "Save changes", + "seedLibrarySeason": "Season:", + "seedLibrarySeed": "Seed", + "seedLibrarySeeds": "seeds", + "seedLibrarySeedDeposit": "Plant deposit", + "seedLibrarySeedLibrary": "Seed library", + "seedLibrarySeedQuantitySimple": "Seed quantity", + "seedLibrarySeedQuantity": "Seed quantity:", + "seedLibraryShowDeadPlants": "Show dead plants", + "seedLibrarySpecies": "Species:", + "seedLibrarySpeciesHelp": "Help on species", + "seedLibrarySpeciesPlural": "Species", + "seedLibrarySpeciesSimple": "Species", + "seedLibrarySpeciesType": "Species type:", + "seedLibrarySpring": "Spring", + "seedLibraryStartMonth": "Start month:", + "seedLibraryStock": "Available stock", + "seedLibrarySummer": "Summer", + "seedLibraryStocks": "Stocks", + "seedLibraryTimeUntilMaturation": "Time until maturation:", + "seedLibraryType": "Type:", + "seedLibraryUnableToOpen": "Unable to open link", + "seedLibraryUpdate": "Edit", + "seedLibraryUpdatedInformation": "Information updated", + "seedLibraryUpdatedSpecies": "Species updated", + "seedLibraryUpdatedPlant": "Plant updated", + "seedLibraryUpdatingError": "Error updating", + "seedLibraryWinter": "Winter", + "seedLibraryWriteReference": "Please write the following reference: ", + "settingsAccount": "Account", + "settingsAddProfilePicture": "Add a photo", + "settingsAdmin": "Administrator", + "settingsAskHelp": "Ask for help", + "settingsAssociation": "Association", + "settingsBirthday": "Birthday", + "settingsBugs": "Bugs", + "settingsChangePassword": "Change password", + "settingsChangingPassword": "Do you really want to change your password?", + "settingsConfirmPassword": "Confirm password", + "settingsCopied": "Copied!", + "settingsDarkMode": "Dark mode", + "settingsDarkModeOff": "Off", + "settingsDeleteLogs": "Delete logs?", + "settingsDeleteNotificationLogs": "Delete notification logs?", + "settingsDetelePersonalData": "Delete my personal data", + "settingsDetelePersonalDataDesc": "This action notifies the administrator that you want to delete your personal data.", + "settingsDeleting": "Deleting", + "settingsEdit": "Edit", + "settingsEditAccount": "Edit account", + "settingsEditPassword": "Edit password", + "settingsEmail": "Email", + "settingsEmptyField": "This field cannot be empty", + "settingsErrorProfilePicture": "Error editing profile picture", + "settingsErrorSendingDemand": "Error sending request", + "settingsEventsIcal": "Ical link for events", + "settingsExpectingDate": "Expected birth date", + "settingsFirstname": "First name", + "settingsFloor": "Floor", + "settingsHelp": "Help", + "settingsIcalCopied": "Ical link copied!", + "settingsLanguage": "Language", + "settingsLanguageVar": "English 🇬🇧", + "settingsLogs": "Logs", + "settingsModules": "Modules", + "settingsMyIcs": "My Ical link", + "settingsName": "Last name", + "settingsNewPassword": "New password", + "settingsNickname": "Nickname", + "settingsNotifications": "Notifications", + "settingsOldPassword": "Old password", + "settingsPasswordChanged": "Password changed", + "settingsPasswordsNotMatch": "Passwords do not match", + "settingsPersonalData": "Personal data", + "settingsPersonalisation": "Personalization", + "settingsPhone": "Phone", + "settingsProfilePicture": "Profile picture", + "settingsPromo": "Promotion", + "settingsRepportBug": "Report a bug", + "settingsSave": "Save", + "settingsSecurity": "Security", + "settingsSendedDemand": "Request sent", + "settingsSettings": "Settings", + "settingsTooHeavyProfilePicture": "Image is too large (max 4MB)", + "settingsUpdatedProfile": "Profile updated", + "settingsUpdatedProfilePicture": "Profile picture updated", + "settingsUpdateNotification": "Update notifications", + "settingsUpdatingError": "Error updating profile", + "settingsVersion": "Version", + "settingsPasswordStrength": "Password strength", + "settingsPasswordStrengthVeryWeak": "Very weak", + "settingsPasswordStrengthWeak": "Weak", + "settingsPasswordStrengthMedium": "Medium", + "settingsPasswordStrengthStrong": "Strong", + "settingsPasswordStrengthVeryStrong": "Very strong", + "settingsPhoneNumber": "Phone number", + "settingsValidate": "Confirm", + "settingsEditedAccount": "Account edited", + "settingsFailedToEditAccount": "Failed to edit account", + "settingsChooseLanguage": "Choose a language", + "settingsNotificationCounter": "{active}/{total} active {active, plural, zero {notification} one {notification} other {notifications}}", + "@settingsNotificationCounter": { + "description": "Affiche le nombre de notifications actives sur le total des notifications disponibles, avec gestion du pluriel", + "placeholders": { + "active": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "settingsEvent": "Event", + "settingsIcal": "Ical link", + "settingsSynncWithCalendar": "Sync with calendar", + "settingsIcalLinkCopied": "Ical link copied", + "settingsProfile": "Profile", + "settingsConnexion": "Connection", + "settingsLogOut": "Log out", + "settingsLogOutDescription": "Do you really want to log out?", + "settingsLogOutSuccess": "Logged out successfully", + "settingsDeleteMyAccount": "Delete my account", + "settingsDeleteMyAccountDescription": "This action will send a request to the administrator to delete your account.", + "settingsDeletionAsked": "Your account deletion request has been sent to the administrator.", + "settingsDeleteMyAccountError": "Error sending account deletion request", + "voteAdd": "Add", + "voteAddMember": "Add a member", + "voteAddedPretendance": "List added", + "voteAddedSection": "Section added", + "voteAddingError": "Error adding", + "voteAddPretendance": "Add a list", + "voteAddSection": "Add a section", + "voteAll": "All", + "voteAlreadyAddedMember": "Member already added", + "voteAlreadyVoted": "Vote recorded", + "voteChooseList": "Choose a list", + "voteClear": "Reset", + "voteClearVotes": "Reset votes", + "voteClosedVote": "Votes closed", + "voteCloseVote": "Close votes", + "voteConfirmVote": "Confirm vote", + "voteCountVote": "Count votes", + "voteDelete": "Delete", + "voteDeletedAll": "All deleted", + "voteDeletedPipo": "Fake lists deleted", + "voteDeletedSection": "Section deleted", + "voteDeleteAll": "Delete all", + "voteDeleteAllDescription": "Do you really want to delete everything?", + "voteDeletePipo": "Delete fake lists", + "voteDeletePipoDescription": "Do you really want to delete the fake lists?", + "voteDeletePretendance": "Delete the list", + "voteDeletePretendanceDesc": "Do you really want to delete this list?", + "voteDeleteSection": "Delete the section", + "voteDeleteSectionDescription": "Do you really want to delete this section?", + "voteDeletingError": "Error deleting", + "voteDescription": "Description", + "voteEdit": "Edit", + "voteEditedPretendance": "List edited", + "voteEditedSection": "Section edited", + "voteEditingError": "Error editing", + "voteErrorClosingVotes": "Error closing votes", + "voteErrorCountingVotes": "Error counting votes", + "voteErrorResetingVotes": "Error resetting votes", + "voteErrorOpeningVotes": "Error opening votes", + "voteIncorrectOrMissingFields": "Incorrect or missing fields", + "voteMembers": "Members", + "voteName": "Name", + "voteNoPretendanceList": "No list of candidates", + "voteNoSection": "No section", + "voteCanNotVote": "You cannot vote", + "voteNoSectionList": "No section", + "voteNotOpenedVote": "Vote not opened", + "voteOnGoingCount": "Counting in progress", + "voteOpenVote": "Open votes", + "votePipo": "Fake", + "votePretendance": "Lists", + "votePretendanceDeleted": "Candidate list deleted", + "votePretendanceNotDeleted": "Error deleting", + "voteProgram": "Program", + "votePublish": "Publish", + "votePublishVoteDescription": "Do you really want to publish the votes?", + "voteResetedVotes": "Votes reset", + "voteResetVote": "Reset votes", + "voteResetVoteDescription": "What do you want to do?", + "voteRole": "Role", + "voteSectionDescription": "Section description", + "voteSection": "Section", + "voteSectionName": "Section name", + "voteSeeMore": "See more", + "voteSelected": "Selected", + "voteShowVotes": "Show votes", + "voteVote": "Vote", + "voteVoteError": "Error recording vote", + "voteVoteFor": "Vote for ", + "voteVoteNotStarted": "Vote not opened", + "voteVoters": "Voting groups", + "voteVoteSuccess": "Vote recorded", + "voteVotes": "Votes", + "voteVotesClosed": "Votes closed", + "voteVotesCounted": "Votes counted", + "voteVotesOpened": "Votes opened", + "voteWarning": "Warning", + "voteWarningMessage": "Selection will not be saved.\nDo you want to continue?", + "moduleAdvert": "Feed", + "moduleAmap": "AMAP", + "moduleBooking": "Booking", + "moduleCalendar": "Calendar", + "moduleCentralisation": "Centralisation", + "moduleCinema": "Cinema", + "moduleEvent": "Event", + "moduleFlappyBird": "Flappy Bird", + "moduleLoan": "Loan", + "modulePhonebook": "Phonebook", + "modulePurchases": "Purchases", + "moduleRaffle": "Raffle", + "moduleRecommendation": "Recommendation", + "moduleSeedLibrary": "Seed Library", + "moduleVote": "Vote", + "modulePh": "PH", + "moduleSettings": "Settings", + "moduleFeed": "Events", + "moduleStyleGuide": "StyleGuide", + "moduleAdmin": "Admin", + "moduleOthers": "Others", + "modulePayment": "Payment", + "moduleAdvertDescription": "View the latest feed", + "moduleAmapDescription": "Order your AMAP basket", + "moduleBookingDescription": "Book a room", + "moduleCalendarDescription": "View the calendar of events", + "moduleCentralisationDescription": "Viw all links", + "moduleCinemaDescription": "View the cinema schedule", + "moduleEventDescription": "View events", + "moduleFlappyBirdDescription": "Play Flappy Bird", + "moduleLoanDescription": "See your loans", + "modulePhonebookDescription": "View the phonebook", + "modulePurchasesDescription": "View your purchases", + "moduleRaffleDescription": "View the raffle", + "moduleRecommendationDescription": "View the recommendations", + "moduleSeedLibraryDescription": "View the seed library", + "moduleVoteDescription": "Vote for the campaigns", + "modulePhDescription": "View the PH", + "moduleSettingsDescription": "Manage your settings", + "moduleFeedDescription": "View the latest events", + "moduleStyleGuideDescription": "Style guide for developers", + "moduleAdminDescription": "Administration module for administrators", + "moduleOthersDescription": "Other modules", + "modulePaymentDescription": "Pay and see your transactions", + "toolInvalidNumber": "Invalid number", + "toolDateRequired": "Date required", + "toolSuccess": "Success" +} diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb new file mode 100644 index 0000000000..2100ec00ca --- /dev/null +++ b/lib/l10n/app_fr.arb @@ -0,0 +1,1655 @@ +{ + "@@locale": "fr", + "dateToday": "Aujourd'hui", + "dateYesterday": "Hier", + "dateTomorrow": "Demain", + "dateAt": "à", + "dateFrom": "de", + "dateTo": "à", + "dateBetweenDays": "au", + "dateStarting": "Commence", + "dateLast": "", + "dateUntil": "Jusqu'au", + "feedFilterAll": "Tous", + "feedFilterPending": "En attente", + "feedFilterApproved": "Approuvés", + "feedFilterRejected": "Rejetés", + "feedEmptyAll": "Aucun événement disponible", + "feedEmptyPending": "Aucun événement en attente de validation", + "feedEmptyApproved": "Aucun événement approuvé", + "feedEmptyRejected": "Aucun événement rejeté", + "feedEventManagement": "Gestion des événements", + "feedTitle": "Titre", + "feedLocation": "Lieu", + "feedSGDate": "Date du SG", + "feedSGExternalLink": "Lien externe du SG", + "feedCreateEvent": "Créer l'événement", + "feedNotification": "Envoyer une notification", + "feedPleaseSelectAnAssociation": "Veuillez sélectionner une association", + "feedReject": "Rejeter", + "feedApprove": "Approuver", + "feedEnded": "Terminé", + "feedOngoing": "En cours", + "feedFilter": "Filtrer", + "feedAssociation": "Association", + "feedAssociationEvent": "Event de {name}", + "@feedAssociationEvent": { + "description": "Association event", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "feedEditEvent": "Modifier l'événement", + "feedManageAssociationEvents": "Gérer les événements de l'association", + "feedNews": "Calendrier", + "feedNewsType": "Type d'actualité", + "feedNoAssociationEvents": "Aucun événement d'association", + "feedApply": "Appliquer", + "feedAdmin": "Administration", + "feedCreateAnEvent": "Créer un événement", + "feedManageRequests": "Demandes de publication", + "feedNoNewsAvailable": "Aucune actualité disponible", + "feedRefresh": "Actualiser", + "feedPleaseProvideASGExternalLink": "Veuillez entrer un lien externe pour le SG", + "feedPleaseProvideASGDate": "Veuillez entrer une date de SG", + "feedShotgunIn": "Shotgun {time}", + "@feedShotgunIn": { + "description": "Placeholder pour le temps restant avant le shotgun", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "feedVoteIn": "Vote {time}", + "@feedVoteIn": { + "description": "Temps restant avant le vote", + "placeholders": { + "time": { + "type": "String" + } + } + }, + "feedCantOpenLink": "Impossible d'ouvrir le lien", + "feedGetReady": "Prépare-toi !", + "eventActionCampaign": "Tu peux voter", + "eventActionEvent": "Tu es invité", + "eventActionCampaignSubtitle": "Votez maintenant", + "eventActionEventSubtitle": "Répondez à l'invitation", + "eventActionCampaignButton": "Voter", + "eventActionEventButton": "Réserver", + "eventActionCampaignValidated": "J'ai voté !", + "eventActionEventValidated": "Je viens !", + "adminAccountTypes": "Types de compte", + "adminAdd": "Ajouter", + "adminAddGroup": "Ajouter un groupe", + "adminAddMember": "Ajouter un membre", + "adminAddedGroup": "Groupe créé", + "adminAddedLoaner": "Préteur ajouté", + "adminAddedMember": "Membre ajouté", + "adminAddingError": "Erreur lors de l'ajout", + "adminAddingMember": "Ajout d'un membre", + "adminAddLoaningGroup": "Ajouter un groupe de prêt", + "adminAddSchool": "Ajouter une école", + "adminAddStructure": "Ajouter une structure", + "adminAddedSchool": "École créée", + "adminAddedStructure": "Structure ajoutée", + "adminEditedStructure": "Structure modifiée", + "adminAdministration": "Administration", + "adminAssociationMembership": "Adhésion", + "adminAssociationMembershipName": "Nom de l'adhésion", + "adminAssociationsMemberships": "Adhésions", + "adminBankAccountHolder": "Titulaire du compte bancaire : {bankAccountHolder}", + "@adminBankAccountHolder": { + "description": "Displays the bank account holder's name", + "placeholders": { + "bankAccountHolder": { + "type": "String" + } + } + }, + "adminBankAccountHolderModified": "Titulaire du compte bancaire modifié", + "adminBankDetails": "Coordonnées bancaires", + "adminBic": "BIC", + "adminBicError": "Le BIC doit faire 11 caractères", + "adminCity": "Ville", + "adminClearFilters": "Effacer les filtres", + "adminCountry": "Pays", + "adminCreateAssociationMembership": "Créer une adhésion", + "adminCreatedAssociationMembership": "Adhésion créée", + "adminCreationError": "Erreur lors de la création", + "adminDateError": "La date de début doit être avant la date de fin", + "adminDefineAsBankAccountHolder": "Définir comme titulaire du compte bancaire", + "adminDelete": "Supprimer", + "adminDeleteAssociationMember": "Supprimer le membre ?", + "adminDeleteAssociationMemberConfirmation": "Êtes-vous sûr de vouloir supprimer ce membre ?", + "adminDeleteAssociationMembership": "Supprimer l'adhésion ?", + "adminDeletedAssociationMembership": "Adhésion supprimée", + "adminDeleteGroup": "Supprimer le groupe", + "adminDeletedGroup": "Groupe supprimé", + "adminDeleteSchool": "Supprimer l'école ?", + "adminDeletedSchool": "École supprimée", + "adminDeleting": "Suppression", + "adminDeletingError": "Erreur lors de la suppression", + "adminDescription": "Description", + "adminEdit": "Modifier", + "adminEditStructure": "Modifier la structure", + "adminEditMembership": "Modifier l'adhésion", + "adminEmptyDate": "Date vide", + "adminEmptyFieldError": "Le nom ne peut pas être vide", + "adminEmailFailed": "Impossible d'envoyer un mail aux adresses suivantes", + "adminEmailRegex": "Email Regex", + "adminEmptyUser": "Utilisateur vide", + "adminEndDate": "Date de fin", + "adminEndDateMaximal": "Date de fin maximale", + "adminEndDateMinimal": "Date de fin minimale", + "adminError": "Erreur", + "adminFilters": "Filtres", + "adminGroup": "Groupe", + "adminGroups": "Groupes", + "adminIban": "IBAN", + "adminIbanError": "L'IBAN doit faire 27 caractères", + "adminLoaningGroup": "Groupe de prêt", + "adminLooking": "Recherche", + "adminManager": "Administrateur de la structure", + "adminMaximum": "Maximum", + "adminMembers": "Membres", + "adminMembershipAddingError": "Erreur lors de l'ajout (surement dû à une superposition de dates)", + "adminMemberships": "Adhésions", + "adminMembershipUpdatingError": "Erreur lors de la modification (surement dû à une superposition de dates)", + "adminMinimum": "Minimum", + "adminModifyModuleVisibility": "Visibilité des modules", + "adminName": "Nom", + "adminNoGroup": "Aucun groupe", + "adminNoManager": "Aucun manager n'est sélectionné", + "adminNoMember": "Aucun membre", + "adminNoMoreLoaner": "Aucun prêteur n'est disponible", + "adminNoSchool": "Sans école", + "adminRemoveGroupMember": "Supprimer le membre du groupe ?", + "adminResearch": "Recherche", + "adminSchools": "Écoles", + "adminShortId": "Short ID (3 lettres)", + "adminShortIdError": "Le short ID doit faire 3 caractères", + "adminSiegeAddress": "Adresse du siège", + "adminSiret": "SIRET", + "adminSiretError": "SIRET must be 14 digits", + "adminStreet": "Numéro et rue", + "adminStructures": "Structures", + "adminStartDate": "Date de début", + "adminStartDateMaximal": "Date de début maximale", + "adminStartDateMinimal": "Date de début minimale", + "adminUndefinedBankAccountHolder": "Titulaire du compte bancaire non défini", + "adminUpdatedAssociationMembership": "Adhésion modifiée", + "adminUpdatedGroup": "Groupe modifié", + "adminUpdatedMembership": "Adhésion modifiée", + "adminUpdatingError": "Erreur lors de la modification", + "adminUser": "Utilisateur", + "adminValidateFilters": "Valider les filtres", + "adminVisibilities": "Visibilités", + "adminZipcode": "Code postal", + "adminGroupNotification": "Notification de groupe", + "adminNotifyGroup": "Notifier le groupe {groupName}", + "@adminNotifyGroup": { + "description": "Notifie les membres du groupe sélectionné", + "placeholders": { + "groupName": { + "type": "String" + } + } + }, + "adminTitle": "Titre", + "adminContent": "Contenu", + "adminSend": "Envoyer", + "adminNotificationSent": "Notification envoyée", + "adminFailedToSendNotification": "Échec de l'envoi de la notification", + "adminGroupsManagement": "Gestion des groupes", + "adminEditGroup": "Modifier le groupe", + "adminManageMembers": "Gérer les membres", + "adminDeleteGroupConfirmation": "Êtes-vous sûr de vouloir supprimer ce groupe ?", + "adminFailedToDeleteGroup": "Échec de la suppression du groupe", + "adminUsersAndGroups": "Utilisateurs et groupes", + "adminUsersManagement": "Gestion des utilisateurs", + "adminUsersManagementDescription": "Gérer les utilisateurs de l'application", + "adminManageUserGroups": "Gérer les groupes d'utilisateurs", + "adminSendNotificationToGroup": "Envoyer une notification à un groupe", + "adminPaiementModule": "Module de paiement", + "adminPaiement": "Paiement", + "adminManagePaiementStructures": "Gérer les structures du module de paiement", + "adminManageUsersAssociationMemberships": "Gérer les adhésions des utilisateurs", + "adminAssociationMembershipsManagement": "Gestion des adhésions", + "adminChooseGroupManager": "Groupe gestionnaire de l'adhésion", + "adminSelectManager": "Sélectionner un gestionnaire", + "adminImportList": "Importer une liste", + "adminImportUsersDescription": "Importer des utilisateurs depuis un fichier CSV. Le fichier CSV doit contenir une adresse email par ligne.", + "adminFailedToInviteUsers": "Échec de l'invitation des utilisateurs", + "adminDeleteUsers": "Supprimer des utilisateurs", + "adminAdmin": "Admin", + "adminAssociations": "Associations", + "adminManageAssociations": "Gérer les associations", + "adminAddAssociation": "Ajouter une association", + "adminAssociationName": "Nom de l'association", + "adminSelectGroupAssociationManager": "Séléctionner roupe gestionnaire de l'association", + "adminEditAssociation": "Modifier l'association : {associationName}", + "@adminEditAssociation": { + "description": "Modifier les informations de l'association", + "placeholders": { + "associationName": { + "type": "String" + } + } + }, + "adminManagerGroup": "Groupe gestionnaire : {groupName}", + "@adminManagerGroup": { + "description": "Groupe qui gère l'association", + "placeholders": { + "groupName": { + "type": "String" + } + } + }, + "adminAssociationCreated": "Association créée", + "adminAssociationUpdated": "Association mise à jour", + "adminAssociationCreationError": "Échec de la création de l'association", + "adminAssociationUpdateError": "Échec de la mise à jour de l'association", + "adminInvite": "Inviter", + "adminInvitedUsers": "Utilisateurs invités", + "adminInviteUsers": "Inviter des utilisateurs", + "adminInviteUsersCounter": "{count, plural, zero {Aucun utilisateur} one {{count} utilisateur} other {{count} utilisateurs}} dans le fichier CSV", + "@adminInviteUsersCounter": { + "description": "Text with the number of users in the CSV file", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "adminUpdatedAssociationLogo": "Logo de l'association mis à jour", + "adminTooHeavyLogo": "Le logo de l'association est trop lourd, il doit faire moins de 4 Mo", + "adminFailedToUpdateAssociationLogo": "Échec de la mise à jour du logo de l'association", + "adminChooseGroup": "Choisir un groupe", + "adminChooseAssociationManagerGroup": "Choisir un groupe gestionnaire pour l'association", + "advertAdd": "Ajouter", + "advertAddedAdvert": "Annonce publiée", + "advertAddedAnnouncer": "Annonceur ajouté", + "advertAddingError": "Erreur lors de l'ajout", + "advertAdmin": "Admin", + "advertAdvert": "Annonce", + "advertChoosingAnnouncer": "Veuillez choisir un annonceur", + "advertChoosingPoster": "Veuillez choisir une image", + "advertContent": "Contenu", + "advertDeleteAdvert": "Supprimer l'annonce", + "advertDeleteAnnouncer": "Supprimer l'annonceur ?", + "advertDeleting": "Suppression", + "advertEdit": "Modifier", + "advertEditedAdvert": "Annonce modifiée", + "advertEditingError": "Erreur lors de la modification", + "advertGroupAdvert": "Groupe", + "advertIncorrectOrMissingFields": "Champs incorrects ou manquants", + "advertInvalidNumber": "Veuillez entrer un nombre", + "advertManagement": "Gestion", + "advertModifyAnnouncingGroup": "Modifier un groupe d'annonce", + "advertNoMoreAnnouncer": "Aucun annonceur n'est disponible", + "advertNoValue": "Veuillez entrer une valeur", + "advertPositiveNumber": "Veuillez entrer un nombre positif", + "advertPublishToFeed": "Publier dans le feed", + "advertNotification": "Envoyer une notification", + "advertRemovedAnnouncer": "Annonceur supprimé", + "advertRemovingError": "Erreur lors de la suppression", + "advertTags": "Tags", + "advertTitle": "Titre", + "advertMonthJan": "Janv", + "advertMonthFeb": "Févr.", + "advertMonthMar": "Mars", + "advertMonthApr": "Avr.", + "advertMonthMay": "Mai", + "advertMonthJun": "Juin", + "advertMonthJul": "Juill.", + "advertMonthAug": "Août", + "advertMonthSep": "Sept.", + "advertMonthOct": "Oct.", + "advertMonthNov": "Nov.", + "advertMonthDec": "Déc.", + "amapAccounts": "Comptes", + "amapAdd": "Ajouter", + "amapAddDelivery": "Ajouter une livraison", + "amapAddedCommand": "Commande ajoutée", + "amapAddedOrder": "Commande ajoutée", + "amapAddedProduct": "Produit ajouté", + "amapAddedUser": "Utilisateur ajouté", + "amapAddProduct": "Ajouter un produit", + "amapAddUser": "Ajouter un utilisateur", + "amapAddingACommand": "Ajouter une commande", + "amapAddingCommand": "Ajouter la commande", + "amapAddingError": "Erreur lors de l'ajout", + "amapAddingProduct": "Ajouter un produit", + "amapAddOrder": "Ajouter une commande", + "amapAdmin": "Admin", + "amapAlreadyExistCommand": "Il existe déjà une commande à cette date", + "amapAmap": "Amap", + "amapAmount": "Solde", + "amapArchive": "Archiver", + "amapArchiveDelivery": "Archiver", + "amapArchivingDelivery": "Archivage de la livraison", + "amapCategory": "Catégorie", + "amapCloseDelivery": "Verrouiller", + "amapCommandDate": "Date de la commande", + "amapCommandProducts": "Produits de la commande", + "amapConfirm": "Confirmer", + "amapContact": "Contacts associatifs ", + "amapCreateCategory": "Créer une catégorie", + "amapDelete": "Supprimer", + "amapDeleteDelivery": "Supprimer la livraison ?", + "amapDeleteDeliveryDescription": "Voulez-vous vraiment supprimer cette livraison ?", + "amapDeletedDelivery": "Livraison supprimée", + "amapDeletedOrder": "Commande supprimée", + "amapDeletedProduct": "Produit supprimé", + "amapDeleteProduct": "Supprimer le produit ?", + "amapDeleteProductDescription": "Voulez-vous vraiment supprimer ce produit ?", + "amapDeleting": "Suppression", + "amapDeletingDelivery": "Supprimer la livraison ?", + "amapDeletingError": "Erreur lors de la suppression", + "amapDeletingOrder": "Supprimer la commande ?", + "amapDeletingProduct": "Supprimer le produit ?", + "amapDeliver": "Livraison teminée ?", + "amapDeliveries": "Livraisons", + "amapDeliveringDelivery": "Toutes les commandes sont livrées ?", + "amapDelivery": "Livraison", + "amapDeliveryArchived": "Livraison archivée", + "amapDeliveryDate": "Date de livraison", + "amapDeliveryDelivered": "Livraison effectuée", + "amapDeliveryHistory": "Historique des livraisons", + "amapDeliveryList": "Liste des livraisons", + "amapDeliveryLocked": "Livraison verrouillée", + "amapDeliveryOn": "Livraison le", + "amapDeliveryOpened": "Livraison ouverte", + "amapDeliveryNotArchived": "Livraison non archivée", + "amapDeliveryNotLocked": "Livraison non verrouillée", + "amapDeliveryNotDelivered": "Livraison non effectuée", + "amapDeliveryNotOpened": "Livraison non ouverte", + "amapEditDelivery": "Modifier la livraison", + "amapEditedCommand": "Commande modifiée", + "amapEditingError": "Erreur lors de la modification", + "amapEditProduct": "Modifier le produit", + "amapEndingDelivery": "Fin de la livraison", + "amapError": "Erreur", + "amapErrorLink": "Erreur lors de l'ouverture du lien", + "amapErrorLoadingUser": "Erreur lors du chargement des utilisateurs", + "amapEvening": "Soir", + "amapExpectingNumber": "Veuillez entrer un nombre", + "amapFillField": "Veuillez remplir ce champ", + "amapHandlingAccount": "Gérer les comptes", + "amapLoading": "Chargement...", + "amapLoadingError": "Erreur lors du chargement", + "amapLock": "Verrouiller", + "amapLocked": "Verrouillée", + "amapLockedDelivery": "Livraison verrouillée", + "amapLockedOrder": "Commande verrouillée", + "amapLooking": "Rechercher", + "amapLockingDelivery": "Verrouiller la livraison ?", + "amapMidDay": "Midi", + "amapMyOrders": "Mes commandes", + "amapName": "Nom", + "amapNextStep": "Étape suivante", + "amapNoProduct": "Pas de produit", + "amapNoCurrentOrder": "Pas de commande en cours", + "amapNoMoney": "Pas assez d'argent", + "amapNoOpennedDelivery": "Pas de livraison ouverte", + "amapNoOrder": "Pas de commande", + "amapNoSelectedDelivery": "Pas de livraison sélectionnée", + "amapNotEnoughMoney": "Pas assez d'argent", + "amapNotPlannedDelivery": "Pas de livraison planifiée", + "amapOneOrder": "commande", + "amapOpenDelivery": "Ouvrir", + "amapOpened": "Ouverte", + "amapOpenningDelivery": "Ouvrir la livraison ?", + "amapOrder": "Commander", + "amapOrders": "Commandes", + "amapPickChooseCategory": "Veuillez entrer une valeur ou choisir une catégorie existante", + "amapPickDeliveryMoment": "Choisissez un moment de livraison", + "amapPresentation": "Présentation", + "amapPresentation1": "L'AMAP (association pour le maintien d'une agriculture paysanne) est un service proposé par l'association Planet&Co de l'ECL. Vous pouvez ainsi recevoir des produits (paniers de fruits et légumes, jus, confitures...) directement sur le campus !\n\nLes commandes doivent être passées avant le vendredi 21h et sont livrées sur le campus le mardi de 13h à 13h45 (ou de 18h15 à 18h30 si vous ne pouvez pas passer le midi) dans le hall du M16.\n\nVous ne pouvez commander que si votre solde le permet. Vous pouvez recharger votre solde via la collecte Lydia ou bien avec un chèque que vous pouvez nous transmettre lors des permanences.\n\nLien vers la collecte Lydia pour le rechargement : ", + "amapPresentation2": "\n\nN'hésitez pas à nous contacter en cas de problème !", + "amapPrice": "Prix", + "amapProduct": "produit", + "amapProducts": "Produits", + "amapProductInDelivery": "Produit dans une livraison non terminée", + "amapQuantity": "Quantité", + "amapRequiredDate": "La date est requise", + "amapSeeMore": "Voir plus", + "amapThe": "Le", + "amapUnlock": "Dévérouiller", + "amapUnlockedDelivery": "Livraison dévérouillée", + "amapUnlockingDelivery": "Dévérouiller la livraison ?", + "amapUpdate": "Modifier", + "amapUpdatedAmount": "Solde modifié", + "amapUpdatedOrder": "Commande modifiée", + "amapUpdatedProduct": "Produit modifié", + "amapUpdatingError": "Echec de la modification", + "amapUsersNotFound": "Aucun utilisateur trouvé", + "amapWaiting": "En attente", + "bookingAdd": "Ajouter", + "bookingAddBookingPage": "Demande", + "bookingAddRoom": "Ajouter une salle", + "bookingAddBooking": "Ajouter une réservation", + "bookingAddedBooking": "Demande ajoutée", + "bookingAddedRoom": "Salle ajoutée", + "bookingAddedManager": "Gestionnaire ajouté", + "bookingAddingError": "Erreur lors de l'ajout", + "bookingAddManager": "Ajouter un gestionnaire", + "bookingAdminPage": "Administrateur", + "bookingAllDay": "Toute la journée", + "bookingBookedFor": "Réservé pour", + "bookingBooking": "Réservation", + "bookingBookingCreated": "Réservation créée", + "bookingBookingDemand": "Demande de réservation", + "bookingBookingNote": "Note de la réservation", + "bookingBookingPage": "Réservation", + "bookingBookingReason": "Motif de la réservation", + "bookingBy": "par", + "bookingConfirm": "Confirmer", + "bookingConfirmation": "Confirmation", + "bookingConfirmBooking": "Confirmer la réservation ?", + "bookingConfirmed": "Validée", + "bookingDates": "Dates", + "bookingDecline": "Refuser", + "bookingDeclineBooking": "Refuser la réservation ?", + "bookingDeclined": "Refusée", + "bookingDelete": "Supprimer", + "bookingDeleting": "Suppression", + "bookingDeleteBooking": "Suppression", + "bookingDeleteBookingConfirmation": "Êtes-vous sûr de vouloir supprimer cette réservation ?", + "bookingDeletedBooking": "Réservation supprimée", + "bookingDeletedRoom": "Salle supprimée", + "bookingDeletedManager": "Gestionnaire supprimé", + "bookingDeleteRoomConfirmation": "Êtes-vous sûr de vouloir supprimer cette salle ?\n\nLa salle ne doit avoir aucune réservation en cours ou à venir pour être supprimée", + "bookingDeleteManagerConfirmation": "Êtes-vous sûr de vouloir supprimer ce gestionnaire ?\n\nLe gestionnaire ne doit être associé à aucune salle pour pouvoir être supprimé", + "bookingDeletingBooking": "Supprimer la réservation ?", + "bookingDeletingError": "Erreur lors de la suppression", + "bookingDeletingRoom": "Supprimer la salle ?", + "bookingEdit": "Modifier", + "bookingEditBooking": "Modifier une réservation", + "bookingEditionError": "Erreur lors de la modification", + "bookingEditedBooking": "Réservation modifiée", + "bookingEditedRoom": "Salle modifiée", + "bookingEditedManager": "Gestionnaire modifié", + "bookingEditManager": "Modifier ou supprimer un gestionnaire", + "bookingEditRoom": "Modifier ou supprimer une salle", + "bookingEndDate": "Date de fin", + "bookingEndHour": "Heure de fin", + "bookingEntity": "Pour qui ?", + "bookingError": "Erreur", + "bookingEventEvery": "Tous les", + "bookingHistoryPage": "Historique", + "bookingIncorrectOrMissingFields": "Champs incorrects ou manquants", + "bookingInterval": "Intervalle", + "bookingInvalidIntervalError": "Intervalle invalide", + "bookingInvalidDates": "Dates invalides", + "bookingInvalidRoom": "Salle invalide", + "bookingKeysRequested": "Clés demandées", + "bookingManagement": "Gestion", + "bookingManager": "Gestionnaire", + "bookingManagerName": "Nom du gestionnaire", + "bookingMultipleDay": "Plusieurs jours", + "bookingMyBookings": "Mes réservations", + "bookingNecessaryKey": "Clé nécessaire", + "bookingNext": "Suivant", + "bookingNo": "Non", + "bookingNoCurrentBooking": "Pas de réservation en cours", + "bookingNoDateError": "Veuillez choisir une date", + "bookingNoAppointmentInReccurence": "Aucun créneau existe avec ces paramètres de récurrence", + "bookingNoDaySelected": "Aucun jour sélectionné", + "bookingNoDescriptionError": "Veuillez entrer une description", + "bookingNoKeys": "Aucune clé", + "bookingNoNoteError": "Veuillez entrer une note", + "bookingNoPhoneRegistered": "Numéro non renseigné", + "bookingNoReasonError": "Veuillez entrer un motif", + "bookingNoRoomFoundError": "Aucune salle enregistrée", + "bookingNoRoomFound": "Aucune salle trouvée", + "bookingNote": "Note", + "bookingOther": "Autre", + "bookingPending": "En attente", + "bookingPrevious": "Précédent", + "bookingReason": "Motif", + "bookingRecurrence": "Récurrence", + "bookingRecurrenceDays": "Jours de récurrence", + "bookingRecurrenceEndDate": "Date de fin de récurrence", + "bookingRecurrent": "Récurrent", + "bookingRegisteredRooms": "Salles enregistrées", + "bookingRoom": "Salle", + "bookingRoomName": "Nom de la salle", + "bookingStartDate": "Date de début", + "bookingStartHour": "Heure de début", + "bookingWeeks": "Semaines", + "bookingYes": "Oui", + "bookingWeekDayMon": "Lundi", + "bookingWeekDayTue": "Mardi", + "bookingWeekDayWed": "Mercredi", + "bookingWeekDayThu": "Jeudi", + "bookingWeekDayFri": "Vendredi", + "bookingWeekDaySat": "Samedi", + "bookingWeekDaySun": "Dimanche", + "cinemaAdd": "Ajouter", + "cinemaAddedSession": "Séance ajoutée", + "cinemaAddingError": "Erreur lors de l'ajout", + "cinemaAddSession": "Ajouter une séance", + "cinemaCinema": "Cinéma", + "cinemaDeleteSession": "Supprimer la séance ?", + "cinemaDeleting": "Suppression", + "cinemaDuration": "Durée", + "cinemaEdit": "Modifier", + "cinemaEditedSession": "Séance modifiée", + "cinemaEditingError": "Erreur lors de la modification", + "cinemaEditSession": "Modifier la séance", + "cinemaEmptyUrl": "Veuillez entrer une URL", + "cinemaImportFromTMDB": "Importer depuis TMDB", + "cinemaIncomingSession": "A l'affiche", + "cinemaIncorrectOrMissingFields": "Champs incorrects ou manquants", + "cinemaInvalidUrl": "URL invalide", + "cinemaGenre": "Genre", + "cinemaName": "Nom", + "cinemaNoDateError": "Veuillez entrer une date", + "cinemaNoDuration": "Veuillez entrer une durée", + "cinemaNoOverview": "Aucun synopsis", + "cinemaNoPoster": "Aucune affiche", + "cinemaNoSession": "Aucune séance", + "cinemaOverview": "Synopsis", + "cinemaPosterUrl": "URL de l'affiche", + "cinemaSessionDate": "Jour de la séance", + "cinemaStartHour": "Heure de début", + "cinemaTagline": "Slogan", + "cinemaThe": "Le", + "drawerAdmin": "Administration", + "drawerAndroidAppLink": "https://play.google.com/store/apps/details?id=fr.myecl.titan", + "drawerCopied": "Copié !", + "drawerDownloadAppOnMobileDevice": "Ce site est la version Web de l'application MyECL. Nous vous invitons à télécharger l'application. N'utilisez ce site qu'en cas de problème avec l'application.\n", + "drawerIosAppLink": "https://apps.apple.com/fr/app/myecl/id6444443430", + "drawerLoginOut": "Voulez-vous vous déconnecter ?", + "drawerLogOut": "Déconnexion", + "drawerOr": " ou ", + "drawerSettings": "Paramètres", + "eventAdd": "Ajouter", + "eventAddEvent": "Ajouter un événement", + "eventAddedEvent": "Événement ajouté", + "eventAddingError": "Erreur lors de l'ajout", + "eventAllDay": "Toute la journée", + "eventConfirm": "Confirmer", + "eventConfirmEvent": "Confirmer l'événement ?", + "eventConfirmation": "Confirmation", + "eventConfirmed": "Confirmé", + "eventDates": "Dates", + "eventDecline": "Refuser", + "eventDeclineEvent": "Refuser l'événement ?", + "eventDeclined": "Refusé", + "eventDelete": "Supprimer", + "eventDeleteConfirm": "Supprimer l'event {name} ?", + "@eventDeleteConfirm": { + "description": "Delete the event with its name", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "eventDeletedEvent": "Événement supprimé", + "eventDeleting": "Suppression", + "eventDeletingError": "Erreur lors de la suppression", + "eventDeletingEvent": "Supprimer l'événement ?", + "eventDescription": "Description", + "eventEdit": "Modifier", + "eventEditEvent": "Modifier un événement", + "eventEditedEvent": "Événement modifié", + "eventEditingError": "Erreur lors de la modification", + "eventEndDate": "Date de fin", + "eventEndHour": "Heure de fin", + "eventError": "Erreur", + "eventEventList": "Liste des événements", + "eventEventType": "Type d'événement", + "eventEvery": "Tous les", + "eventHistory": "Historique", + "eventIncorrectOrMissingFields": "Certains champs sont incorrects ou manquants", + "eventInterval": "Intervalle", + "eventInvalidDates": "La date de fin doit être après la date de début", + "eventInvalidIntervalError": "Veuillez entrer un intervalle valide", + "eventLocation": "Lieu", + "eventModifiedEvent": "Événement modifié", + "eventModifyingError": "Erreur lors de la modification", + "eventMyEvents": "Mes événements", + "eventName": "Nom", + "eventNext": "Suivant", + "eventNo": "Non", + "eventNoCurrentEvent": "Aucun événement en cours", + "eventNoDateError": "Veuillez entrer une date", + "eventNoDaySelected": "Aucun jour sélectionné", + "eventNoDescriptionError": "Veuillez entrer une description", + "eventNoEvent": "Aucun événement", + "eventNoNameError": "Veuillez entrer un nom", + "eventNoOrganizerError": "Veuillez entrer un organisateur", + "eventNoPlaceError": "Veuillez entrer un lieu", + "eventNoPhoneRegistered": "Numéro non renseigné", + "eventNoRuleError": "Veuillez entrer une règle de récurrence", + "eventOrganizer": "Organisateur", + "eventOther": "Autre", + "eventPending": "En attente", + "eventPrevious": "Précédent", + "eventRecurrence": "Récurrence", + "eventRecurrenceDays": "Jours de récurrence", + "eventRecurrenceEndDate": "Date de fin de la récurrence", + "eventRecurrenceRule": "Règle de récurrence", + "eventRoom": "Salle", + "eventStartDate": "Date de début", + "eventStartHour": "Heure de début", + "eventTitle": "Événements", + "eventYes": "Oui", + "eventEventEvery": "Toutes les", + "eventWeeks": "semaines", + "eventDayMon": "Lundi", + "eventDayTue": "Mardi", + "eventDayWed": "Mercredi", + "eventDayThu": "Jeudi", + "eventDayFri": "Vendredi", + "eventDaySat": "Samedi", + "eventDaySun": "Dimanche", + "globalConfirm": "Confirmer", + "globalCancel": "Annuler", + "globalIrreversibleAction": "Cette action est irréversible", + "globalOptionnal": "{text} (Optionnel)", + "@globalOptionnal": { + "description": "Texte avec complément optionnel", + "placeholders": { + "text": { + "type": "String" + } + } + }, + "homeCalendar": "Calendrier", + "homeEventOf": "Évènements du", + "homeIncomingEvents": "Évènements à venir", + "homeLastInfos": "Dernières annonces", + "homeNoEvents": "Aucun évènement", + "homeTranslateDayShortMon": "Lun", + "homeTranslateDayShortTue": "Mar", + "homeTranslateDayShortWed": "Mer", + "homeTranslateDayShortThu": "Jeu", + "homeTranslateDayShortFri": "Ven", + "homeTranslateDayShortSat": "Sam", + "homeTranslateDayShortSun": "Dim", + "loanAdd": "Ajouter", + "loanAddLoan": "Ajouter un prêt", + "loanAddObject": "Ajouter un objet", + "loanAddedLoan": "Prêt ajouté", + "loanAddedObject": "Objet ajouté", + "loanAddedRoom": "Salle ajoutée", + "loanAddingError": "Erreur lors de l'ajout", + "loanAdmin": "Administrateur", + "loanAvailable": "Disponible", + "loanAvailableMultiple": "Disponibles", + "loanBorrowed": "Emprunté", + "loanBorrowedMultiple": "Empruntés", + "loanAnd": "et", + "loanAssociation": "Association", + "loanAvailableItems": "Objets disponibles", + "loanBeginDate": "Date du début du prêt", + "loanBorrower": "Emprunteur", + "loanCaution": "Caution", + "loanCancel": "Annuler", + "loanConfirm": "Confirmer", + "loanConfirmation": "Confirmation", + "loanDates": "Dates", + "loanDays": "Jours", + "loanDelay": "Délai de la prolongation", + "loanDelete": "Supprimer", + "loanDeletingLoan": "Supprimer le prêt ?", + "loanDeletedItem": "Objet supprimé", + "loanDeletedLoan": "Prêt supprimé", + "loanDeleting": "Suppression", + "loanDeletingError": "Erreur lors de la suppression", + "loanDeletingItem": "Supprimer l'objet ?", + "loanDuration": "Durée", + "loanEdit": "Modifier", + "loanEditItem": "Modifier l'objet", + "loanEditLoan": "Modifier le prêt", + "loanEditedRoom": "Salle modifiée", + "loanEndDate": "Date de fin du prêt", + "loanEnded": "Terminé", + "loanEnterDate": "Veuillez entrer une date", + "loanExtendedLoan": "Prêt prolongé", + "loanExtendingError": "Erreur lors de la prolongation", + "loanHistory": "Historique", + "loanIncorrectOrMissingFields": "Des champs sont manquants ou incorrects", + "loanInvalidNumber": "Veuillez entrer un nombre", + "loanInvalidDates": "Les dates ne sont pas valides", + "loanItem": "Objet", + "loanItems": "Objets", + "loanItemHandling": "Gestion des objets", + "loanItemSelected": "objet sélectionné", + "loanItemsSelected": "objets sélectionnés", + "loanLendingDuration": "Durée possible du prêt", + "loanLoan": "Prêt", + "loanLoanHandling": "Gestion des prêts", + "loanLooking": "Rechercher", + "loanName": "Nom", + "loanNext": "Suivant", + "loanNo": "Non", + "loanNoAssociationsFounded": "Aucune association trouvée", + "loanNoAvailableItems": "Aucun objet disponible", + "loanNoBorrower": "Aucun emprunteur", + "loanNoItems": "Aucun objet", + "loanNoItemSelected": "Aucun objet sélectionné", + "loanNoLoan": "Aucun prêt", + "loanNoReturnedDate": "Pas de date de retour", + "loanQuantity": "Quantité", + "loanNone": "Aucun", + "loanNote": "Note", + "loanNoValue": "Veuillez entrer une valeur", + "loanOnGoing": "En cours", + "loanOnGoingLoan": "Prêt en cours", + "loanOthers": "autres", + "loanPaidCaution": "Caution payée", + "loanPositiveNumber": "Veuillez entrer un nombre positif", + "loanPrevious": "Précédent", + "loanReturned": "Rendu", + "loanReturnedLoan": "Prêt rendu", + "loanReturningError": "Erreur lors du retour", + "loanReturningLoan": "Retour", + "loanReturnLoan": "Rendre le prêt ?", + "loanReturnLoanDescription": "Voulez-vous rendre ce prêt ?", + "loanToReturn": "A rendre", + "loanUnavailable": "Indisponible", + "loanUpdate": "Modifier", + "loanUpdatedItem": "Objet modifié", + "loanUpdatedLoan": "Prêt modifié", + "loanUpdatingError": "Erreur lors de la modification", + "loanYes": "Oui", + "loginAppName": "MyECL", + "loginCreateAccount": "Créer un compte", + "loginForgotPassword": "Mot de passe oublié ?", + "loginFruitVegetableOrders": "Commandes de fruits et légumes", + "loginInterfaceCustomization": "Personnalisation de l'interface", + "loginLoginFailed": "Échec de la connexion", + "loginMadeBy": "Développé par ProximApp", + "loginMaterialLoans": "Gestion des prêts de matériel", + "loginNewTermsElections": "L'élection des nouveaux mandats", + "loginRaffles": "Tombolas", + "loginSignIn": "Se connecter", + "loginRegister": "S'inscrire", + "loginShortDescription": "L'application de l'associatif", + "loginUpcomingEvents": "Les évènements à venir", + "loginUpcomingScreenings": "Les prochaines séances", + "othersCheckInternetConnection": "Veuillez vérifier votre connexion internet", + "othersRetry": "Réessayer", + "othersTooOldVersion": "Votre version de l'application est trop ancienne.\n\nVeuillez mettre à jour l'application.", + "othersUnableToConnectToServer": "Impossible de se connecter au serveur", + "othersVersion": "Version", + "othersNoModule": "Aucun module disponible, veuillez réessayer ultérieurement 😢😢", + "othersAdmin": "Admin", + "othersError": "Une erreur est survenue", + "othersNoValue": "Veuillez entrer une valeur", + "othersInvalidNumber": "Veuillez entrer un nombre", + "othersNoDateError": "Veuillez entrer une date", + "othersImageSizeTooBig": "La taille de l'image ne doit pas dépasser 4 Mio", + "othersImageError": "Erreur lors de l'ajout de l'image", + "paiementAccept": "Accepter", + "paiementAccessPage": "Accéder à la page", + "paiementAdd": "Ajouter", + "paiementAddedSeller": "Vendeur ajouté", + "paiementAddingSellerError": "Erreur lors de l'ajout du vendeur", + "paiementAddingStoreError": "Erreur lors de l'ajout du magasin", + "paiementAddSeller": "Ajouter un vendeur", + "paiementAddStore": "Ajouter un magasin", + "paiementAddThisDevice": "Ajouter cet appareil", + "paiementAdmin": "Administrateur", + "paiementAmount": "Montant", + "paiementAskDeviceActivation": "Demande d'activation de l'appareil", + "paiementAStore": "un magasin", + "paiementAt": "à", + "paiementAuthenticationRequired": "Authentification requise pour payer", + "paiementAuthentificationFailed": "Échec de l'authentification", + "paiementBalanceAfterTopUp": "Solde après recharge :", + "paiementBalanceAfterTransaction": "Solde après paiement : ", + "paiementBank": "Encaisser", + "paiementBillingSpace": "Espace facturation", + "paiementCameraPermissionRequired": "Permission d'accès à la caméra requise", + "paiementCameraPerssionRequiredDescription": "Pour scanner un QR Code, vous devez autoriser l'accès à la caméra.", + "paiementCanBank": "Peut encaisser", + "paiementCanCancelTransaction": "Peut annuler des transactions", + "paiementCancel": "Annuler", + "paiementCancelled": "Annulé", + "paiementCancelledTransaction": "Paiement annulé", + "paiementCancelTransaction": "Annuler la transaction", + "paiementCancelTransactions": "Annuler les transactions", + "paiementCanManageSellers": "Peut gérer les vendeurs", + "paiementCanSeeHistory": "Peut voir l'historique", + "paiementCantLaunchURL": "Impossible d'ouvrir le lien", + "paiementClose": "Fermer", + "paiementCreate": "Créer", + "paiementCreateInvoice": "Créer une facture", + "paiementDecline": "Refuser", + "paiementDeletedSeller": "Vendeur supprimé", + "paiementDeleteInvoice": "Supprimer la facture", + "paiementDeleteSeller": "Supprimer le vendeur", + "paiementDeleteSellerDescription": "Voulez-vous vraiment supprimer ce vendeur ?", + "paiementDeleteSuccessfully": "Supprimé avec succès", + "paiementDeleteStore": "Supprimer le magasin", + "paiementDeleteStoreDescription": "Voulez-vous vraiment supprimer ce magasin ?", + "paiementDeleteStoreError": "Impossible de supprimer le magasin", + "paiementDeletingSellerError": "Erreur lors de la suppression du vendeur", + "paiementDeviceActivationReceived": "La demande d'activation est prise en compte, veuilliez consulter votre boite mail pour finaliser la démarche", + "paiementDeviceNotActivated": "Appareil non activé", + "paiementDeviceNotActivatedDescription": "Votre appareil n'est pas encore activé. \nPour l'activer, veuillez vous rendre sur la page des appareils.", + "paiementDeviceNotRegistered": "Appareil non enregistré", + "paiementDeviceNotRegisteredDescription": "Votre appareil n'est pas encore enregistré. \nPour l'enregistrer, veuillez vous rendre sur la page des appareils.", + "paiementDeviceRecoveryError": "Erreur lors de la récupération de l'appareil", + "paiementDeviceRevoked": "Appareil révoqué", + "paiementDeviceRevokingError": "Erreur lors de la révocation de l'appareil", + "paiementDevices": "Appareils", + "paiementDoneTransaction": "Transaction effectuée", + "paiementDownload": "Télécharger", + "paiementEditStore": "Modifier le magasin {store}", + "@paiementEditStore": { + "description": "Modifier le magasin", + "placeholders": { + "store": { + "type": "String" + } + } + }, + "paiementErrorDeleting": "Erreur lors de la suppression", + "paiementErrorUpdatingStatus": "Erreur lors de la mise à jour du statut", + "paiementFromTo": "Du {from} au {to}", + "@paiementFromTo": { + "description": "Text with a date range", + "placeholders": { + "from": { + "type": "DateTime", + "format": "yMd" + }, + "to": { + "type": "DateTime", + "format": "yMd" + } + } + }, + "paiementGetBalanceError": "Erreur lors de la récupération du solde : ", + "paiementGetTransactionsError": "Erreur lors de la récupération des transactions : ", + "paiementHandOver": "Passation", + "paiementHistory": "Historique", + "paiementInvoiceCreatedSuccessfully": "Facture créée avec succès", + "paiementInvoices": "Factures", + "paiementInvoicesPerPage": "{quantity} factures/page", + "@paiementInvoicesPerPage": { + "description": "Text with the number of invoices per page", + "placeholders": { + "quantity": { + "type": "int" + } + } + }, + "paiementLastTransactions": "Dernières transactions", + "paiementLimitedTo": "Limité à", + "paiementManagement": "Gestion", + "paiementManageSellers": "Gérer les vendeurs", + "paiementMarkPaid": "Marquer comme payé", + "paiementMarkReceived": "Marquer comme reçu", + "paiementMarkUnpaid": "Marquer comme non payé", + "paiementMaxAmount": "Le montant maximum de votre portefeuille est de", + "paiementMean": "Moyenne : ", + "paiementModify": "Modifier", + "paiementModifyingStoreError": "Erreur lors de la modification du magasin", + "paiementModifySuccessfully": "Modifié avec succès", + "paiementNewCGU": "Nouvelles Conditions Générales d'Utilisation", + "paiementNext": "Suivant", + "paiementNextAccountable": "Prochain responsable", + "paiementNoInvoiceToCreate": "Aucune facture à créer", + "paiementNoMembership": "Aucune adhésion", + "paiementNoMembershipDescription": "Ce produit n'est pas disponnible pour les non-adhérents. Confirmer l'encaissement ?", + "paiementNoThanks": "Non merci", + "paiementNoTransaction": "Aucune transaction", + "paiementNoTransactionForThisMonth": "Aucune transaction pour ce mois", + "paiementOf": "de", + "paiementPaid": "Payé", + "paiementPay": "Payer", + "paiementPayment": "Paiement", + "paiementPayWithHA": "Payer avec HelloAsso", + "paiementPending": "En attente", + "paiementPersonalBalance": "Solde personnel", + "paiementAddFunds": "Ajouter des fonds", + "paiementInsufficientFunds": "Fonds insuffisants", + "paiementTimeRemaining": "Temps restant", + "paiementHurryUp": "Dépêchez-vous !", + "paiementCompletePayment": "Finaliser le paiement", + "paiementConfirmPayment": "Confirmer le paiement", + "paiementPleaseAcceptPopup": "Veuillez autoriser les popups", + "paiementPleaseAcceptTOS": "Veuillez accepter les Conditions Générales d'Utilisation.", + "paiementPleaseAddDevice": "Veuillez ajouter cet appareil pour payer", + "paiementPleaseAuthenticate": "Veuillez vous authentifier", + "paiementPleaseEnterMinAmount": "Veuillez entrer un montant supérieur à 1", + "paiementPleaseEnterValidAmount": "Veuillez entrer un montant valide", + "paiementProceedSuccessfully": "Paiement effectué avec succès", + "paiementQRCodeAlreadyUsed": "QR Code déjà utilisé", + "paiementReactivateRevokedDeviceDescription": "Votre appareil a été révoqué. \nPour le réactiver, veuillez vous rendre sur la page des appareils.", + "paiementReceived": "Reçu", + "paiementRefund": "Remboursement", + "paiementRefundAction": "Rembourser", + "paiementRefundedThe": "Remboursé le", + "paiementRevokeDevice": "Révoquer l'appareil ?", + "paiementRevokeDeviceDescription": "Vous ne pourrez plus utiliser cet appareil pour les paiements", + "paiementRightsOf": "Droits de", + "paiementRightsUpdated": "Droits mis à jour", + "paiementRightsUpdateError": "Erreur lors de la mise à jour des droits", + "paiementScan": "Scanner", + "paiementScanAlreadyUsedQRCode": "QR Code déjà utilisé", + "paiementScanCode": "Scanner un code", + "paiementScanNoMembership": "Pas d'adhésion", + "paiementScanNoMembershipConfirmation": "Ce produit n'est pas disponnible pour les non-adhérents. Confirmer l'encaissement ?", + "paiementSeeHistory": "Voir l'historique", + "paiementSelectStructure": "Choisir une structure", + "paiementSellerError": "Vous n'êtes pas vendeur de ce magasin", + "paiementSellerRigths": "Droits du vendeur", + "paiementSellersOf": "Les vendeurs de", + "paiementSettings": "Paramètres", + "paiementSpent": "Déboursé", + "paiementStats": "Stats", + "paiementStoreBalance": "Solde du magasin", + "paiementStoreDeleted": "Magasin supprimée", + "paiementStructureManagement": "Gestion de {structure}", + "@paiementStructureManagement": { + "description": "Gestion de la structure", + "placeholders": { + "structure": { + "type": "String" + } + } + }, + "paiementStoreName": "Nom du magasin", + "paiementStores": "Magasins", + "paiementStructureAdmin": "Administrateur de la structure", + "paiementSuccededTransaction": "Paiement réussi", + "paiementConfirmYourPurchase": "Confirmer votre achat", + "paiementYourBalance": "Votre solde", + "paiementPaymentSuccessful": "Paiement réussi !", + "paiementPaymentCanceled": "Paiement annulé", + "paiementPaymentRequest": "Demande de paiement", + "paiementPaymentRequestAccepted": "Demande de paiement acceptée", + "paiementPaymentRequestRefused": "Demande de paiement refusée", + "paiementPaymentRequestError": "Erreur lors du traitement de la demande", + "paiementAccept": "Accepter", + "paiementRefuse": "Refuser", + "paiementSuccessfullyAddedStore": "Magasin ajoutée avec succès", + "paiementSuccessfullyModifiedStore": "Magasin modifiée avec succès", + "paiementThe": "Le", + "paiementThisDevice": "(cet appareil)", + "paiementTopUp": "Recharge", + "paiementTopUpAction": "Recharger", + "paiementTotalDuringPeriod": "Total sur la période", + "paiementTransaction": "ransaction", + "paiementTransactionCancelled": "Transaction annulée", + "paiementTransactionCancelledDescription": "Voulez-vous vraiment annuler la transaction de", + "paiementTransactionCancelledError": "Erreur lors de l'annulation de la transaction", + "paiementTransferStructure": "Transfert de structure", + "paiementTransferStructureDescription": "Le nouveau responsable aura accès à toutes les fonctionnalités de gestion de la structure. Vous allez recevoir un email pour valider ce transfert. Le lien ne sera actif que pendant 20 minutes. Cette action est irréversible. Êtes-vous sûr de vouloir continuer ?", + "paiementTransferStructureError": "Erreur lors du transfert de la structure", + "paiementTransferStructureSuccess": "Transfert de structure demandé avec succès", + "paiementUnknownDevice": "Appareil inconnu", + "paiementValidUntil": "Valide jusqu'à", + "paiementYouAreTransferingStructureTo": "Vous êtes sur le point de transférer la structure à ", + "phAddNewJournal": "Ajouter un nouveau journal", + "phNameField": "Nom : ", + "phDateField": "Date : ", + "phDelete": "Voulez-vous vraiment supprimer ce journal ?", + "phIrreversibleAction": "Cette action est irréversible", + "phToHeavyFile": "Fichier trop volumineux", + "phAddPdfFile": "Ajouter un fichier PDF", + "phEditPdfFile": "Modifier le fichier PDF", + "phPhName": "Nom du PH", + "phDate": "Date", + "phAdded": "Ajouté", + "phEdited": "Modifié", + "phAddingFileError": "Erreur d'ajout", + "phMissingInformatonsOrPdf": "Informations manquantes ou fichier PDF manquant", + "phAdd": "Ajouter", + "phEdit": "Modifier", + "phSeePreviousJournal": "Voir les anciens journaux", + "phNoJournalInDatabase": "Pas encore de PH dans la base de donnée", + "phSuccesDowloading": "Téléchargé avec succès", + "phonebookAdd": "Ajouter", + "phonebookAddAssociation": "Ajouter une association", + "phonebookAddAssociationGroupement": "Ajouter un groupement d'association", + "phonebookAddedAssociation": "Association ajoutée", + "phonebookAddedMember": "Membre ajouté", + "phonebookAddingError": "Erreur lors de l'ajout", + "phonebookAddMember": "Ajouter un membre", + "phonebookAddRole": "Ajouter un rôle", + "phonebookAdmin": "Admin", + "phonebookAll": "Toutes", + "phonebookApparentName": "Nom public du rôle :", + "phonebookAssociation": "Association", + "phonebookAssociationDetail": "Détail de l'association :", + "phonebookAssociationGroupement": "Groupement d'association", + "phonebookAssociationKind": "Type d'association :", + "phonebookAssociationName": "Nom de l'association", + "phonebookAssociations": "Associations", + "phonebookCancel": "Annuler", + "phonebookChangeTermYear": "Passer au mandat {year}", + "@phonebookChangeTermYear": { + "description": "Permet de changer le mandat d'une association", + "placeholders": { + "year": { + "type": "int" + } + } + }, + "phonebookChangeTermConfirm": "Êtes-vous sûr de vouloir changer tout le mandat ?\nCette action est irréversible !", + "phonebookClose": "Fermer", + "phonebookConfirm": "Confirmer", + "phonebookCopied": "Copié dans le presse-papier", + "phonebookDeactivateAssociation": "Désactiver l'association", + "phonebookDeactivatedAssociation": "Association désactivée", + "phonebookDeactivatedAssociationWarning": "Attention, cette association est désactivée, vous ne pouvez pas la modifier", + "phonebookDeactivateSelectedAssociation": "Désactiver l'association {association} ?", + "@phonebookDeactivateSelectedAssociation": { + "description": "Permet de désactiver une association", + "placeholders": { + "association": { + "type": "String" + } + } + }, + "phonebookDeactivatingError": "Erreur lors de la désactivation", + "phonebookDetail": "Détail :", + "phonebookDelete": "Supprimer", + "phonebookDeleteAssociation": "Supprimer l'association", + "phonebookDeleteSelectedAssociation": "Supprimer l'association {association} ?", + "@phonebookDeleteSelectedAssociation": { + "description": "Permet de supprimer une association", + "placeholders": { + "association": { + "type": "String" + } + } + }, + "phonebookDeleteAssociationDescription": "Ceci va supprimer l'historique de l'association", + "phonebookDeletedAssociation": "Association supprimée", + "phonebookDeletedMember": "Membre supprimé", + "phonebookDeleteRole": "Supprimer le rôle", + "phonebookDeleteUserRole": "Supprimer le rôle de l'utilisateur {name} ?", + "@phonebookDeleteUserRole": { + "description": "Permet de supprimer le rôle d'un utilisateur dans une association", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "phonebookDeactivating": "Désactiver l'association ?", + "phonebookDeleting": "Suppression", + "phonebookDeletingError": "Erreur lors de la suppression", + "phonebookDescription": "Description", + "phonebookEdit": "Modifier", + "phonebookEditAssociationGroupement": "Modifier le groupement d'association", + "phonebookEditAssociationGroups": "Gérer les groupes", + "phonebookEditAssociationInfo": "Modifier", + "phonebookEditAssociationMembers": "Gérer les membres", + "phonebookEditRole": "Modifier le rôle", + "phonebookEditMembership": "Modifier le rôle", + "phonebookEmail": "Email :", + "phonebookEmailCopied": "Email copié dans le presse-papier", + "phonebookEmptyApparentName": "Veuillez entrer un nom de role", + "phonebookEmptyFieldError": "Un champ n'est pas rempli", + "phonebookEmptyKindError": "Veuillez choisir un type d'association", + "phonebookEmptyMember": "Aucun membre sélectionné", + "phonebookErrorAssociationLoading": "Erreur lors du chargement de l'association", + "phonebookErrorAssociationNameEmpty": "Veuillez entrer un nom d'association", + "phonebookErrorAssociationPicture": "Erreur lors de la modification de la photo d'association", + "phonebookErrorKindsLoading": "Erreur lors du chargement des types d'association", + "phonebookErrorLoadAssociationList": "Erreur lors du chargement de la liste des associations", + "phonebookErrorLoadAssociationMember": "Erreur lors du chargement des membres de l'association", + "phonebookErrorLoadAssociationPicture": "Erreur lors du chargement de la photo d'association", + "phonebookErrorLoadProfilePicture": "Erreur", + "phonebookErrorRoleTagsLoading": "Erreur lors du chargement des tags de rôle", + "phonebookExistingMembership": "Ce membre est déjà dans le mandat actuel", + "phonebookFilter": "Filtrer", + "phonebookFilterDescription": "Filtrer les associations par type", + "phonebookFirstname": "Prénom :", + "phonebookGroupementDeleted": "Groupement d'association supprimé", + "phonebookGroupementDeleteError": "Erreur lors de la suppression du groupement d'association", + "phonebookGroupementName": "Nom du groupement", + "phonebookGroups": "Gérer les groupes de {association}", + "@phonebookGroups": { + "description": "Permet de gérer les groupes d'une association", + "placeholders": { + "association": { + "type": "String" + } + } + }, + "phonebookTerm": "Mandat {year}", + "@phonebookTerm": { + "description": "Année de mandat d'une association", + "placeholders": { + "year": { + "type": "int" + } + } + }, + "phonebookTermChangingError": "Erreur lors du changement de mandat", + "phonebookMember": "Membre", + "phonebookMemberReordered": "Membre réordonné", + "phonebookMembers": "Gérer les membres de {association}", + "@phonebookMembers": { + "description": "Permet de gérer les membres d'une association", + "placeholders": { + "association": { + "type": "String" + } + } + }, + "phonebookMembershipAssociationError": "Veuillez choisir une association", + "phonebookMembershipRole": "Rôle :", + "phonebookMembershipRoleError": "Veuillez choisir un rôle", + "phonebookModifyMembership": "Modifier le rôle de {name}", + "@phonebookModifyMembership": { + "description": "Permet de modifier le rôle d'un membre dans une association", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "phonebookName": "Nom :", + "phonebookNameCopied": "Nom et prénom copié dans le presse-papier", + "phonebookNamePure": "Nom", + "phonebookNewTerm": "Nouveau mandat", + "phonebookNewTermConfirmed": "Mandat changé", + "phonebookNickname": "Surnom :", + "phonebookNicknameCopied": "Surnom copié dans le presse-papier", + "phonebookNoAssociationFound": "Aucune association trouvée", + "phonebookNoMember": "Aucun membre", + "phonebookNoMemberRole": "Aucun role trouvé", + "phonebookNoRoleTags": "Aucun tag de rôle trouvé", + "phonebookPhone": "Téléphone :", + "phonebookPhonebook": "Annuaire", + "phonebookPhonebookSearch": "Rechercher", + "phonebookPhonebookSearchAssociation": "Association", + "phonebookPhonebookSearchField": "Rechercher :", + "phonebookPhonebookSearchName": "Nom/Prénom/Surnom", + "phonebookPhonebookSearchRole": "Poste", + "phonebookPresidentRoleTag": "Prez'", + "phonebookPromoNotGiven": "Promo non renseignée", + "phonebookPromotion": "Promotion {year}", + "@phonebookPromotion": { + "description": "Année de promotion d'un membre", + "placeholders": { + "year": { + "type": "int" + } + } + }, + "phonebookReorderingError": "Erreur lors du réordonnement", + "phonebookResearch": "Rechercher", + "phonebookRolePure": "Rôle", + "phonebookSearchUser": "Rechercher un utilisateur", + "phonebookTooHeavyAssociationPicture": "L'image est trop lourde (max 4Mo)", + "phonebookUpdateGroups": "Mettre à jour les groupes", + "phonebookUpdatedAssociation": "Association modifiée", + "phonebookUpdatedAssociationPicture": "La photo d'association a été changée", + "phonebookUpdatedGroups": "Groupes mis à jour", + "phonebookUpdatedMember": "Membre modifié", + "phonebookUpdatingError": "Erreur lors de la modification", + "phonebookValidation": "Valider", + "purchasesPurchases": "Achats", + "purchasesResearch": "Rechercher", + "purchasesNoPurchasesFound": "Aucun achat trouvé", + "purchasesNoTickets": "Aucun ticket", + "purchasesTicketsError": "Erreur lors du chargement des tickets", + "purchasesPurchasesError": "Erreur lors du chargement des achats", + "purchasesNoPurchases": "Aucun achat", + "purchasesTimes": "fois", + "purchasesAlreadyUsed": "Déjà utilisé", + "purchasesNotPaid": "Non validé", + "purchasesPleaseSelectProduct": "Veuillez sélectionner un produit", + "purchasesProducts": "Produits", + "purchasesCancel": "Annuler", + "purchasesValidate": "Valider", + "purchasesLeftScan": "Scans restants", + "purchasesTag": "Tag", + "purchasesHistory": "Historique", + "purchasesPleaseSelectSeller": "Veuillez sélectionner un vendeur", + "purchasesNoTagGiven": "Attention, aucun tag n'a été entré", + "purchasesTickets": "Tickets", + "purchasesNoScannableProducts": "Aucun produit scannable", + "purchasesLoading": "En attente de scan", + "purchasesScan": "Scanner", + "raffleRaffle": "Tombola", + "rafflePrize": "Lot", + "rafflePrizes": "Lots", + "raffleActualRaffles": "Tombola en cours", + "rafflePastRaffles": "Tombola passés", + "raffleYourTickets": "Tous vos tickets", + "raffleCreateMenu": "Menu de Création", + "raffleNextRaffles": "Prochaines tombolas", + "raffleNoTicket": "Vous n'avez pas de ticket", + "raffleSeeRaffleDetail": "Voir lots/tickets", + "raffleActualPrize": "Lots actuels", + "raffleMajorPrize": "Lot Majeurs", + "raffleTakeTickets": "Prendre vos tickets", + "raffleNoTicketBuyable": "Vous ne pouvez pas achetez de billets pour l'instant", + "raffleNoCurrentPrize": "Il n'y a aucun lots actuellement", + "raffleModifTombola": "Vous pouvez modifiez vos tombolas ou en créer de nouvelles, toute décision doit ensuite être prise par les admins", + "raffleCreateYourRaffle": "Votre menu de création de tombolas", + "rafflePossiblePrice": "Prix possible", + "raffleInformation": "Information et Statistiques", + "raffleAccounts": "Comptes", + "raffleAdd": "Ajouter", + "raffleUpdatedAmount": "Montant mis à jour", + "raffleUpdatingError": "Erreur lors de la mise à jour", + "raffleDeletedPrize": "Lot supprimé", + "raffleDeletingError": "Erreur lors de la suppression", + "raffleQuantity": "Quantité", + "raffleClose": "Fermer", + "raffleOpen": "Ouvrir", + "raffleAddTypeTicketSimple": "Ajouter", + "raffleAddingError": "Erreur lors de l'ajout", + "raffleEditTypeTicketSimple": "Modifier", + "raffleFillField": "Le champ ne peut pas être vide", + "raffleWaiting": "Chargement", + "raffleEditingError": "Erreur lors de la modification", + "raffleAddedTicket": "Ticket ajouté", + "raffleEditedTicket": "Ticket modifié", + "raffleAlreadyExistTicket": "Le ticket existe déjà", + "raffleNumberExpected": "Un entier est attendu", + "raffleDeletedTicket": "Ticket supprimé", + "raffleAddPrize": "Ajouter", + "raffleEditPrize": "Modifier", + "raffleOpenRaffle": "Ouvrir la tombola", + "raffleCloseRaffle": "Fermer la tombola", + "raffleOpenRaffleDescription": "Vous allez ouvrir la tombola, les utilisateurs pourront acheter des tickets. Vous ne pourrez plus modifier la tombola. Êtes-vous sûr de vouloir continuer ?", + "raffleCloseRaffleDescription": "Vous allez fermer la tombola, les utilisateurs ne pourront plus acheter de tickets. Êtes-vous sûr de vouloir continuer ?", + "raffleNoCurrentRaffle": "Il n'y a aucune tombola en cours", + "raffleBoughtTicket": "Ticket acheté", + "raffleDrawingError": "Erreur lors du tirage", + "raffleInvalidPrice": "Le prix doit être supérieur à 0", + "raffleMustBePositive": "Le nombre doit être strictement positif", + "raffleDraw": "Tirer", + "raffleDrawn": "Tiré", + "raffleError": "Erreur", + "raffleGathered": "Récolté", + "raffleTickets": "Tickets", + "raffleTicket": "ticket", + "raffleWinner": "Gagnant", + "raffleNoPrize": "Aucun lot", + "raffleDeletePrize": "Supprimer le lot", + "raffleDeletePrizeDescription": "Vous allez supprimer le lot, êtes-vous sûr de vouloir continuer ?", + "raffleDrawing": "Tirage", + "raffleDrawingDescription": "Tirer le gagnant du lot ?", + "raffleDeleteTicket": "Supprimer le ticket", + "raffleDeleteTicketDescription": "Vous allez supprimer le ticket, êtes-vous sûr de vouloir continuer ?", + "raffleWinningTickets": "Tickets gagnants", + "raffleNoWinningTicketYet": "Les tickets gagnants seront affichés ici", + "raffleName": "Nom", + "raffleDescription": "Description", + "raffleBuyThisTicket": "Acheter ce ticket", + "raffleLockedRaffle": "Tombola verrouillée", + "raffleUnavailableRaffle": "Tombola indisponible", + "raffleNotEnoughMoney": "Vous n'avez pas assez d'argent", + "raffleWinnable": "gagnable", + "raffleNoDescription": "Aucune description", + "raffleAmount": "Solde", + "raffleLoading": "Chargement", + "raffleTicketNumber": "Nombre de ticket", + "rafflePrice": "Prix", + "raffleEditRaffle": "Modifier la tombola", + "raffleEdit": "Modifier", + "raffleAddPackTicket": "Ajouter un pack de ticket", + "recommendationRecommendation": "Bons plans", + "recommendationTitle": "Titre", + "recommendationLogo": "Logo", + "recommendationCode": "Code", + "recommendationSummary": "Court résumé", + "recommendationDescription": "Description", + "recommendationAdd": "Ajouter", + "recommendationEdit": "Modifier", + "recommendationDelete": "Supprimer", + "recommendationAddImage": "Veuillez ajouter une image", + "recommendationAddedRecommendation": "Bon plan ajouté", + "recommendationEditedRecommendation": "Bon plan modifié", + "recommendationDeleteRecommendationConfirmation": "Êtes-vous sûr de vouloir supprimer ce bon plan ?", + "recommendationDeleteRecommendation": "Suppresion", + "recommendationDeletingRecommendationError": "Erreur lors de la suppression", + "recommendationDeletedRecommendation": "Bon plan supprimé", + "recommendationIncorrectOrMissingFields": "Champs incorrects ou manquants", + "recommendationEditingError": "Échec de la modification", + "recommendationAddingError": "Échec de l'ajout", + "recommendationCopiedCode": "Code de réduction copié", + "seedLibraryAdd": "Ajouter", + "seedLibraryAddedPlant": "Plante ajoutée", + "seedLibraryAddedSpecies": "Espèce ajoutée", + "seedLibraryAddingError": "Erreur lors de l'ajout", + "seedLibraryAddPlant": "Déposer une plante", + "seedLibraryAddSpecies": "Ajouter une espèce", + "seedLibraryAll": "Toutes", + "seedLibraryAncestor": "Ancêtre", + "seedLibraryAround": "environ", + "seedLibraryAutumn": "Automne", + "seedLibraryBorrowedPlant": "Plante empruntée", + "seedLibraryBorrowingDate": "Date d'emprunt :", + "seedLibraryBorrowPlant": "Emprunter la plante", + "seedLibraryCard": "Carte", + "seedLibraryChoosingAncestor": "Veuillez choisir un ancêtre", + "seedLibraryChoosingSpecies": "Veuillez choisir une espèce", + "seedLibraryChoosingSpeciesOrAncestor": "Veuillez choisir une espèce ou un ancêtre", + "seedLibraryContact": "Contact :", + "seedLibraryDays": "jours", + "seedLibraryDeadMsg": "Voulez-vous déclarer la plante morte ?", + "seedLibraryDeadPlant": "Plante morte", + "seedLibraryDeathDate": "Date de mort", + "seedLibraryDeletedSpecies": "Espèce supprimée", + "seedLibraryDeleteSpecies": "Supprimer l'espèce ?", + "seedLibraryDeleting": "Suppression", + "seedLibraryDeletingError": "Erreur lors de la suppression", + "seedLibraryDepositNotAvailable": "Le dépôt de plantes n'est pas possible sans emprunter une plante au préalable", + "seedLibraryDescription": "Description", + "seedLibraryDifficulty": "Difficulté :", + "seedLibraryEdit": "Modifier", + "seedLibraryEditedPlant": "Plante modifiée", + "seedLibraryEditInformation": "Modifier les informations", + "seedLibraryEditingError": "Erreur lors de la modification", + "seedLibraryEditSpecies": "Modifier l'espèce", + "seedLibraryEmptyDifficultyError": "Veuillez choisir une difficulté", + "seedLibraryEmptyFieldError": "Veuillez remplir tous les champs", + "seedLibraryEmptyTypeError": "Veuillez choisir un type de plante", + "seedLibraryEndMonth": "Mois de fin :", + "seedLibraryFacebookUrl": "Lien Facebook", + "seedLibraryFilters": "Filtres", + "seedLibraryForum": "Oskour maman j'ai tué ma plante - Forum d'aide", + "seedLibraryForumUrl": "Lien Forum", + "seedLibraryHelpSheets": "Fiches sur les plantes", + "seedLibraryInformation": "Informations :", + "seedLibraryMaturationTime": "Temps de maturation", + "seedLibraryMonthJan": "Janvier", + "seedLibraryMonthFeb": "Février", + "seedLibraryMonthMar": "Mars", + "seedLibraryMonthApr": "Avril", + "seedLibraryMonthMay": "Mai", + "seedLibraryMonthJun": "Juin", + "seedLibraryMonthJul": "Juillet", + "seedLibraryMonthAug": "Août", + "seedLibraryMonthSep": "Septembre", + "seedLibraryMonthOct": "Octobre", + "seedLibraryMonthNov": "Novembre", + "seedLibraryMonthDec": "Décembre", + "seedLibraryMyPlants": "Mes plantes", + "seedLibraryName": "Nom", + "seedLibraryNbSeedsRecommended": "Nombre de graines recommandées", + "seedLibraryNbSeedsRecommendedError": "Veuillez entrer un nombre de graines recommandé supérieur à 0", + "seedLibraryNoDateError": "Veuillez entrer une date", + "seedLibraryNoFilteredPlants": "Aucune plante ne correspond à votre recherche. Essayez d'autres filtres.", + "seedLibraryNoMorePlant": "Aucune plante n'est disponible", + "seedLibraryNoPersonalPlants": "Vous n'avez pas encore de plantes dans votre grainothèque. Vous pouvez en ajouter en allant dans les stocks.", + "seedLibraryNoSpecies": "Aucune espèce trouvée", + "seedLibraryNoStockPlants": "Aucune plante disponible dans le stock", + "seedLibraryNotes": "Notes", + "seedLibraryOk": "OK", + "seedLibraryPlantationPeriod": "Période de plantation :", + "seedLibraryPlantationType": "Type de plantation :", + "seedLibraryPlantDetail": "Détail de la plante", + "seedLibraryPlantingDate": "Date de plantation", + "seedLibraryPlantingNow": "Je la plante maintenant", + "seedLibraryPrefix": "Préfixe", + "seedLibraryPrefixError": "Prefixe déjà utilisé", + "seedLibraryPrefixLengthError": "Le préfixe doit faire 3 caractères", + "seedLibraryPropagationMethod": "Méthode de propagation :", + "seedLibraryReference": "Référence :", + "seedLibraryRemovedPlant": "Plante supprimée", + "seedLibraryRemovingError": "Erreur lors de la suppression", + "seedLibraryResearch": "Recherche", + "seedLibrarySaveChanges": "Sauvegarder les modifications", + "seedLibrarySeason": "Saison :", + "seedLibrarySeed": "Graine", + "seedLibrarySeeds": "graines", + "seedLibrarySeedDeposit": "Dépôt de plantes", + "seedLibrarySeedLibrary": "Grainothèque", + "seedLibrarySeedQuantitySimple": "Quantité de graines", + "seedLibrarySeedQuantity": "Quantité de graines :", + "seedLibraryShowDeadPlants": "Afficher les plantes mortes", + "seedLibrarySpecies": "Espèce :", + "seedLibrarySpeciesHelp": "Aide sur l'espèce", + "seedLibrarySpeciesPlural": "Espèces", + "seedLibrarySpeciesSimple": "Espèce", + "seedLibrarySpeciesType": "Type d'espèce :", + "seedLibrarySpring": "Printemps", + "seedLibraryStartMonth": "Mois de début :", + "seedLibraryStock": "Stock disponible", + "seedLibrarySummer": "Été", + "seedLibraryStocks": "Stocks", + "seedLibraryTimeUntilMaturation": "Temps avant maturation :", + "seedLibraryType": "Type :", + "seedLibraryUnableToOpen": "Impossible d'ouvrir le lien", + "seedLibraryUpdate": "Modifier", + "seedLibraryUpdatedInformation": "Informations modifiées", + "seedLibraryUpdatedSpecies": "Espèce modifiée", + "seedLibraryUpdatedPlant": "Plante modifiée", + "seedLibraryUpdatingError": "Erreur lors de la modification", + "seedLibraryWinter": "Hiver", + "seedLibraryWriteReference": "Veuillez écrire la référence suivante : ", + "settingsAccount": "Compte", + "settingsAddProfilePicture": "Ajouter une photo", + "settingsAdmin": "Administrateur", + "settingsAskHelp": "Demander de l'aide", + "settingsAssociation": "Association", + "settingsBirthday": "Date de naissance", + "settingsBugs": "Bugs", + "settingsChangePassword": "Changer de mot de passe", + "settingsChangingPassword": "Voulez-vous vraiment changer votre mot de passe ?", + "settingsConfirmPassword": "Confirmer le mot de passe", + "settingsCopied": "Copié !", + "settingsDarkMode": "Mode sombre", + "settingsDarkModeOff": "Désactivé", + "settingsDeleteLogs": "Supprimer les logs ?", + "settingsDeleteNotificationLogs": "Supprimer les logs des notifications ?", + "settingsDetelePersonalData": "Supprimer mes données personnelles", + "settingsDetelePersonalDataDesc": "Cette action notifie l'administrateur que vous souhaitez supprimer vos données personnelles.", + "settingsDeleting": "Suppresion", + "settingsEdit": "Modifier", + "settingsEditAccount": "Modifier mon profil", + "settingsEmail": "Email", + "settingsEmptyField": "Ce champ ne peut pas être vide", + "settingsErrorProfilePicture": "Erreur lors de la modification de la photo de profil", + "settingsErrorSendingDemand": "Erreur lors de l'envoi de la demande", + "settingsEventsIcal": "Lien Ical des événements", + "settingsExpectingDate": "Date de naissance attendue", + "settingsFirstname": "Prénom", + "settingsFloor": "Étage", + "settingsHelp": "Aide", + "settingsIcalCopied": "Lien Ical copié !", + "settingsLanguage": "Langue", + "settingsLanguageVar": "Français 🇫🇷", + "settingsLogs": "Logs", + "settingsModules": "Modules", + "settingsMyIcs": "Mon lien Ical", + "settingsName": "Nom", + "settingsNewPassword": "Nouveau mot de passe", + "settingsNickname": "Surnom", + "settingsNotifications": "Notifications", + "settingsOldPassword": "Ancien mot de passe", + "settingsPasswordChanged": "Mot de passe changé", + "settingsPasswordsNotMatch": "Les mots de passe ne correspondent pas", + "settingsPersonalData": "Données personnelles", + "settingsPersonalisation": "Personnalisation", + "settingsPhone": "Téléphone", + "settingsProfilePicture": "Photo de profil", + "settingsPromo": "Promotion", + "settingsRepportBug": "Signaler un bug", + "settingsSave": "Enregistrer", + "settingsSecurity": "Sécurité", + "settingsSendedDemand": "Demande envoyée", + "settingsSettings": "Paramètres", + "settingsTooHeavyProfilePicture": "L'image est trop lourde (max 4Mo)", + "settingsUpdatedProfile": "Profil modifié", + "settingsUpdatedProfilePicture": "Photo de profil modifiée", + "settingsUpdateNotification": "Mettre à jour les notifications", + "settingsUpdatingError": "Erreur lors de la modification du profil", + "settingsVersion": "Version", + "settingsPasswordStrength": "Force du mot de passe", + "settingsPasswordStrengthVeryWeak": "Très faible", + "settingsPasswordStrengthWeak": "Faible", + "settingsPasswordStrengthMedium": "Moyen", + "settingsPasswordStrengthStrong": "Fort", + "settingsPasswordStrengthVeryStrong": "Très fort", + "settingsPhoneNumber": "Numéro de téléphone", + "settingsValidate": "Valider", + "settingsEditedAccount": "Compte modifié avec succès", + "settingsFailedToEditAccount": "Échec de la modification du compte", + "settingsChooseLanguage": "Choix de la langue", + "settingsNotificationCounter": "{active}/{total} {active, plural, zero {activée} one {activée} other {activées}}", + "@settingsNotificationCounter": { + "description": "Affiche le nombre de notifications actives sur le total des notifications disponibles, avec gestion du pluriel", + "placeholders": { + "active": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "settingsEvent": "Événement", + "settingsIcal": "Lien Ical", + "settingsSynncWithCalendar": "Synchroniser avec votre calendrier", + "settingsIcalLinkCopied": "Lien Ical copié dans le presse-papier", + "settingsProfile": "Profil", + "settingsConnexion": "Connexion", + "settingsLogOut": "Se déconnecter", + "settingsLogOutDescription": "Êtes-vous sûr de vouloir vous déconnecter ?", + "settingsLogOutSuccess": "Déconnexion réussie", + "settingsDeleteMyAccount": "Supprimer mon compte", + "settingsDeleteMyAccountDescription": "Cette action notifie l'administrateur que vous souhaitez supprimer votre compte.", + "settingsDeletionAsked": "Demande de suppression de compte envoyée", + "settingsDeleteMyAccountError": "Erreur lors de la demande de suppression de compte", + "voteAdd": "Ajouter", + "voteAddMember": "Ajouter un membre", + "voteAddedPretendance": "Liste ajoutée", + "voteAddedSection": "Section ajoutée", + "voteAddingError": "Erreur lors de l'ajout", + "voteAddPretendance": "Ajouter une liste", + "voteAddSection": "Ajouter une section", + "voteAll": "Tous", + "voteAlreadyAddedMember": "Membre déjà ajouté", + "voteAlreadyVoted": "Vote enregistré", + "voteChooseList": "Choisir une liste", + "voteClear": "Réinitialiser", + "voteClearVotes": "Réinitialiser les votes", + "voteClosedVote": "Votes clos", + "voteCloseVote": "Fermer les votes", + "voteConfirmVote": "Confirmer le vote", + "voteCountVote": "Dépouiller les votes", + "voteDelete": "Supprimer", + "voteDeletedAll": "Tout supprimé", + "voteDeletedPipo": "Listes pipos supprimées", + "voteDeletedSection": "Section supprimée", + "voteDeleteAll": "Supprimer tout", + "voteDeleteAllDescription": "Voulez-vous vraiment supprimer tout ?", + "voteDeletePipo": "Supprimer les listes pipos", + "voteDeletePipoDescription": "Voulez-vous vraiment supprimer les listes pipos ?", + "voteDeletePretendance": "Supprimer la liste", + "voteDeletePretendanceDesc": "Voulez-vous vraiment supprimer cette liste ?", + "voteDeleteSection": "Supprimer la section", + "voteDeleteSectionDescription": "Voulez-vous vraiment supprimer cette section ?", + "voteDeletingError": "Erreur lors de la suppression", + "voteDescription": "Description", + "voteEdit": "Modifier", + "voteEditedPretendance": "Liste modifiée", + "voteEditedSection": "Section modifiée", + "voteEditingError": "Erreur lors de la modification", + "voteErrorClosingVotes": "Erreur lors de la fermeture des votes", + "voteErrorCountingVotes": "Erreur lors du dépouillement des votes", + "voteErrorResetingVotes": "Erreur lors de la réinitialisation des votes", + "voteErrorOpeningVotes": "Erreur lors de l'ouverture des votes", + "voteIncorrectOrMissingFields": "Champs incorrects ou manquants", + "voteMembers": "Membres", + "voteName": "Nom", + "voteNoPretendanceList": "Aucune liste de prétendance", + "voteNoSection": "Aucune section", + "voteCanNotVote": "Vous ne pouvez pas voter", + "voteNoSectionList": "Aucune section", + "voteNotOpenedVote": "Vote non ouvert", + "voteOnGoingCount": "Dépouillement en cours", + "voteOpenVote": "Ouvrir les votes", + "votePipo": "Pipo", + "votePretendance": "Listes", + "votePretendanceDeleted": "Prétendance supprimée", + "votePretendanceNotDeleted": "Erreur lors de la suppression", + "voteProgram": "Programme", + "votePublish": "Publier", + "votePublishVoteDescription": "Voulez-vous vraiment publier les votes ?", + "voteResetedVotes": "Votes réinitialisés", + "voteResetVote": "Réinitialiser les votes", + "voteResetVoteDescription": "Que voulez-vous faire ?", + "voteRole": "Rôle", + "voteSectionDescription": "Description de la section", + "voteSection": "Section", + "voteSectionName": "Nom de la section", + "voteSeeMore": "Voir plus", + "voteSelected": "Sélectionné", + "voteShowVotes": "Voir les votes", + "voteVote": "Vote", + "voteVoteError": "Erreur lors de l'enregistrement du vote", + "voteVoteFor": "Voter pour ", + "voteVoteNotStarted": "Vote non ouvert", + "voteVoters": "Groupes votants", + "voteVoteSuccess": "Vote enregistré", + "voteVotes": "Voix", + "voteVotesClosed": "Votes clos", + "voteVotesCounted": "Votes dépouillés", + "voteVotesOpened": "Votes ouverts", + "voteWarning": "Attention", + "voteWarningMessage": "La sélection ne sera pas sauvegardée.\nVoulez-vous continuer ?", + "moduleAdvert": "Feed", + "moduleAdvertDescription": "Gérer les feeds", + "moduleAmap": "AMAP", + "moduleAmapDescription": "Gérer les livraisons et les produits", + "moduleBooking": "Réservation", + "moduleBookingDescription": "Gérer les réservations, les salles et les managers", + "moduleCalendar": "Calendrier", + "moduleCalendarDescription": "Consulter les événements et les activités", + "moduleCentralisation": "Centralisation", + "moduleCentralisationDescription": "Gérer la centralisation des données", + "moduleCinema": "Cinéma", + "moduleCinemaDescription": "Gérer les séances de cinéma", + "moduleEvent": "Événement", + "moduleEventDescription": "Gérer les événements et les participants", + "moduleFlappyBird": "Flappy Bird", + "moduleFlappyBirdDescription": "Jouer à Flappy Bird et consulter le classement", + "moduleLoan": "Prêt", + "moduleLoanDescription": "Gérer les prêts et les articles", + "modulePhonebook": "Annuaire", + "modulePhonebookDescription": "Gérer les associations, les membres et les administrateurs", + "modulePurchases": "Achats", + "modulePurchasesDescription": "Gérer les achats, les tickets et l'historique", + "moduleRaffle": "Tombola", + "moduleRaffleDescription": "Gérer les tombolas, les prix et les tickets", + "moduleRecommendation": "Bons plans", + "moduleRecommendationDescription": "Gérer les recommandations, les informations et les administrateurs", + "moduleSeedLibrary": "Grainothèque", + "moduleSeedLibraryDescription": "Gérer les graines, les espèces et les stocks", + "moduleVote": "Vote", + "moduleVoteDescription": "Gérer les votes, les sections et les candidats", + "modulePh": "PH", + "modulePhDescription": "Gérer les PH, les formulaires et les administrateurs", + "moduleSettings": "Paramètres", + "moduleSettingsDescription": "Gérer les paramètres de l'application", + "moduleFeed": "Events", + "moduleFeedDescription": "Consulter les événements", + "moduleStyleGuide": "StyleGuide", + "moduleStyleGuideDescription": "Explore the UI components and styles used in Titan", + "moduleAdmin": "Admin", + "moduleAdminDescription": "Gérer les utilisateurs, groupes et structures", + "moduleOthers": "Autres", + "moduleOthersDescription": "Afficher les autres modules", + "modulePayment": "Paiement", + "modulePaymentDescription": "Gérer les paiements, les statistiques et les appareils", + "toolInvalidNumber": "Chiffre invalide", + "toolDateRequired": "Date requise", + "toolSuccess": "Succès" +} diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart new file mode 100644 index 0000000000..afb31aa700 --- /dev/null +++ b/lib/l10n/app_localizations.dart @@ -0,0 +1,8840 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_en.dart'; +import 'app_localizations_fr.dart'; + +// ignore_for_file: type=lint + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'l10n/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations? of(BuildContext context) { + return Localizations.of(context, AppLocalizations); + } + + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('en'), + Locale('fr'), + ]; + + /// No description provided for @dateToday. + /// + /// In fr, this message translates to: + /// **'Aujourd\'hui'** + String get dateToday; + + /// No description provided for @dateYesterday. + /// + /// In fr, this message translates to: + /// **'Hier'** + String get dateYesterday; + + /// No description provided for @dateTomorrow. + /// + /// In fr, this message translates to: + /// **'Demain'** + String get dateTomorrow; + + /// No description provided for @dateAt. + /// + /// In fr, this message translates to: + /// **'à'** + String get dateAt; + + /// No description provided for @dateFrom. + /// + /// In fr, this message translates to: + /// **'de'** + String get dateFrom; + + /// No description provided for @dateTo. + /// + /// In fr, this message translates to: + /// **'à'** + String get dateTo; + + /// No description provided for @dateBetweenDays. + /// + /// In fr, this message translates to: + /// **'au'** + String get dateBetweenDays; + + /// No description provided for @dateStarting. + /// + /// In fr, this message translates to: + /// **'Commence'** + String get dateStarting; + + /// No description provided for @dateLast. + /// + /// In fr, this message translates to: + /// **''** + String get dateLast; + + /// No description provided for @dateUntil. + /// + /// In fr, this message translates to: + /// **'Jusqu\'au'** + String get dateUntil; + + /// No description provided for @feedFilterAll. + /// + /// In fr, this message translates to: + /// **'Tous'** + String get feedFilterAll; + + /// No description provided for @feedFilterPending. + /// + /// In fr, this message translates to: + /// **'En attente'** + String get feedFilterPending; + + /// No description provided for @feedFilterApproved. + /// + /// In fr, this message translates to: + /// **'Approuvés'** + String get feedFilterApproved; + + /// No description provided for @feedFilterRejected. + /// + /// In fr, this message translates to: + /// **'Rejetés'** + String get feedFilterRejected; + + /// No description provided for @feedEmptyAll. + /// + /// In fr, this message translates to: + /// **'Aucun événement disponible'** + String get feedEmptyAll; + + /// No description provided for @feedEmptyPending. + /// + /// In fr, this message translates to: + /// **'Aucun événement en attente de validation'** + String get feedEmptyPending; + + /// No description provided for @feedEmptyApproved. + /// + /// In fr, this message translates to: + /// **'Aucun événement approuvé'** + String get feedEmptyApproved; + + /// No description provided for @feedEmptyRejected. + /// + /// In fr, this message translates to: + /// **'Aucun événement rejeté'** + String get feedEmptyRejected; + + /// No description provided for @feedEventManagement. + /// + /// In fr, this message translates to: + /// **'Gestion des événements'** + String get feedEventManagement; + + /// No description provided for @feedTitle. + /// + /// In fr, this message translates to: + /// **'Titre'** + String get feedTitle; + + /// No description provided for @feedLocation. + /// + /// In fr, this message translates to: + /// **'Lieu'** + String get feedLocation; + + /// No description provided for @feedSGDate. + /// + /// In fr, this message translates to: + /// **'Date du SG'** + String get feedSGDate; + + /// No description provided for @feedSGExternalLink. + /// + /// In fr, this message translates to: + /// **'Lien externe du SG'** + String get feedSGExternalLink; + + /// No description provided for @feedCreateEvent. + /// + /// In fr, this message translates to: + /// **'Créer l\'événement'** + String get feedCreateEvent; + + /// No description provided for @feedNotification. + /// + /// In fr, this message translates to: + /// **'Envoyer une notification'** + String get feedNotification; + + /// No description provided for @feedPleaseSelectAnAssociation. + /// + /// In fr, this message translates to: + /// **'Veuillez sélectionner une association'** + String get feedPleaseSelectAnAssociation; + + /// No description provided for @feedReject. + /// + /// In fr, this message translates to: + /// **'Rejeter'** + String get feedReject; + + /// No description provided for @feedApprove. + /// + /// In fr, this message translates to: + /// **'Approuver'** + String get feedApprove; + + /// No description provided for @feedEnded. + /// + /// In fr, this message translates to: + /// **'Terminé'** + String get feedEnded; + + /// No description provided for @feedOngoing. + /// + /// In fr, this message translates to: + /// **'En cours'** + String get feedOngoing; + + /// No description provided for @feedFilter. + /// + /// In fr, this message translates to: + /// **'Filtrer'** + String get feedFilter; + + /// No description provided for @feedAssociation. + /// + /// In fr, this message translates to: + /// **'Association'** + String get feedAssociation; + + /// Association event + /// + /// In fr, this message translates to: + /// **'Event de {name}'** + String feedAssociationEvent(String name); + + /// No description provided for @feedEditEvent. + /// + /// In fr, this message translates to: + /// **'Modifier l\'événement'** + String get feedEditEvent; + + /// No description provided for @feedManageAssociationEvents. + /// + /// In fr, this message translates to: + /// **'Gérer les événements de l\'association'** + String get feedManageAssociationEvents; + + /// No description provided for @feedNews. + /// + /// In fr, this message translates to: + /// **'Calendrier'** + String get feedNews; + + /// No description provided for @feedNewsType. + /// + /// In fr, this message translates to: + /// **'Type d\'actualité'** + String get feedNewsType; + + /// No description provided for @feedNoAssociationEvents. + /// + /// In fr, this message translates to: + /// **'Aucun événement d\'association'** + String get feedNoAssociationEvents; + + /// No description provided for @feedApply. + /// + /// In fr, this message translates to: + /// **'Appliquer'** + String get feedApply; + + /// No description provided for @feedAdmin. + /// + /// In fr, this message translates to: + /// **'Administration'** + String get feedAdmin; + + /// No description provided for @feedCreateAnEvent. + /// + /// In fr, this message translates to: + /// **'Créer un événement'** + String get feedCreateAnEvent; + + /// No description provided for @feedManageRequests. + /// + /// In fr, this message translates to: + /// **'Demandes de publication'** + String get feedManageRequests; + + /// No description provided for @feedNoNewsAvailable. + /// + /// In fr, this message translates to: + /// **'Aucune actualité disponible'** + String get feedNoNewsAvailable; + + /// No description provided for @feedRefresh. + /// + /// In fr, this message translates to: + /// **'Actualiser'** + String get feedRefresh; + + /// No description provided for @feedPleaseProvideASGExternalLink. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer un lien externe pour le SG'** + String get feedPleaseProvideASGExternalLink; + + /// No description provided for @feedPleaseProvideASGDate. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer une date de SG'** + String get feedPleaseProvideASGDate; + + /// Placeholder pour le temps restant avant le shotgun + /// + /// In fr, this message translates to: + /// **'Shotgun {time}'** + String feedShotgunIn(String time); + + /// Temps restant avant le vote + /// + /// In fr, this message translates to: + /// **'Vote {time}'** + String feedVoteIn(String time); + + /// No description provided for @feedCantOpenLink. + /// + /// In fr, this message translates to: + /// **'Impossible d\'ouvrir le lien'** + String get feedCantOpenLink; + + /// No description provided for @feedGetReady. + /// + /// In fr, this message translates to: + /// **'Prépare-toi !'** + String get feedGetReady; + + /// No description provided for @eventActionCampaign. + /// + /// In fr, this message translates to: + /// **'Tu peux voter'** + String get eventActionCampaign; + + /// No description provided for @eventActionEvent. + /// + /// In fr, this message translates to: + /// **'Tu es invité'** + String get eventActionEvent; + + /// No description provided for @eventActionCampaignSubtitle. + /// + /// In fr, this message translates to: + /// **'Votez maintenant'** + String get eventActionCampaignSubtitle; + + /// No description provided for @eventActionEventSubtitle. + /// + /// In fr, this message translates to: + /// **'Répondez à l\'invitation'** + String get eventActionEventSubtitle; + + /// No description provided for @eventActionCampaignButton. + /// + /// In fr, this message translates to: + /// **'Voter'** + String get eventActionCampaignButton; + + /// No description provided for @eventActionEventButton. + /// + /// In fr, this message translates to: + /// **'Réserver'** + String get eventActionEventButton; + + /// No description provided for @eventActionCampaignValidated. + /// + /// In fr, this message translates to: + /// **'J\'ai voté !'** + String get eventActionCampaignValidated; + + /// No description provided for @eventActionEventValidated. + /// + /// In fr, this message translates to: + /// **'Je viens !'** + String get eventActionEventValidated; + + /// No description provided for @adminAccountTypes. + /// + /// In fr, this message translates to: + /// **'Types de compte'** + String get adminAccountTypes; + + /// No description provided for @adminAdd. + /// + /// In fr, this message translates to: + /// **'Ajouter'** + String get adminAdd; + + /// No description provided for @adminAddGroup. + /// + /// In fr, this message translates to: + /// **'Ajouter un groupe'** + String get adminAddGroup; + + /// No description provided for @adminAddMember. + /// + /// In fr, this message translates to: + /// **'Ajouter un membre'** + String get adminAddMember; + + /// No description provided for @adminAddedGroup. + /// + /// In fr, this message translates to: + /// **'Groupe créé'** + String get adminAddedGroup; + + /// No description provided for @adminAddedLoaner. + /// + /// In fr, this message translates to: + /// **'Préteur ajouté'** + String get adminAddedLoaner; + + /// No description provided for @adminAddedMember. + /// + /// In fr, this message translates to: + /// **'Membre ajouté'** + String get adminAddedMember; + + /// No description provided for @adminAddingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de l\'ajout'** + String get adminAddingError; + + /// No description provided for @adminAddingMember. + /// + /// In fr, this message translates to: + /// **'Ajout d\'un membre'** + String get adminAddingMember; + + /// No description provided for @adminAddLoaningGroup. + /// + /// In fr, this message translates to: + /// **'Ajouter un groupe de prêt'** + String get adminAddLoaningGroup; + + /// No description provided for @adminAddSchool. + /// + /// In fr, this message translates to: + /// **'Ajouter une école'** + String get adminAddSchool; + + /// No description provided for @adminAddStructure. + /// + /// In fr, this message translates to: + /// **'Ajouter une structure'** + String get adminAddStructure; + + /// No description provided for @adminAddedSchool. + /// + /// In fr, this message translates to: + /// **'École créée'** + String get adminAddedSchool; + + /// No description provided for @adminAddedStructure. + /// + /// In fr, this message translates to: + /// **'Structure ajoutée'** + String get adminAddedStructure; + + /// No description provided for @adminEditedStructure. + /// + /// In fr, this message translates to: + /// **'Structure modifiée'** + String get adminEditedStructure; + + /// No description provided for @adminAdministration. + /// + /// In fr, this message translates to: + /// **'Administration'** + String get adminAdministration; + + /// No description provided for @adminAssociationMembership. + /// + /// In fr, this message translates to: + /// **'Adhésion'** + String get adminAssociationMembership; + + /// No description provided for @adminAssociationMembershipName. + /// + /// In fr, this message translates to: + /// **'Nom de l\'adhésion'** + String get adminAssociationMembershipName; + + /// No description provided for @adminAssociationsMemberships. + /// + /// In fr, this message translates to: + /// **'Adhésions'** + String get adminAssociationsMemberships; + + /// Displays the bank account holder's name + /// + /// In fr, this message translates to: + /// **'Titulaire du compte bancaire : {bankAccountHolder}'** + String adminBankAccountHolder(String bankAccountHolder); + + /// No description provided for @adminBankAccountHolderModified. + /// + /// In fr, this message translates to: + /// **'Titulaire du compte bancaire modifié'** + String get adminBankAccountHolderModified; + + /// No description provided for @adminBankDetails. + /// + /// In fr, this message translates to: + /// **'Coordonnées bancaires'** + String get adminBankDetails; + + /// No description provided for @adminBic. + /// + /// In fr, this message translates to: + /// **'BIC'** + String get adminBic; + + /// No description provided for @adminBicError. + /// + /// In fr, this message translates to: + /// **'Le BIC doit faire 11 caractères'** + String get adminBicError; + + /// No description provided for @adminCity. + /// + /// In fr, this message translates to: + /// **'Ville'** + String get adminCity; + + /// No description provided for @adminClearFilters. + /// + /// In fr, this message translates to: + /// **'Effacer les filtres'** + String get adminClearFilters; + + /// No description provided for @adminCountry. + /// + /// In fr, this message translates to: + /// **'Pays'** + String get adminCountry; + + /// No description provided for @adminCreateAssociationMembership. + /// + /// In fr, this message translates to: + /// **'Créer une adhésion'** + String get adminCreateAssociationMembership; + + /// No description provided for @adminCreatedAssociationMembership. + /// + /// In fr, this message translates to: + /// **'Adhésion créée'** + String get adminCreatedAssociationMembership; + + /// No description provided for @adminCreationError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la création'** + String get adminCreationError; + + /// No description provided for @adminDateError. + /// + /// In fr, this message translates to: + /// **'La date de début doit être avant la date de fin'** + String get adminDateError; + + /// No description provided for @adminDefineAsBankAccountHolder. + /// + /// In fr, this message translates to: + /// **'Définir comme titulaire du compte bancaire'** + String get adminDefineAsBankAccountHolder; + + /// No description provided for @adminDelete. + /// + /// In fr, this message translates to: + /// **'Supprimer'** + String get adminDelete; + + /// No description provided for @adminDeleteAssociationMember. + /// + /// In fr, this message translates to: + /// **'Supprimer le membre ?'** + String get adminDeleteAssociationMember; + + /// No description provided for @adminDeleteAssociationMemberConfirmation. + /// + /// In fr, this message translates to: + /// **'Êtes-vous sûr de vouloir supprimer ce membre ?'** + String get adminDeleteAssociationMemberConfirmation; + + /// No description provided for @adminDeleteAssociationMembership. + /// + /// In fr, this message translates to: + /// **'Supprimer l\'adhésion ?'** + String get adminDeleteAssociationMembership; + + /// No description provided for @adminDeletedAssociationMembership. + /// + /// In fr, this message translates to: + /// **'Adhésion supprimée'** + String get adminDeletedAssociationMembership; + + /// No description provided for @adminDeleteGroup. + /// + /// In fr, this message translates to: + /// **'Supprimer le groupe'** + String get adminDeleteGroup; + + /// No description provided for @adminDeletedGroup. + /// + /// In fr, this message translates to: + /// **'Groupe supprimé'** + String get adminDeletedGroup; + + /// No description provided for @adminDeleteSchool. + /// + /// In fr, this message translates to: + /// **'Supprimer l\'école ?'** + String get adminDeleteSchool; + + /// No description provided for @adminDeletedSchool. + /// + /// In fr, this message translates to: + /// **'École supprimée'** + String get adminDeletedSchool; + + /// No description provided for @adminDeleting. + /// + /// In fr, this message translates to: + /// **'Suppression'** + String get adminDeleting; + + /// No description provided for @adminDeletingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la suppression'** + String get adminDeletingError; + + /// No description provided for @adminDescription. + /// + /// In fr, this message translates to: + /// **'Description'** + String get adminDescription; + + /// No description provided for @adminEdit. + /// + /// In fr, this message translates to: + /// **'Modifier'** + String get adminEdit; + + /// No description provided for @adminEditStructure. + /// + /// In fr, this message translates to: + /// **'Modifier la structure'** + String get adminEditStructure; + + /// No description provided for @adminEditMembership. + /// + /// In fr, this message translates to: + /// **'Modifier l\'adhésion'** + String get adminEditMembership; + + /// No description provided for @adminEmptyDate. + /// + /// In fr, this message translates to: + /// **'Date vide'** + String get adminEmptyDate; + + /// No description provided for @adminEmptyFieldError. + /// + /// In fr, this message translates to: + /// **'Le nom ne peut pas être vide'** + String get adminEmptyFieldError; + + /// No description provided for @adminEmailFailed. + /// + /// In fr, this message translates to: + /// **'Impossible d\'envoyer un mail aux adresses suivantes'** + String get adminEmailFailed; + + /// No description provided for @adminEmailRegex. + /// + /// In fr, this message translates to: + /// **'Email Regex'** + String get adminEmailRegex; + + /// No description provided for @adminEmptyUser. + /// + /// In fr, this message translates to: + /// **'Utilisateur vide'** + String get adminEmptyUser; + + /// No description provided for @adminEndDate. + /// + /// In fr, this message translates to: + /// **'Date de fin'** + String get adminEndDate; + + /// No description provided for @adminEndDateMaximal. + /// + /// In fr, this message translates to: + /// **'Date de fin maximale'** + String get adminEndDateMaximal; + + /// No description provided for @adminEndDateMinimal. + /// + /// In fr, this message translates to: + /// **'Date de fin minimale'** + String get adminEndDateMinimal; + + /// No description provided for @adminError. + /// + /// In fr, this message translates to: + /// **'Erreur'** + String get adminError; + + /// No description provided for @adminFilters. + /// + /// In fr, this message translates to: + /// **'Filtres'** + String get adminFilters; + + /// No description provided for @adminGroup. + /// + /// In fr, this message translates to: + /// **'Groupe'** + String get adminGroup; + + /// No description provided for @adminGroups. + /// + /// In fr, this message translates to: + /// **'Groupes'** + String get adminGroups; + + /// No description provided for @adminIban. + /// + /// In fr, this message translates to: + /// **'IBAN'** + String get adminIban; + + /// No description provided for @adminIbanError. + /// + /// In fr, this message translates to: + /// **'L\'IBAN doit faire 27 caractères'** + String get adminIbanError; + + /// No description provided for @adminLoaningGroup. + /// + /// In fr, this message translates to: + /// **'Groupe de prêt'** + String get adminLoaningGroup; + + /// No description provided for @adminLooking. + /// + /// In fr, this message translates to: + /// **'Recherche'** + String get adminLooking; + + /// No description provided for @adminManager. + /// + /// In fr, this message translates to: + /// **'Administrateur de la structure'** + String get adminManager; + + /// No description provided for @adminMaximum. + /// + /// In fr, this message translates to: + /// **'Maximum'** + String get adminMaximum; + + /// No description provided for @adminMembers. + /// + /// In fr, this message translates to: + /// **'Membres'** + String get adminMembers; + + /// No description provided for @adminMembershipAddingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de l\'ajout (surement dû à une superposition de dates)'** + String get adminMembershipAddingError; + + /// No description provided for @adminMemberships. + /// + /// In fr, this message translates to: + /// **'Adhésions'** + String get adminMemberships; + + /// No description provided for @adminMembershipUpdatingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la modification (surement dû à une superposition de dates)'** + String get adminMembershipUpdatingError; + + /// No description provided for @adminMinimum. + /// + /// In fr, this message translates to: + /// **'Minimum'** + String get adminMinimum; + + /// No description provided for @adminModifyModuleVisibility. + /// + /// In fr, this message translates to: + /// **'Visibilité des modules'** + String get adminModifyModuleVisibility; + + /// No description provided for @adminName. + /// + /// In fr, this message translates to: + /// **'Nom'** + String get adminName; + + /// No description provided for @adminNoGroup. + /// + /// In fr, this message translates to: + /// **'Aucun groupe'** + String get adminNoGroup; + + /// No description provided for @adminNoManager. + /// + /// In fr, this message translates to: + /// **'Aucun manager n\'est sélectionné'** + String get adminNoManager; + + /// No description provided for @adminNoMember. + /// + /// In fr, this message translates to: + /// **'Aucun membre'** + String get adminNoMember; + + /// No description provided for @adminNoMoreLoaner. + /// + /// In fr, this message translates to: + /// **'Aucun prêteur n\'est disponible'** + String get adminNoMoreLoaner; + + /// No description provided for @adminNoSchool. + /// + /// In fr, this message translates to: + /// **'Sans école'** + String get adminNoSchool; + + /// No description provided for @adminRemoveGroupMember. + /// + /// In fr, this message translates to: + /// **'Supprimer le membre du groupe ?'** + String get adminRemoveGroupMember; + + /// No description provided for @adminResearch. + /// + /// In fr, this message translates to: + /// **'Recherche'** + String get adminResearch; + + /// No description provided for @adminSchools. + /// + /// In fr, this message translates to: + /// **'Écoles'** + String get adminSchools; + + /// No description provided for @adminShortId. + /// + /// In fr, this message translates to: + /// **'Short ID (3 lettres)'** + String get adminShortId; + + /// No description provided for @adminShortIdError. + /// + /// In fr, this message translates to: + /// **'Le short ID doit faire 3 caractères'** + String get adminShortIdError; + + /// No description provided for @adminSiegeAddress. + /// + /// In fr, this message translates to: + /// **'Adresse du siège'** + String get adminSiegeAddress; + + /// No description provided for @adminSiret. + /// + /// In fr, this message translates to: + /// **'SIRET'** + String get adminSiret; + + /// No description provided for @adminSiretError. + /// + /// In fr, this message translates to: + /// **'SIRET must be 14 digits'** + String get adminSiretError; + + /// No description provided for @adminStreet. + /// + /// In fr, this message translates to: + /// **'Numéro et rue'** + String get adminStreet; + + /// No description provided for @adminStructures. + /// + /// In fr, this message translates to: + /// **'Structures'** + String get adminStructures; + + /// No description provided for @adminStartDate. + /// + /// In fr, this message translates to: + /// **'Date de début'** + String get adminStartDate; + + /// No description provided for @adminStartDateMaximal. + /// + /// In fr, this message translates to: + /// **'Date de début maximale'** + String get adminStartDateMaximal; + + /// No description provided for @adminStartDateMinimal. + /// + /// In fr, this message translates to: + /// **'Date de début minimale'** + String get adminStartDateMinimal; + + /// No description provided for @adminUndefinedBankAccountHolder. + /// + /// In fr, this message translates to: + /// **'Titulaire du compte bancaire non défini'** + String get adminUndefinedBankAccountHolder; + + /// No description provided for @adminUpdatedAssociationMembership. + /// + /// In fr, this message translates to: + /// **'Adhésion modifiée'** + String get adminUpdatedAssociationMembership; + + /// No description provided for @adminUpdatedGroup. + /// + /// In fr, this message translates to: + /// **'Groupe modifié'** + String get adminUpdatedGroup; + + /// No description provided for @adminUpdatedMembership. + /// + /// In fr, this message translates to: + /// **'Adhésion modifiée'** + String get adminUpdatedMembership; + + /// No description provided for @adminUpdatingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la modification'** + String get adminUpdatingError; + + /// No description provided for @adminUser. + /// + /// In fr, this message translates to: + /// **'Utilisateur'** + String get adminUser; + + /// No description provided for @adminValidateFilters. + /// + /// In fr, this message translates to: + /// **'Valider les filtres'** + String get adminValidateFilters; + + /// No description provided for @adminVisibilities. + /// + /// In fr, this message translates to: + /// **'Visibilités'** + String get adminVisibilities; + + /// No description provided for @adminZipcode. + /// + /// In fr, this message translates to: + /// **'Code postal'** + String get adminZipcode; + + /// No description provided for @adminGroupNotification. + /// + /// In fr, this message translates to: + /// **'Notification de groupe'** + String get adminGroupNotification; + + /// Notifie les membres du groupe sélectionné + /// + /// In fr, this message translates to: + /// **'Notifier le groupe {groupName}'** + String adminNotifyGroup(String groupName); + + /// No description provided for @adminTitle. + /// + /// In fr, this message translates to: + /// **'Titre'** + String get adminTitle; + + /// No description provided for @adminContent. + /// + /// In fr, this message translates to: + /// **'Contenu'** + String get adminContent; + + /// No description provided for @adminSend. + /// + /// In fr, this message translates to: + /// **'Envoyer'** + String get adminSend; + + /// No description provided for @adminNotificationSent. + /// + /// In fr, this message translates to: + /// **'Notification envoyée'** + String get adminNotificationSent; + + /// No description provided for @adminFailedToSendNotification. + /// + /// In fr, this message translates to: + /// **'Échec de l\'envoi de la notification'** + String get adminFailedToSendNotification; + + /// No description provided for @adminGroupsManagement. + /// + /// In fr, this message translates to: + /// **'Gestion des groupes'** + String get adminGroupsManagement; + + /// No description provided for @adminEditGroup. + /// + /// In fr, this message translates to: + /// **'Modifier le groupe'** + String get adminEditGroup; + + /// No description provided for @adminManageMembers. + /// + /// In fr, this message translates to: + /// **'Gérer les membres'** + String get adminManageMembers; + + /// No description provided for @adminDeleteGroupConfirmation. + /// + /// In fr, this message translates to: + /// **'Êtes-vous sûr de vouloir supprimer ce groupe ?'** + String get adminDeleteGroupConfirmation; + + /// No description provided for @adminFailedToDeleteGroup. + /// + /// In fr, this message translates to: + /// **'Échec de la suppression du groupe'** + String get adminFailedToDeleteGroup; + + /// No description provided for @adminUsersAndGroups. + /// + /// In fr, this message translates to: + /// **'Utilisateurs et groupes'** + String get adminUsersAndGroups; + + /// No description provided for @adminUsersManagement. + /// + /// In fr, this message translates to: + /// **'Gestion des utilisateurs'** + String get adminUsersManagement; + + /// No description provided for @adminUsersManagementDescription. + /// + /// In fr, this message translates to: + /// **'Gérer les utilisateurs de l\'application'** + String get adminUsersManagementDescription; + + /// No description provided for @adminManageUserGroups. + /// + /// In fr, this message translates to: + /// **'Gérer les groupes d\'utilisateurs'** + String get adminManageUserGroups; + + /// No description provided for @adminSendNotificationToGroup. + /// + /// In fr, this message translates to: + /// **'Envoyer une notification à un groupe'** + String get adminSendNotificationToGroup; + + /// No description provided for @adminPaiementModule. + /// + /// In fr, this message translates to: + /// **'Module de paiement'** + String get adminPaiementModule; + + /// No description provided for @adminPaiement. + /// + /// In fr, this message translates to: + /// **'Paiement'** + String get adminPaiement; + + /// No description provided for @adminManagePaiementStructures. + /// + /// In fr, this message translates to: + /// **'Gérer les structures du module de paiement'** + String get adminManagePaiementStructures; + + /// No description provided for @adminManageUsersAssociationMemberships. + /// + /// In fr, this message translates to: + /// **'Gérer les adhésions des utilisateurs'** + String get adminManageUsersAssociationMemberships; + + /// No description provided for @adminAssociationMembershipsManagement. + /// + /// In fr, this message translates to: + /// **'Gestion des adhésions'** + String get adminAssociationMembershipsManagement; + + /// No description provided for @adminChooseGroupManager. + /// + /// In fr, this message translates to: + /// **'Groupe gestionnaire de l\'adhésion'** + String get adminChooseGroupManager; + + /// No description provided for @adminSelectManager. + /// + /// In fr, this message translates to: + /// **'Sélectionner un gestionnaire'** + String get adminSelectManager; + + /// No description provided for @adminImportList. + /// + /// In fr, this message translates to: + /// **'Importer une liste'** + String get adminImportList; + + /// No description provided for @adminImportUsersDescription. + /// + /// In fr, this message translates to: + /// **'Importer des utilisateurs depuis un fichier CSV. Le fichier CSV doit contenir une adresse email par ligne.'** + String get adminImportUsersDescription; + + /// No description provided for @adminFailedToInviteUsers. + /// + /// In fr, this message translates to: + /// **'Échec de l\'invitation des utilisateurs'** + String get adminFailedToInviteUsers; + + /// No description provided for @adminDeleteUsers. + /// + /// In fr, this message translates to: + /// **'Supprimer des utilisateurs'** + String get adminDeleteUsers; + + /// No description provided for @adminAdmin. + /// + /// In fr, this message translates to: + /// **'Admin'** + String get adminAdmin; + + /// No description provided for @adminAssociations. + /// + /// In fr, this message translates to: + /// **'Associations'** + String get adminAssociations; + + /// No description provided for @adminManageAssociations. + /// + /// In fr, this message translates to: + /// **'Gérer les associations'** + String get adminManageAssociations; + + /// No description provided for @adminAddAssociation. + /// + /// In fr, this message translates to: + /// **'Ajouter une association'** + String get adminAddAssociation; + + /// No description provided for @adminAssociationName. + /// + /// In fr, this message translates to: + /// **'Nom de l\'association'** + String get adminAssociationName; + + /// No description provided for @adminSelectGroupAssociationManager. + /// + /// In fr, this message translates to: + /// **'Séléctionner roupe gestionnaire de l\'association'** + String get adminSelectGroupAssociationManager; + + /// Modifier les informations de l'association + /// + /// In fr, this message translates to: + /// **'Modifier l\'association : {associationName}'** + String adminEditAssociation(String associationName); + + /// Groupe qui gère l'association + /// + /// In fr, this message translates to: + /// **'Groupe gestionnaire : {groupName}'** + String adminManagerGroup(String groupName); + + /// No description provided for @adminAssociationCreated. + /// + /// In fr, this message translates to: + /// **'Association créée'** + String get adminAssociationCreated; + + /// No description provided for @adminAssociationUpdated. + /// + /// In fr, this message translates to: + /// **'Association mise à jour'** + String get adminAssociationUpdated; + + /// No description provided for @adminAssociationCreationError. + /// + /// In fr, this message translates to: + /// **'Échec de la création de l\'association'** + String get adminAssociationCreationError; + + /// No description provided for @adminAssociationUpdateError. + /// + /// In fr, this message translates to: + /// **'Échec de la mise à jour de l\'association'** + String get adminAssociationUpdateError; + + /// No description provided for @adminInvite. + /// + /// In fr, this message translates to: + /// **'Inviter'** + String get adminInvite; + + /// No description provided for @adminInvitedUsers. + /// + /// In fr, this message translates to: + /// **'Utilisateurs invités'** + String get adminInvitedUsers; + + /// No description provided for @adminInviteUsers. + /// + /// In fr, this message translates to: + /// **'Inviter des utilisateurs'** + String get adminInviteUsers; + + /// Text with the number of users in the CSV file + /// + /// In fr, this message translates to: + /// **'{count, plural, zero {Aucun utilisateur} one {{count} utilisateur} other {{count} utilisateurs}} dans le fichier CSV'** + String adminInviteUsersCounter(int count); + + /// No description provided for @adminUpdatedAssociationLogo. + /// + /// In fr, this message translates to: + /// **'Logo de l\'association mis à jour'** + String get adminUpdatedAssociationLogo; + + /// No description provided for @adminTooHeavyLogo. + /// + /// In fr, this message translates to: + /// **'Le logo de l\'association est trop lourd, il doit faire moins de 4 Mo'** + String get adminTooHeavyLogo; + + /// No description provided for @adminFailedToUpdateAssociationLogo. + /// + /// In fr, this message translates to: + /// **'Échec de la mise à jour du logo de l\'association'** + String get adminFailedToUpdateAssociationLogo; + + /// No description provided for @adminChooseGroup. + /// + /// In fr, this message translates to: + /// **'Choisir un groupe'** + String get adminChooseGroup; + + /// No description provided for @adminChooseAssociationManagerGroup. + /// + /// In fr, this message translates to: + /// **'Choisir un groupe gestionnaire pour l\'association'** + String get adminChooseAssociationManagerGroup; + + /// No description provided for @advertAdd. + /// + /// In fr, this message translates to: + /// **'Ajouter'** + String get advertAdd; + + /// No description provided for @advertAddedAdvert. + /// + /// In fr, this message translates to: + /// **'Annonce publiée'** + String get advertAddedAdvert; + + /// No description provided for @advertAddedAnnouncer. + /// + /// In fr, this message translates to: + /// **'Annonceur ajouté'** + String get advertAddedAnnouncer; + + /// No description provided for @advertAddingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de l\'ajout'** + String get advertAddingError; + + /// No description provided for @advertAdmin. + /// + /// In fr, this message translates to: + /// **'Admin'** + String get advertAdmin; + + /// No description provided for @advertAdvert. + /// + /// In fr, this message translates to: + /// **'Annonce'** + String get advertAdvert; + + /// No description provided for @advertChoosingAnnouncer. + /// + /// In fr, this message translates to: + /// **'Veuillez choisir un annonceur'** + String get advertChoosingAnnouncer; + + /// No description provided for @advertChoosingPoster. + /// + /// In fr, this message translates to: + /// **'Veuillez choisir une image'** + String get advertChoosingPoster; + + /// No description provided for @advertContent. + /// + /// In fr, this message translates to: + /// **'Contenu'** + String get advertContent; + + /// No description provided for @advertDeleteAdvert. + /// + /// In fr, this message translates to: + /// **'Supprimer l\'annonce'** + String get advertDeleteAdvert; + + /// No description provided for @advertDeleteAnnouncer. + /// + /// In fr, this message translates to: + /// **'Supprimer l\'annonceur ?'** + String get advertDeleteAnnouncer; + + /// No description provided for @advertDeleting. + /// + /// In fr, this message translates to: + /// **'Suppression'** + String get advertDeleting; + + /// No description provided for @advertEdit. + /// + /// In fr, this message translates to: + /// **'Modifier'** + String get advertEdit; + + /// No description provided for @advertEditedAdvert. + /// + /// In fr, this message translates to: + /// **'Annonce modifiée'** + String get advertEditedAdvert; + + /// No description provided for @advertEditingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la modification'** + String get advertEditingError; + + /// No description provided for @advertGroupAdvert. + /// + /// In fr, this message translates to: + /// **'Groupe'** + String get advertGroupAdvert; + + /// No description provided for @advertIncorrectOrMissingFields. + /// + /// In fr, this message translates to: + /// **'Champs incorrects ou manquants'** + String get advertIncorrectOrMissingFields; + + /// No description provided for @advertInvalidNumber. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer un nombre'** + String get advertInvalidNumber; + + /// No description provided for @advertManagement. + /// + /// In fr, this message translates to: + /// **'Gestion'** + String get advertManagement; + + /// No description provided for @advertModifyAnnouncingGroup. + /// + /// In fr, this message translates to: + /// **'Modifier un groupe d\'annonce'** + String get advertModifyAnnouncingGroup; + + /// No description provided for @advertNoMoreAnnouncer. + /// + /// In fr, this message translates to: + /// **'Aucun annonceur n\'est disponible'** + String get advertNoMoreAnnouncer; + + /// No description provided for @advertNoValue. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer une valeur'** + String get advertNoValue; + + /// No description provided for @advertPositiveNumber. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer un nombre positif'** + String get advertPositiveNumber; + + /// No description provided for @advertPublishToFeed. + /// + /// In fr, this message translates to: + /// **'Publier dans le feed'** + String get advertPublishToFeed; + + /// No description provided for @advertNotification. + /// + /// In fr, this message translates to: + /// **'Envoyer une notification'** + String get advertNotification; + + /// No description provided for @advertRemovedAnnouncer. + /// + /// In fr, this message translates to: + /// **'Annonceur supprimé'** + String get advertRemovedAnnouncer; + + /// No description provided for @advertRemovingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la suppression'** + String get advertRemovingError; + + /// No description provided for @advertTags. + /// + /// In fr, this message translates to: + /// **'Tags'** + String get advertTags; + + /// No description provided for @advertTitle. + /// + /// In fr, this message translates to: + /// **'Titre'** + String get advertTitle; + + /// No description provided for @advertMonthJan. + /// + /// In fr, this message translates to: + /// **'Janv'** + String get advertMonthJan; + + /// No description provided for @advertMonthFeb. + /// + /// In fr, this message translates to: + /// **'Févr.'** + String get advertMonthFeb; + + /// No description provided for @advertMonthMar. + /// + /// In fr, this message translates to: + /// **'Mars'** + String get advertMonthMar; + + /// No description provided for @advertMonthApr. + /// + /// In fr, this message translates to: + /// **'Avr.'** + String get advertMonthApr; + + /// No description provided for @advertMonthMay. + /// + /// In fr, this message translates to: + /// **'Mai'** + String get advertMonthMay; + + /// No description provided for @advertMonthJun. + /// + /// In fr, this message translates to: + /// **'Juin'** + String get advertMonthJun; + + /// No description provided for @advertMonthJul. + /// + /// In fr, this message translates to: + /// **'Juill.'** + String get advertMonthJul; + + /// No description provided for @advertMonthAug. + /// + /// In fr, this message translates to: + /// **'Août'** + String get advertMonthAug; + + /// No description provided for @advertMonthSep. + /// + /// In fr, this message translates to: + /// **'Sept.'** + String get advertMonthSep; + + /// No description provided for @advertMonthOct. + /// + /// In fr, this message translates to: + /// **'Oct.'** + String get advertMonthOct; + + /// No description provided for @advertMonthNov. + /// + /// In fr, this message translates to: + /// **'Nov.'** + String get advertMonthNov; + + /// No description provided for @advertMonthDec. + /// + /// In fr, this message translates to: + /// **'Déc.'** + String get advertMonthDec; + + /// No description provided for @amapAccounts. + /// + /// In fr, this message translates to: + /// **'Comptes'** + String get amapAccounts; + + /// No description provided for @amapAdd. + /// + /// In fr, this message translates to: + /// **'Ajouter'** + String get amapAdd; + + /// No description provided for @amapAddDelivery. + /// + /// In fr, this message translates to: + /// **'Ajouter une livraison'** + String get amapAddDelivery; + + /// No description provided for @amapAddedCommand. + /// + /// In fr, this message translates to: + /// **'Commande ajoutée'** + String get amapAddedCommand; + + /// No description provided for @amapAddedOrder. + /// + /// In fr, this message translates to: + /// **'Commande ajoutée'** + String get amapAddedOrder; + + /// No description provided for @amapAddedProduct. + /// + /// In fr, this message translates to: + /// **'Produit ajouté'** + String get amapAddedProduct; + + /// No description provided for @amapAddedUser. + /// + /// In fr, this message translates to: + /// **'Utilisateur ajouté'** + String get amapAddedUser; + + /// No description provided for @amapAddProduct. + /// + /// In fr, this message translates to: + /// **'Ajouter un produit'** + String get amapAddProduct; + + /// No description provided for @amapAddUser. + /// + /// In fr, this message translates to: + /// **'Ajouter un utilisateur'** + String get amapAddUser; + + /// No description provided for @amapAddingACommand. + /// + /// In fr, this message translates to: + /// **'Ajouter une commande'** + String get amapAddingACommand; + + /// No description provided for @amapAddingCommand. + /// + /// In fr, this message translates to: + /// **'Ajouter la commande'** + String get amapAddingCommand; + + /// No description provided for @amapAddingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de l\'ajout'** + String get amapAddingError; + + /// No description provided for @amapAddingProduct. + /// + /// In fr, this message translates to: + /// **'Ajouter un produit'** + String get amapAddingProduct; + + /// No description provided for @amapAddOrder. + /// + /// In fr, this message translates to: + /// **'Ajouter une commande'** + String get amapAddOrder; + + /// No description provided for @amapAdmin. + /// + /// In fr, this message translates to: + /// **'Admin'** + String get amapAdmin; + + /// No description provided for @amapAlreadyExistCommand. + /// + /// In fr, this message translates to: + /// **'Il existe déjà une commande à cette date'** + String get amapAlreadyExistCommand; + + /// No description provided for @amapAmap. + /// + /// In fr, this message translates to: + /// **'Amap'** + String get amapAmap; + + /// No description provided for @amapAmount. + /// + /// In fr, this message translates to: + /// **'Solde'** + String get amapAmount; + + /// No description provided for @amapArchive. + /// + /// In fr, this message translates to: + /// **'Archiver'** + String get amapArchive; + + /// No description provided for @amapArchiveDelivery. + /// + /// In fr, this message translates to: + /// **'Archiver'** + String get amapArchiveDelivery; + + /// No description provided for @amapArchivingDelivery. + /// + /// In fr, this message translates to: + /// **'Archivage de la livraison'** + String get amapArchivingDelivery; + + /// No description provided for @amapCategory. + /// + /// In fr, this message translates to: + /// **'Catégorie'** + String get amapCategory; + + /// No description provided for @amapCloseDelivery. + /// + /// In fr, this message translates to: + /// **'Verrouiller'** + String get amapCloseDelivery; + + /// No description provided for @amapCommandDate. + /// + /// In fr, this message translates to: + /// **'Date de la commande'** + String get amapCommandDate; + + /// No description provided for @amapCommandProducts. + /// + /// In fr, this message translates to: + /// **'Produits de la commande'** + String get amapCommandProducts; + + /// No description provided for @amapConfirm. + /// + /// In fr, this message translates to: + /// **'Confirmer'** + String get amapConfirm; + + /// No description provided for @amapContact. + /// + /// In fr, this message translates to: + /// **'Contacts associatifs '** + String get amapContact; + + /// No description provided for @amapCreateCategory. + /// + /// In fr, this message translates to: + /// **'Créer une catégorie'** + String get amapCreateCategory; + + /// No description provided for @amapDelete. + /// + /// In fr, this message translates to: + /// **'Supprimer'** + String get amapDelete; + + /// No description provided for @amapDeleteDelivery. + /// + /// In fr, this message translates to: + /// **'Supprimer la livraison ?'** + String get amapDeleteDelivery; + + /// No description provided for @amapDeleteDeliveryDescription. + /// + /// In fr, this message translates to: + /// **'Voulez-vous vraiment supprimer cette livraison ?'** + String get amapDeleteDeliveryDescription; + + /// No description provided for @amapDeletedDelivery. + /// + /// In fr, this message translates to: + /// **'Livraison supprimée'** + String get amapDeletedDelivery; + + /// No description provided for @amapDeletedOrder. + /// + /// In fr, this message translates to: + /// **'Commande supprimée'** + String get amapDeletedOrder; + + /// No description provided for @amapDeletedProduct. + /// + /// In fr, this message translates to: + /// **'Produit supprimé'** + String get amapDeletedProduct; + + /// No description provided for @amapDeleteProduct. + /// + /// In fr, this message translates to: + /// **'Supprimer le produit ?'** + String get amapDeleteProduct; + + /// No description provided for @amapDeleteProductDescription. + /// + /// In fr, this message translates to: + /// **'Voulez-vous vraiment supprimer ce produit ?'** + String get amapDeleteProductDescription; + + /// No description provided for @amapDeleting. + /// + /// In fr, this message translates to: + /// **'Suppression'** + String get amapDeleting; + + /// No description provided for @amapDeletingDelivery. + /// + /// In fr, this message translates to: + /// **'Supprimer la livraison ?'** + String get amapDeletingDelivery; + + /// No description provided for @amapDeletingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la suppression'** + String get amapDeletingError; + + /// No description provided for @amapDeletingOrder. + /// + /// In fr, this message translates to: + /// **'Supprimer la commande ?'** + String get amapDeletingOrder; + + /// No description provided for @amapDeletingProduct. + /// + /// In fr, this message translates to: + /// **'Supprimer le produit ?'** + String get amapDeletingProduct; + + /// No description provided for @amapDeliver. + /// + /// In fr, this message translates to: + /// **'Livraison teminée ?'** + String get amapDeliver; + + /// No description provided for @amapDeliveries. + /// + /// In fr, this message translates to: + /// **'Livraisons'** + String get amapDeliveries; + + /// No description provided for @amapDeliveringDelivery. + /// + /// In fr, this message translates to: + /// **'Toutes les commandes sont livrées ?'** + String get amapDeliveringDelivery; + + /// No description provided for @amapDelivery. + /// + /// In fr, this message translates to: + /// **'Livraison'** + String get amapDelivery; + + /// No description provided for @amapDeliveryArchived. + /// + /// In fr, this message translates to: + /// **'Livraison archivée'** + String get amapDeliveryArchived; + + /// No description provided for @amapDeliveryDate. + /// + /// In fr, this message translates to: + /// **'Date de livraison'** + String get amapDeliveryDate; + + /// No description provided for @amapDeliveryDelivered. + /// + /// In fr, this message translates to: + /// **'Livraison effectuée'** + String get amapDeliveryDelivered; + + /// No description provided for @amapDeliveryHistory. + /// + /// In fr, this message translates to: + /// **'Historique des livraisons'** + String get amapDeliveryHistory; + + /// No description provided for @amapDeliveryList. + /// + /// In fr, this message translates to: + /// **'Liste des livraisons'** + String get amapDeliveryList; + + /// No description provided for @amapDeliveryLocked. + /// + /// In fr, this message translates to: + /// **'Livraison verrouillée'** + String get amapDeliveryLocked; + + /// No description provided for @amapDeliveryOn. + /// + /// In fr, this message translates to: + /// **'Livraison le'** + String get amapDeliveryOn; + + /// No description provided for @amapDeliveryOpened. + /// + /// In fr, this message translates to: + /// **'Livraison ouverte'** + String get amapDeliveryOpened; + + /// No description provided for @amapDeliveryNotArchived. + /// + /// In fr, this message translates to: + /// **'Livraison non archivée'** + String get amapDeliveryNotArchived; + + /// No description provided for @amapDeliveryNotLocked. + /// + /// In fr, this message translates to: + /// **'Livraison non verrouillée'** + String get amapDeliveryNotLocked; + + /// No description provided for @amapDeliveryNotDelivered. + /// + /// In fr, this message translates to: + /// **'Livraison non effectuée'** + String get amapDeliveryNotDelivered; + + /// No description provided for @amapDeliveryNotOpened. + /// + /// In fr, this message translates to: + /// **'Livraison non ouverte'** + String get amapDeliveryNotOpened; + + /// No description provided for @amapEditDelivery. + /// + /// In fr, this message translates to: + /// **'Modifier la livraison'** + String get amapEditDelivery; + + /// No description provided for @amapEditedCommand. + /// + /// In fr, this message translates to: + /// **'Commande modifiée'** + String get amapEditedCommand; + + /// No description provided for @amapEditingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la modification'** + String get amapEditingError; + + /// No description provided for @amapEditProduct. + /// + /// In fr, this message translates to: + /// **'Modifier le produit'** + String get amapEditProduct; + + /// No description provided for @amapEndingDelivery. + /// + /// In fr, this message translates to: + /// **'Fin de la livraison'** + String get amapEndingDelivery; + + /// No description provided for @amapError. + /// + /// In fr, this message translates to: + /// **'Erreur'** + String get amapError; + + /// No description provided for @amapErrorLink. + /// + /// In fr, this message translates to: + /// **'Erreur lors de l\'ouverture du lien'** + String get amapErrorLink; + + /// No description provided for @amapErrorLoadingUser. + /// + /// In fr, this message translates to: + /// **'Erreur lors du chargement des utilisateurs'** + String get amapErrorLoadingUser; + + /// No description provided for @amapEvening. + /// + /// In fr, this message translates to: + /// **'Soir'** + String get amapEvening; + + /// No description provided for @amapExpectingNumber. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer un nombre'** + String get amapExpectingNumber; + + /// No description provided for @amapFillField. + /// + /// In fr, this message translates to: + /// **'Veuillez remplir ce champ'** + String get amapFillField; + + /// No description provided for @amapHandlingAccount. + /// + /// In fr, this message translates to: + /// **'Gérer les comptes'** + String get amapHandlingAccount; + + /// No description provided for @amapLoading. + /// + /// In fr, this message translates to: + /// **'Chargement...'** + String get amapLoading; + + /// No description provided for @amapLoadingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors du chargement'** + String get amapLoadingError; + + /// No description provided for @amapLock. + /// + /// In fr, this message translates to: + /// **'Verrouiller'** + String get amapLock; + + /// No description provided for @amapLocked. + /// + /// In fr, this message translates to: + /// **'Verrouillée'** + String get amapLocked; + + /// No description provided for @amapLockedDelivery. + /// + /// In fr, this message translates to: + /// **'Livraison verrouillée'** + String get amapLockedDelivery; + + /// No description provided for @amapLockedOrder. + /// + /// In fr, this message translates to: + /// **'Commande verrouillée'** + String get amapLockedOrder; + + /// No description provided for @amapLooking. + /// + /// In fr, this message translates to: + /// **'Rechercher'** + String get amapLooking; + + /// No description provided for @amapLockingDelivery. + /// + /// In fr, this message translates to: + /// **'Verrouiller la livraison ?'** + String get amapLockingDelivery; + + /// No description provided for @amapMidDay. + /// + /// In fr, this message translates to: + /// **'Midi'** + String get amapMidDay; + + /// No description provided for @amapMyOrders. + /// + /// In fr, this message translates to: + /// **'Mes commandes'** + String get amapMyOrders; + + /// No description provided for @amapName. + /// + /// In fr, this message translates to: + /// **'Nom'** + String get amapName; + + /// No description provided for @amapNextStep. + /// + /// In fr, this message translates to: + /// **'Étape suivante'** + String get amapNextStep; + + /// No description provided for @amapNoProduct. + /// + /// In fr, this message translates to: + /// **'Pas de produit'** + String get amapNoProduct; + + /// No description provided for @amapNoCurrentOrder. + /// + /// In fr, this message translates to: + /// **'Pas de commande en cours'** + String get amapNoCurrentOrder; + + /// No description provided for @amapNoMoney. + /// + /// In fr, this message translates to: + /// **'Pas assez d\'argent'** + String get amapNoMoney; + + /// No description provided for @amapNoOpennedDelivery. + /// + /// In fr, this message translates to: + /// **'Pas de livraison ouverte'** + String get amapNoOpennedDelivery; + + /// No description provided for @amapNoOrder. + /// + /// In fr, this message translates to: + /// **'Pas de commande'** + String get amapNoOrder; + + /// No description provided for @amapNoSelectedDelivery. + /// + /// In fr, this message translates to: + /// **'Pas de livraison sélectionnée'** + String get amapNoSelectedDelivery; + + /// No description provided for @amapNotEnoughMoney. + /// + /// In fr, this message translates to: + /// **'Pas assez d\'argent'** + String get amapNotEnoughMoney; + + /// No description provided for @amapNotPlannedDelivery. + /// + /// In fr, this message translates to: + /// **'Pas de livraison planifiée'** + String get amapNotPlannedDelivery; + + /// No description provided for @amapOneOrder. + /// + /// In fr, this message translates to: + /// **'commande'** + String get amapOneOrder; + + /// No description provided for @amapOpenDelivery. + /// + /// In fr, this message translates to: + /// **'Ouvrir'** + String get amapOpenDelivery; + + /// No description provided for @amapOpened. + /// + /// In fr, this message translates to: + /// **'Ouverte'** + String get amapOpened; + + /// No description provided for @amapOpenningDelivery. + /// + /// In fr, this message translates to: + /// **'Ouvrir la livraison ?'** + String get amapOpenningDelivery; + + /// No description provided for @amapOrder. + /// + /// In fr, this message translates to: + /// **'Commander'** + String get amapOrder; + + /// No description provided for @amapOrders. + /// + /// In fr, this message translates to: + /// **'Commandes'** + String get amapOrders; + + /// No description provided for @amapPickChooseCategory. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer une valeur ou choisir une catégorie existante'** + String get amapPickChooseCategory; + + /// No description provided for @amapPickDeliveryMoment. + /// + /// In fr, this message translates to: + /// **'Choisissez un moment de livraison'** + String get amapPickDeliveryMoment; + + /// No description provided for @amapPresentation. + /// + /// In fr, this message translates to: + /// **'Présentation'** + String get amapPresentation; + + /// No description provided for @amapPresentation1. + /// + /// In fr, this message translates to: + /// **'L\'AMAP (association pour le maintien d\'une agriculture paysanne) est un service proposé par l\'association Planet&Co de l\'ECL. Vous pouvez ainsi recevoir des produits (paniers de fruits et légumes, jus, confitures...) directement sur le campus !\n\nLes commandes doivent être passées avant le vendredi 21h et sont livrées sur le campus le mardi de 13h à 13h45 (ou de 18h15 à 18h30 si vous ne pouvez pas passer le midi) dans le hall du M16.\n\nVous ne pouvez commander que si votre solde le permet. Vous pouvez recharger votre solde via la collecte Lydia ou bien avec un chèque que vous pouvez nous transmettre lors des permanences.\n\nLien vers la collecte Lydia pour le rechargement : '** + String get amapPresentation1; + + /// No description provided for @amapPresentation2. + /// + /// In fr, this message translates to: + /// **'\n\nN\'hésitez pas à nous contacter en cas de problème !'** + String get amapPresentation2; + + /// No description provided for @amapPrice. + /// + /// In fr, this message translates to: + /// **'Prix'** + String get amapPrice; + + /// No description provided for @amapProduct. + /// + /// In fr, this message translates to: + /// **'produit'** + String get amapProduct; + + /// No description provided for @amapProducts. + /// + /// In fr, this message translates to: + /// **'Produits'** + String get amapProducts; + + /// No description provided for @amapProductInDelivery. + /// + /// In fr, this message translates to: + /// **'Produit dans une livraison non terminée'** + String get amapProductInDelivery; + + /// No description provided for @amapQuantity. + /// + /// In fr, this message translates to: + /// **'Quantité'** + String get amapQuantity; + + /// No description provided for @amapRequiredDate. + /// + /// In fr, this message translates to: + /// **'La date est requise'** + String get amapRequiredDate; + + /// No description provided for @amapSeeMore. + /// + /// In fr, this message translates to: + /// **'Voir plus'** + String get amapSeeMore; + + /// No description provided for @amapThe. + /// + /// In fr, this message translates to: + /// **'Le'** + String get amapThe; + + /// No description provided for @amapUnlock. + /// + /// In fr, this message translates to: + /// **'Dévérouiller'** + String get amapUnlock; + + /// No description provided for @amapUnlockedDelivery. + /// + /// In fr, this message translates to: + /// **'Livraison dévérouillée'** + String get amapUnlockedDelivery; + + /// No description provided for @amapUnlockingDelivery. + /// + /// In fr, this message translates to: + /// **'Dévérouiller la livraison ?'** + String get amapUnlockingDelivery; + + /// No description provided for @amapUpdate. + /// + /// In fr, this message translates to: + /// **'Modifier'** + String get amapUpdate; + + /// No description provided for @amapUpdatedAmount. + /// + /// In fr, this message translates to: + /// **'Solde modifié'** + String get amapUpdatedAmount; + + /// No description provided for @amapUpdatedOrder. + /// + /// In fr, this message translates to: + /// **'Commande modifiée'** + String get amapUpdatedOrder; + + /// No description provided for @amapUpdatedProduct. + /// + /// In fr, this message translates to: + /// **'Produit modifié'** + String get amapUpdatedProduct; + + /// No description provided for @amapUpdatingError. + /// + /// In fr, this message translates to: + /// **'Echec de la modification'** + String get amapUpdatingError; + + /// No description provided for @amapUsersNotFound. + /// + /// In fr, this message translates to: + /// **'Aucun utilisateur trouvé'** + String get amapUsersNotFound; + + /// No description provided for @amapWaiting. + /// + /// In fr, this message translates to: + /// **'En attente'** + String get amapWaiting; + + /// No description provided for @bookingAdd. + /// + /// In fr, this message translates to: + /// **'Ajouter'** + String get bookingAdd; + + /// No description provided for @bookingAddBookingPage. + /// + /// In fr, this message translates to: + /// **'Demande'** + String get bookingAddBookingPage; + + /// No description provided for @bookingAddRoom. + /// + /// In fr, this message translates to: + /// **'Ajouter une salle'** + String get bookingAddRoom; + + /// No description provided for @bookingAddBooking. + /// + /// In fr, this message translates to: + /// **'Ajouter une réservation'** + String get bookingAddBooking; + + /// No description provided for @bookingAddedBooking. + /// + /// In fr, this message translates to: + /// **'Demande ajoutée'** + String get bookingAddedBooking; + + /// No description provided for @bookingAddedRoom. + /// + /// In fr, this message translates to: + /// **'Salle ajoutée'** + String get bookingAddedRoom; + + /// No description provided for @bookingAddedManager. + /// + /// In fr, this message translates to: + /// **'Gestionnaire ajouté'** + String get bookingAddedManager; + + /// No description provided for @bookingAddingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de l\'ajout'** + String get bookingAddingError; + + /// No description provided for @bookingAddManager. + /// + /// In fr, this message translates to: + /// **'Ajouter un gestionnaire'** + String get bookingAddManager; + + /// No description provided for @bookingAdminPage. + /// + /// In fr, this message translates to: + /// **'Administrateur'** + String get bookingAdminPage; + + /// No description provided for @bookingAllDay. + /// + /// In fr, this message translates to: + /// **'Toute la journée'** + String get bookingAllDay; + + /// No description provided for @bookingBookedFor. + /// + /// In fr, this message translates to: + /// **'Réservé pour'** + String get bookingBookedFor; + + /// No description provided for @bookingBooking. + /// + /// In fr, this message translates to: + /// **'Réservation'** + String get bookingBooking; + + /// No description provided for @bookingBookingCreated. + /// + /// In fr, this message translates to: + /// **'Réservation créée'** + String get bookingBookingCreated; + + /// No description provided for @bookingBookingDemand. + /// + /// In fr, this message translates to: + /// **'Demande de réservation'** + String get bookingBookingDemand; + + /// No description provided for @bookingBookingNote. + /// + /// In fr, this message translates to: + /// **'Note de la réservation'** + String get bookingBookingNote; + + /// No description provided for @bookingBookingPage. + /// + /// In fr, this message translates to: + /// **'Réservation'** + String get bookingBookingPage; + + /// No description provided for @bookingBookingReason. + /// + /// In fr, this message translates to: + /// **'Motif de la réservation'** + String get bookingBookingReason; + + /// No description provided for @bookingBy. + /// + /// In fr, this message translates to: + /// **'par'** + String get bookingBy; + + /// No description provided for @bookingConfirm. + /// + /// In fr, this message translates to: + /// **'Confirmer'** + String get bookingConfirm; + + /// No description provided for @bookingConfirmation. + /// + /// In fr, this message translates to: + /// **'Confirmation'** + String get bookingConfirmation; + + /// No description provided for @bookingConfirmBooking. + /// + /// In fr, this message translates to: + /// **'Confirmer la réservation ?'** + String get bookingConfirmBooking; + + /// No description provided for @bookingConfirmed. + /// + /// In fr, this message translates to: + /// **'Validée'** + String get bookingConfirmed; + + /// No description provided for @bookingDates. + /// + /// In fr, this message translates to: + /// **'Dates'** + String get bookingDates; + + /// No description provided for @bookingDecline. + /// + /// In fr, this message translates to: + /// **'Refuser'** + String get bookingDecline; + + /// No description provided for @bookingDeclineBooking. + /// + /// In fr, this message translates to: + /// **'Refuser la réservation ?'** + String get bookingDeclineBooking; + + /// No description provided for @bookingDeclined. + /// + /// In fr, this message translates to: + /// **'Refusée'** + String get bookingDeclined; + + /// No description provided for @bookingDelete. + /// + /// In fr, this message translates to: + /// **'Supprimer'** + String get bookingDelete; + + /// No description provided for @bookingDeleting. + /// + /// In fr, this message translates to: + /// **'Suppression'** + String get bookingDeleting; + + /// No description provided for @bookingDeleteBooking. + /// + /// In fr, this message translates to: + /// **'Suppression'** + String get bookingDeleteBooking; + + /// No description provided for @bookingDeleteBookingConfirmation. + /// + /// In fr, this message translates to: + /// **'Êtes-vous sûr de vouloir supprimer cette réservation ?'** + String get bookingDeleteBookingConfirmation; + + /// No description provided for @bookingDeletedBooking. + /// + /// In fr, this message translates to: + /// **'Réservation supprimée'** + String get bookingDeletedBooking; + + /// No description provided for @bookingDeletedRoom. + /// + /// In fr, this message translates to: + /// **'Salle supprimée'** + String get bookingDeletedRoom; + + /// No description provided for @bookingDeletedManager. + /// + /// In fr, this message translates to: + /// **'Gestionnaire supprimé'** + String get bookingDeletedManager; + + /// No description provided for @bookingDeleteRoomConfirmation. + /// + /// In fr, this message translates to: + /// **'Êtes-vous sûr de vouloir supprimer cette salle ?\n\nLa salle ne doit avoir aucune réservation en cours ou à venir pour être supprimée'** + String get bookingDeleteRoomConfirmation; + + /// No description provided for @bookingDeleteManagerConfirmation. + /// + /// In fr, this message translates to: + /// **'Êtes-vous sûr de vouloir supprimer ce gestionnaire ?\n\nLe gestionnaire ne doit être associé à aucune salle pour pouvoir être supprimé'** + String get bookingDeleteManagerConfirmation; + + /// No description provided for @bookingDeletingBooking. + /// + /// In fr, this message translates to: + /// **'Supprimer la réservation ?'** + String get bookingDeletingBooking; + + /// No description provided for @bookingDeletingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la suppression'** + String get bookingDeletingError; + + /// No description provided for @bookingDeletingRoom. + /// + /// In fr, this message translates to: + /// **'Supprimer la salle ?'** + String get bookingDeletingRoom; + + /// No description provided for @bookingEdit. + /// + /// In fr, this message translates to: + /// **'Modifier'** + String get bookingEdit; + + /// No description provided for @bookingEditBooking. + /// + /// In fr, this message translates to: + /// **'Modifier une réservation'** + String get bookingEditBooking; + + /// No description provided for @bookingEditionError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la modification'** + String get bookingEditionError; + + /// No description provided for @bookingEditedBooking. + /// + /// In fr, this message translates to: + /// **'Réservation modifiée'** + String get bookingEditedBooking; + + /// No description provided for @bookingEditedRoom. + /// + /// In fr, this message translates to: + /// **'Salle modifiée'** + String get bookingEditedRoom; + + /// No description provided for @bookingEditedManager. + /// + /// In fr, this message translates to: + /// **'Gestionnaire modifié'** + String get bookingEditedManager; + + /// No description provided for @bookingEditManager. + /// + /// In fr, this message translates to: + /// **'Modifier ou supprimer un gestionnaire'** + String get bookingEditManager; + + /// No description provided for @bookingEditRoom. + /// + /// In fr, this message translates to: + /// **'Modifier ou supprimer une salle'** + String get bookingEditRoom; + + /// No description provided for @bookingEndDate. + /// + /// In fr, this message translates to: + /// **'Date de fin'** + String get bookingEndDate; + + /// No description provided for @bookingEndHour. + /// + /// In fr, this message translates to: + /// **'Heure de fin'** + String get bookingEndHour; + + /// No description provided for @bookingEntity. + /// + /// In fr, this message translates to: + /// **'Pour qui ?'** + String get bookingEntity; + + /// No description provided for @bookingError. + /// + /// In fr, this message translates to: + /// **'Erreur'** + String get bookingError; + + /// No description provided for @bookingEventEvery. + /// + /// In fr, this message translates to: + /// **'Tous les'** + String get bookingEventEvery; + + /// No description provided for @bookingHistoryPage. + /// + /// In fr, this message translates to: + /// **'Historique'** + String get bookingHistoryPage; + + /// No description provided for @bookingIncorrectOrMissingFields. + /// + /// In fr, this message translates to: + /// **'Champs incorrects ou manquants'** + String get bookingIncorrectOrMissingFields; + + /// No description provided for @bookingInterval. + /// + /// In fr, this message translates to: + /// **'Intervalle'** + String get bookingInterval; + + /// No description provided for @bookingInvalidIntervalError. + /// + /// In fr, this message translates to: + /// **'Intervalle invalide'** + String get bookingInvalidIntervalError; + + /// No description provided for @bookingInvalidDates. + /// + /// In fr, this message translates to: + /// **'Dates invalides'** + String get bookingInvalidDates; + + /// No description provided for @bookingInvalidRoom. + /// + /// In fr, this message translates to: + /// **'Salle invalide'** + String get bookingInvalidRoom; + + /// No description provided for @bookingKeysRequested. + /// + /// In fr, this message translates to: + /// **'Clés demandées'** + String get bookingKeysRequested; + + /// No description provided for @bookingManagement. + /// + /// In fr, this message translates to: + /// **'Gestion'** + String get bookingManagement; + + /// No description provided for @bookingManager. + /// + /// In fr, this message translates to: + /// **'Gestionnaire'** + String get bookingManager; + + /// No description provided for @bookingManagerName. + /// + /// In fr, this message translates to: + /// **'Nom du gestionnaire'** + String get bookingManagerName; + + /// No description provided for @bookingMultipleDay. + /// + /// In fr, this message translates to: + /// **'Plusieurs jours'** + String get bookingMultipleDay; + + /// No description provided for @bookingMyBookings. + /// + /// In fr, this message translates to: + /// **'Mes réservations'** + String get bookingMyBookings; + + /// No description provided for @bookingNecessaryKey. + /// + /// In fr, this message translates to: + /// **'Clé nécessaire'** + String get bookingNecessaryKey; + + /// No description provided for @bookingNext. + /// + /// In fr, this message translates to: + /// **'Suivant'** + String get bookingNext; + + /// No description provided for @bookingNo. + /// + /// In fr, this message translates to: + /// **'Non'** + String get bookingNo; + + /// No description provided for @bookingNoCurrentBooking. + /// + /// In fr, this message translates to: + /// **'Pas de réservation en cours'** + String get bookingNoCurrentBooking; + + /// No description provided for @bookingNoDateError. + /// + /// In fr, this message translates to: + /// **'Veuillez choisir une date'** + String get bookingNoDateError; + + /// No description provided for @bookingNoAppointmentInReccurence. + /// + /// In fr, this message translates to: + /// **'Aucun créneau existe avec ces paramètres de récurrence'** + String get bookingNoAppointmentInReccurence; + + /// No description provided for @bookingNoDaySelected. + /// + /// In fr, this message translates to: + /// **'Aucun jour sélectionné'** + String get bookingNoDaySelected; + + /// No description provided for @bookingNoDescriptionError. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer une description'** + String get bookingNoDescriptionError; + + /// No description provided for @bookingNoKeys. + /// + /// In fr, this message translates to: + /// **'Aucune clé'** + String get bookingNoKeys; + + /// No description provided for @bookingNoNoteError. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer une note'** + String get bookingNoNoteError; + + /// No description provided for @bookingNoPhoneRegistered. + /// + /// In fr, this message translates to: + /// **'Numéro non renseigné'** + String get bookingNoPhoneRegistered; + + /// No description provided for @bookingNoReasonError. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer un motif'** + String get bookingNoReasonError; + + /// No description provided for @bookingNoRoomFoundError. + /// + /// In fr, this message translates to: + /// **'Aucune salle enregistrée'** + String get bookingNoRoomFoundError; + + /// No description provided for @bookingNoRoomFound. + /// + /// In fr, this message translates to: + /// **'Aucune salle trouvée'** + String get bookingNoRoomFound; + + /// No description provided for @bookingNote. + /// + /// In fr, this message translates to: + /// **'Note'** + String get bookingNote; + + /// No description provided for @bookingOther. + /// + /// In fr, this message translates to: + /// **'Autre'** + String get bookingOther; + + /// No description provided for @bookingPending. + /// + /// In fr, this message translates to: + /// **'En attente'** + String get bookingPending; + + /// No description provided for @bookingPrevious. + /// + /// In fr, this message translates to: + /// **'Précédent'** + String get bookingPrevious; + + /// No description provided for @bookingReason. + /// + /// In fr, this message translates to: + /// **'Motif'** + String get bookingReason; + + /// No description provided for @bookingRecurrence. + /// + /// In fr, this message translates to: + /// **'Récurrence'** + String get bookingRecurrence; + + /// No description provided for @bookingRecurrenceDays. + /// + /// In fr, this message translates to: + /// **'Jours de récurrence'** + String get bookingRecurrenceDays; + + /// No description provided for @bookingRecurrenceEndDate. + /// + /// In fr, this message translates to: + /// **'Date de fin de récurrence'** + String get bookingRecurrenceEndDate; + + /// No description provided for @bookingRecurrent. + /// + /// In fr, this message translates to: + /// **'Récurrent'** + String get bookingRecurrent; + + /// No description provided for @bookingRegisteredRooms. + /// + /// In fr, this message translates to: + /// **'Salles enregistrées'** + String get bookingRegisteredRooms; + + /// No description provided for @bookingRoom. + /// + /// In fr, this message translates to: + /// **'Salle'** + String get bookingRoom; + + /// No description provided for @bookingRoomName. + /// + /// In fr, this message translates to: + /// **'Nom de la salle'** + String get bookingRoomName; + + /// No description provided for @bookingStartDate. + /// + /// In fr, this message translates to: + /// **'Date de début'** + String get bookingStartDate; + + /// No description provided for @bookingStartHour. + /// + /// In fr, this message translates to: + /// **'Heure de début'** + String get bookingStartHour; + + /// No description provided for @bookingWeeks. + /// + /// In fr, this message translates to: + /// **'Semaines'** + String get bookingWeeks; + + /// No description provided for @bookingYes. + /// + /// In fr, this message translates to: + /// **'Oui'** + String get bookingYes; + + /// No description provided for @bookingWeekDayMon. + /// + /// In fr, this message translates to: + /// **'Lundi'** + String get bookingWeekDayMon; + + /// No description provided for @bookingWeekDayTue. + /// + /// In fr, this message translates to: + /// **'Mardi'** + String get bookingWeekDayTue; + + /// No description provided for @bookingWeekDayWed. + /// + /// In fr, this message translates to: + /// **'Mercredi'** + String get bookingWeekDayWed; + + /// No description provided for @bookingWeekDayThu. + /// + /// In fr, this message translates to: + /// **'Jeudi'** + String get bookingWeekDayThu; + + /// No description provided for @bookingWeekDayFri. + /// + /// In fr, this message translates to: + /// **'Vendredi'** + String get bookingWeekDayFri; + + /// No description provided for @bookingWeekDaySat. + /// + /// In fr, this message translates to: + /// **'Samedi'** + String get bookingWeekDaySat; + + /// No description provided for @bookingWeekDaySun. + /// + /// In fr, this message translates to: + /// **'Dimanche'** + String get bookingWeekDaySun; + + /// No description provided for @cinemaAdd. + /// + /// In fr, this message translates to: + /// **'Ajouter'** + String get cinemaAdd; + + /// No description provided for @cinemaAddedSession. + /// + /// In fr, this message translates to: + /// **'Séance ajoutée'** + String get cinemaAddedSession; + + /// No description provided for @cinemaAddingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de l\'ajout'** + String get cinemaAddingError; + + /// No description provided for @cinemaAddSession. + /// + /// In fr, this message translates to: + /// **'Ajouter une séance'** + String get cinemaAddSession; + + /// No description provided for @cinemaCinema. + /// + /// In fr, this message translates to: + /// **'Cinéma'** + String get cinemaCinema; + + /// No description provided for @cinemaDeleteSession. + /// + /// In fr, this message translates to: + /// **'Supprimer la séance ?'** + String get cinemaDeleteSession; + + /// No description provided for @cinemaDeleting. + /// + /// In fr, this message translates to: + /// **'Suppression'** + String get cinemaDeleting; + + /// No description provided for @cinemaDuration. + /// + /// In fr, this message translates to: + /// **'Durée'** + String get cinemaDuration; + + /// No description provided for @cinemaEdit. + /// + /// In fr, this message translates to: + /// **'Modifier'** + String get cinemaEdit; + + /// No description provided for @cinemaEditedSession. + /// + /// In fr, this message translates to: + /// **'Séance modifiée'** + String get cinemaEditedSession; + + /// No description provided for @cinemaEditingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la modification'** + String get cinemaEditingError; + + /// No description provided for @cinemaEditSession. + /// + /// In fr, this message translates to: + /// **'Modifier la séance'** + String get cinemaEditSession; + + /// No description provided for @cinemaEmptyUrl. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer une URL'** + String get cinemaEmptyUrl; + + /// No description provided for @cinemaImportFromTMDB. + /// + /// In fr, this message translates to: + /// **'Importer depuis TMDB'** + String get cinemaImportFromTMDB; + + /// No description provided for @cinemaIncomingSession. + /// + /// In fr, this message translates to: + /// **'A l\'affiche'** + String get cinemaIncomingSession; + + /// No description provided for @cinemaIncorrectOrMissingFields. + /// + /// In fr, this message translates to: + /// **'Champs incorrects ou manquants'** + String get cinemaIncorrectOrMissingFields; + + /// No description provided for @cinemaInvalidUrl. + /// + /// In fr, this message translates to: + /// **'URL invalide'** + String get cinemaInvalidUrl; + + /// No description provided for @cinemaGenre. + /// + /// In fr, this message translates to: + /// **'Genre'** + String get cinemaGenre; + + /// No description provided for @cinemaName. + /// + /// In fr, this message translates to: + /// **'Nom'** + String get cinemaName; + + /// No description provided for @cinemaNoDateError. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer une date'** + String get cinemaNoDateError; + + /// No description provided for @cinemaNoDuration. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer une durée'** + String get cinemaNoDuration; + + /// No description provided for @cinemaNoOverview. + /// + /// In fr, this message translates to: + /// **'Aucun synopsis'** + String get cinemaNoOverview; + + /// No description provided for @cinemaNoPoster. + /// + /// In fr, this message translates to: + /// **'Aucune affiche'** + String get cinemaNoPoster; + + /// No description provided for @cinemaNoSession. + /// + /// In fr, this message translates to: + /// **'Aucune séance'** + String get cinemaNoSession; + + /// No description provided for @cinemaOverview. + /// + /// In fr, this message translates to: + /// **'Synopsis'** + String get cinemaOverview; + + /// No description provided for @cinemaPosterUrl. + /// + /// In fr, this message translates to: + /// **'URL de l\'affiche'** + String get cinemaPosterUrl; + + /// No description provided for @cinemaSessionDate. + /// + /// In fr, this message translates to: + /// **'Jour de la séance'** + String get cinemaSessionDate; + + /// No description provided for @cinemaStartHour. + /// + /// In fr, this message translates to: + /// **'Heure de début'** + String get cinemaStartHour; + + /// No description provided for @cinemaTagline. + /// + /// In fr, this message translates to: + /// **'Slogan'** + String get cinemaTagline; + + /// No description provided for @cinemaThe. + /// + /// In fr, this message translates to: + /// **'Le'** + String get cinemaThe; + + /// No description provided for @drawerAdmin. + /// + /// In fr, this message translates to: + /// **'Administration'** + String get drawerAdmin; + + /// No description provided for @drawerAndroidAppLink. + /// + /// In fr, this message translates to: + /// **'https://play.google.com/store/apps/details?id=fr.myecl.titan'** + String get drawerAndroidAppLink; + + /// No description provided for @drawerCopied. + /// + /// In fr, this message translates to: + /// **'Copié !'** + String get drawerCopied; + + /// No description provided for @drawerDownloadAppOnMobileDevice. + /// + /// In fr, this message translates to: + /// **'Ce site est la version Web de l\'application MyECL. Nous vous invitons à télécharger l\'application. N\'utilisez ce site qu\'en cas de problème avec l\'application.\n'** + String get drawerDownloadAppOnMobileDevice; + + /// No description provided for @drawerIosAppLink. + /// + /// In fr, this message translates to: + /// **'https://apps.apple.com/fr/app/myecl/id6444443430'** + String get drawerIosAppLink; + + /// No description provided for @drawerLoginOut. + /// + /// In fr, this message translates to: + /// **'Voulez-vous vous déconnecter ?'** + String get drawerLoginOut; + + /// No description provided for @drawerLogOut. + /// + /// In fr, this message translates to: + /// **'Déconnexion'** + String get drawerLogOut; + + /// No description provided for @drawerOr. + /// + /// In fr, this message translates to: + /// **' ou '** + String get drawerOr; + + /// No description provided for @drawerSettings. + /// + /// In fr, this message translates to: + /// **'Paramètres'** + String get drawerSettings; + + /// No description provided for @eventAdd. + /// + /// In fr, this message translates to: + /// **'Ajouter'** + String get eventAdd; + + /// No description provided for @eventAddEvent. + /// + /// In fr, this message translates to: + /// **'Ajouter un événement'** + String get eventAddEvent; + + /// No description provided for @eventAddedEvent. + /// + /// In fr, this message translates to: + /// **'Événement ajouté'** + String get eventAddedEvent; + + /// No description provided for @eventAddingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de l\'ajout'** + String get eventAddingError; + + /// No description provided for @eventAllDay. + /// + /// In fr, this message translates to: + /// **'Toute la journée'** + String get eventAllDay; + + /// No description provided for @eventConfirm. + /// + /// In fr, this message translates to: + /// **'Confirmer'** + String get eventConfirm; + + /// No description provided for @eventConfirmEvent. + /// + /// In fr, this message translates to: + /// **'Confirmer l\'événement ?'** + String get eventConfirmEvent; + + /// No description provided for @eventConfirmation. + /// + /// In fr, this message translates to: + /// **'Confirmation'** + String get eventConfirmation; + + /// No description provided for @eventConfirmed. + /// + /// In fr, this message translates to: + /// **'Confirmé'** + String get eventConfirmed; + + /// No description provided for @eventDates. + /// + /// In fr, this message translates to: + /// **'Dates'** + String get eventDates; + + /// No description provided for @eventDecline. + /// + /// In fr, this message translates to: + /// **'Refuser'** + String get eventDecline; + + /// No description provided for @eventDeclineEvent. + /// + /// In fr, this message translates to: + /// **'Refuser l\'événement ?'** + String get eventDeclineEvent; + + /// No description provided for @eventDeclined. + /// + /// In fr, this message translates to: + /// **'Refusé'** + String get eventDeclined; + + /// No description provided for @eventDelete. + /// + /// In fr, this message translates to: + /// **'Supprimer'** + String get eventDelete; + + /// Delete the event with its name + /// + /// In fr, this message translates to: + /// **'Supprimer l\'event {name} ?'** + String eventDeleteConfirm(String name); + + /// No description provided for @eventDeletedEvent. + /// + /// In fr, this message translates to: + /// **'Événement supprimé'** + String get eventDeletedEvent; + + /// No description provided for @eventDeleting. + /// + /// In fr, this message translates to: + /// **'Suppression'** + String get eventDeleting; + + /// No description provided for @eventDeletingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la suppression'** + String get eventDeletingError; + + /// No description provided for @eventDeletingEvent. + /// + /// In fr, this message translates to: + /// **'Supprimer l\'événement ?'** + String get eventDeletingEvent; + + /// No description provided for @eventDescription. + /// + /// In fr, this message translates to: + /// **'Description'** + String get eventDescription; + + /// No description provided for @eventEdit. + /// + /// In fr, this message translates to: + /// **'Modifier'** + String get eventEdit; + + /// No description provided for @eventEditEvent. + /// + /// In fr, this message translates to: + /// **'Modifier un événement'** + String get eventEditEvent; + + /// No description provided for @eventEditedEvent. + /// + /// In fr, this message translates to: + /// **'Événement modifié'** + String get eventEditedEvent; + + /// No description provided for @eventEditingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la modification'** + String get eventEditingError; + + /// No description provided for @eventEndDate. + /// + /// In fr, this message translates to: + /// **'Date de fin'** + String get eventEndDate; + + /// No description provided for @eventEndHour. + /// + /// In fr, this message translates to: + /// **'Heure de fin'** + String get eventEndHour; + + /// No description provided for @eventError. + /// + /// In fr, this message translates to: + /// **'Erreur'** + String get eventError; + + /// No description provided for @eventEventList. + /// + /// In fr, this message translates to: + /// **'Liste des événements'** + String get eventEventList; + + /// No description provided for @eventEventType. + /// + /// In fr, this message translates to: + /// **'Type d\'événement'** + String get eventEventType; + + /// No description provided for @eventEvery. + /// + /// In fr, this message translates to: + /// **'Tous les'** + String get eventEvery; + + /// No description provided for @eventHistory. + /// + /// In fr, this message translates to: + /// **'Historique'** + String get eventHistory; + + /// No description provided for @eventIncorrectOrMissingFields. + /// + /// In fr, this message translates to: + /// **'Certains champs sont incorrects ou manquants'** + String get eventIncorrectOrMissingFields; + + /// No description provided for @eventInterval. + /// + /// In fr, this message translates to: + /// **'Intervalle'** + String get eventInterval; + + /// No description provided for @eventInvalidDates. + /// + /// In fr, this message translates to: + /// **'La date de fin doit être après la date de début'** + String get eventInvalidDates; + + /// No description provided for @eventInvalidIntervalError. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer un intervalle valide'** + String get eventInvalidIntervalError; + + /// No description provided for @eventLocation. + /// + /// In fr, this message translates to: + /// **'Lieu'** + String get eventLocation; + + /// No description provided for @eventModifiedEvent. + /// + /// In fr, this message translates to: + /// **'Événement modifié'** + String get eventModifiedEvent; + + /// No description provided for @eventModifyingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la modification'** + String get eventModifyingError; + + /// No description provided for @eventMyEvents. + /// + /// In fr, this message translates to: + /// **'Mes événements'** + String get eventMyEvents; + + /// No description provided for @eventName. + /// + /// In fr, this message translates to: + /// **'Nom'** + String get eventName; + + /// No description provided for @eventNext. + /// + /// In fr, this message translates to: + /// **'Suivant'** + String get eventNext; + + /// No description provided for @eventNo. + /// + /// In fr, this message translates to: + /// **'Non'** + String get eventNo; + + /// No description provided for @eventNoCurrentEvent. + /// + /// In fr, this message translates to: + /// **'Aucun événement en cours'** + String get eventNoCurrentEvent; + + /// No description provided for @eventNoDateError. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer une date'** + String get eventNoDateError; + + /// No description provided for @eventNoDaySelected. + /// + /// In fr, this message translates to: + /// **'Aucun jour sélectionné'** + String get eventNoDaySelected; + + /// No description provided for @eventNoDescriptionError. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer une description'** + String get eventNoDescriptionError; + + /// No description provided for @eventNoEvent. + /// + /// In fr, this message translates to: + /// **'Aucun événement'** + String get eventNoEvent; + + /// No description provided for @eventNoNameError. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer un nom'** + String get eventNoNameError; + + /// No description provided for @eventNoOrganizerError. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer un organisateur'** + String get eventNoOrganizerError; + + /// No description provided for @eventNoPlaceError. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer un lieu'** + String get eventNoPlaceError; + + /// No description provided for @eventNoPhoneRegistered. + /// + /// In fr, this message translates to: + /// **'Numéro non renseigné'** + String get eventNoPhoneRegistered; + + /// No description provided for @eventNoRuleError. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer une règle de récurrence'** + String get eventNoRuleError; + + /// No description provided for @eventOrganizer. + /// + /// In fr, this message translates to: + /// **'Organisateur'** + String get eventOrganizer; + + /// No description provided for @eventOther. + /// + /// In fr, this message translates to: + /// **'Autre'** + String get eventOther; + + /// No description provided for @eventPending. + /// + /// In fr, this message translates to: + /// **'En attente'** + String get eventPending; + + /// No description provided for @eventPrevious. + /// + /// In fr, this message translates to: + /// **'Précédent'** + String get eventPrevious; + + /// No description provided for @eventRecurrence. + /// + /// In fr, this message translates to: + /// **'Récurrence'** + String get eventRecurrence; + + /// No description provided for @eventRecurrenceDays. + /// + /// In fr, this message translates to: + /// **'Jours de récurrence'** + String get eventRecurrenceDays; + + /// No description provided for @eventRecurrenceEndDate. + /// + /// In fr, this message translates to: + /// **'Date de fin de la récurrence'** + String get eventRecurrenceEndDate; + + /// No description provided for @eventRecurrenceRule. + /// + /// In fr, this message translates to: + /// **'Règle de récurrence'** + String get eventRecurrenceRule; + + /// No description provided for @eventRoom. + /// + /// In fr, this message translates to: + /// **'Salle'** + String get eventRoom; + + /// No description provided for @eventStartDate. + /// + /// In fr, this message translates to: + /// **'Date de début'** + String get eventStartDate; + + /// No description provided for @eventStartHour. + /// + /// In fr, this message translates to: + /// **'Heure de début'** + String get eventStartHour; + + /// No description provided for @eventTitle. + /// + /// In fr, this message translates to: + /// **'Événements'** + String get eventTitle; + + /// No description provided for @eventYes. + /// + /// In fr, this message translates to: + /// **'Oui'** + String get eventYes; + + /// No description provided for @eventEventEvery. + /// + /// In fr, this message translates to: + /// **'Toutes les'** + String get eventEventEvery; + + /// No description provided for @eventWeeks. + /// + /// In fr, this message translates to: + /// **'semaines'** + String get eventWeeks; + + /// No description provided for @eventDayMon. + /// + /// In fr, this message translates to: + /// **'Lundi'** + String get eventDayMon; + + /// No description provided for @eventDayTue. + /// + /// In fr, this message translates to: + /// **'Mardi'** + String get eventDayTue; + + /// No description provided for @eventDayWed. + /// + /// In fr, this message translates to: + /// **'Mercredi'** + String get eventDayWed; + + /// No description provided for @eventDayThu. + /// + /// In fr, this message translates to: + /// **'Jeudi'** + String get eventDayThu; + + /// No description provided for @eventDayFri. + /// + /// In fr, this message translates to: + /// **'Vendredi'** + String get eventDayFri; + + /// No description provided for @eventDaySat. + /// + /// In fr, this message translates to: + /// **'Samedi'** + String get eventDaySat; + + /// No description provided for @eventDaySun. + /// + /// In fr, this message translates to: + /// **'Dimanche'** + String get eventDaySun; + + /// No description provided for @globalConfirm. + /// + /// In fr, this message translates to: + /// **'Confirmer'** + String get globalConfirm; + + /// No description provided for @globalCancel. + /// + /// In fr, this message translates to: + /// **'Annuler'** + String get globalCancel; + + /// No description provided for @globalIrreversibleAction. + /// + /// In fr, this message translates to: + /// **'Cette action est irréversible'** + String get globalIrreversibleAction; + + /// Texte avec complément optionnel + /// + /// In fr, this message translates to: + /// **'{text} (Optionnel)'** + String globalOptionnal(String text); + + /// No description provided for @homeCalendar. + /// + /// In fr, this message translates to: + /// **'Calendrier'** + String get homeCalendar; + + /// No description provided for @homeEventOf. + /// + /// In fr, this message translates to: + /// **'Évènements du'** + String get homeEventOf; + + /// No description provided for @homeIncomingEvents. + /// + /// In fr, this message translates to: + /// **'Évènements à venir'** + String get homeIncomingEvents; + + /// No description provided for @homeLastInfos. + /// + /// In fr, this message translates to: + /// **'Dernières annonces'** + String get homeLastInfos; + + /// No description provided for @homeNoEvents. + /// + /// In fr, this message translates to: + /// **'Aucun évènement'** + String get homeNoEvents; + + /// No description provided for @homeTranslateDayShortMon. + /// + /// In fr, this message translates to: + /// **'Lun'** + String get homeTranslateDayShortMon; + + /// No description provided for @homeTranslateDayShortTue. + /// + /// In fr, this message translates to: + /// **'Mar'** + String get homeTranslateDayShortTue; + + /// No description provided for @homeTranslateDayShortWed. + /// + /// In fr, this message translates to: + /// **'Mer'** + String get homeTranslateDayShortWed; + + /// No description provided for @homeTranslateDayShortThu. + /// + /// In fr, this message translates to: + /// **'Jeu'** + String get homeTranslateDayShortThu; + + /// No description provided for @homeTranslateDayShortFri. + /// + /// In fr, this message translates to: + /// **'Ven'** + String get homeTranslateDayShortFri; + + /// No description provided for @homeTranslateDayShortSat. + /// + /// In fr, this message translates to: + /// **'Sam'** + String get homeTranslateDayShortSat; + + /// No description provided for @homeTranslateDayShortSun. + /// + /// In fr, this message translates to: + /// **'Dim'** + String get homeTranslateDayShortSun; + + /// No description provided for @loanAdd. + /// + /// In fr, this message translates to: + /// **'Ajouter'** + String get loanAdd; + + /// No description provided for @loanAddLoan. + /// + /// In fr, this message translates to: + /// **'Ajouter un prêt'** + String get loanAddLoan; + + /// No description provided for @loanAddObject. + /// + /// In fr, this message translates to: + /// **'Ajouter un objet'** + String get loanAddObject; + + /// No description provided for @loanAddedLoan. + /// + /// In fr, this message translates to: + /// **'Prêt ajouté'** + String get loanAddedLoan; + + /// No description provided for @loanAddedObject. + /// + /// In fr, this message translates to: + /// **'Objet ajouté'** + String get loanAddedObject; + + /// No description provided for @loanAddedRoom. + /// + /// In fr, this message translates to: + /// **'Salle ajoutée'** + String get loanAddedRoom; + + /// No description provided for @loanAddingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de l\'ajout'** + String get loanAddingError; + + /// No description provided for @loanAdmin. + /// + /// In fr, this message translates to: + /// **'Administrateur'** + String get loanAdmin; + + /// No description provided for @loanAvailable. + /// + /// In fr, this message translates to: + /// **'Disponible'** + String get loanAvailable; + + /// No description provided for @loanAvailableMultiple. + /// + /// In fr, this message translates to: + /// **'Disponibles'** + String get loanAvailableMultiple; + + /// No description provided for @loanBorrowed. + /// + /// In fr, this message translates to: + /// **'Emprunté'** + String get loanBorrowed; + + /// No description provided for @loanBorrowedMultiple. + /// + /// In fr, this message translates to: + /// **'Empruntés'** + String get loanBorrowedMultiple; + + /// No description provided for @loanAnd. + /// + /// In fr, this message translates to: + /// **'et'** + String get loanAnd; + + /// No description provided for @loanAssociation. + /// + /// In fr, this message translates to: + /// **'Association'** + String get loanAssociation; + + /// No description provided for @loanAvailableItems. + /// + /// In fr, this message translates to: + /// **'Objets disponibles'** + String get loanAvailableItems; + + /// No description provided for @loanBeginDate. + /// + /// In fr, this message translates to: + /// **'Date du début du prêt'** + String get loanBeginDate; + + /// No description provided for @loanBorrower. + /// + /// In fr, this message translates to: + /// **'Emprunteur'** + String get loanBorrower; + + /// No description provided for @loanCaution. + /// + /// In fr, this message translates to: + /// **'Caution'** + String get loanCaution; + + /// No description provided for @loanCancel. + /// + /// In fr, this message translates to: + /// **'Annuler'** + String get loanCancel; + + /// No description provided for @loanConfirm. + /// + /// In fr, this message translates to: + /// **'Confirmer'** + String get loanConfirm; + + /// No description provided for @loanConfirmation. + /// + /// In fr, this message translates to: + /// **'Confirmation'** + String get loanConfirmation; + + /// No description provided for @loanDates. + /// + /// In fr, this message translates to: + /// **'Dates'** + String get loanDates; + + /// No description provided for @loanDays. + /// + /// In fr, this message translates to: + /// **'Jours'** + String get loanDays; + + /// No description provided for @loanDelay. + /// + /// In fr, this message translates to: + /// **'Délai de la prolongation'** + String get loanDelay; + + /// No description provided for @loanDelete. + /// + /// In fr, this message translates to: + /// **'Supprimer'** + String get loanDelete; + + /// No description provided for @loanDeletingLoan. + /// + /// In fr, this message translates to: + /// **'Supprimer le prêt ?'** + String get loanDeletingLoan; + + /// No description provided for @loanDeletedItem. + /// + /// In fr, this message translates to: + /// **'Objet supprimé'** + String get loanDeletedItem; + + /// No description provided for @loanDeletedLoan. + /// + /// In fr, this message translates to: + /// **'Prêt supprimé'** + String get loanDeletedLoan; + + /// No description provided for @loanDeleting. + /// + /// In fr, this message translates to: + /// **'Suppression'** + String get loanDeleting; + + /// No description provided for @loanDeletingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la suppression'** + String get loanDeletingError; + + /// No description provided for @loanDeletingItem. + /// + /// In fr, this message translates to: + /// **'Supprimer l\'objet ?'** + String get loanDeletingItem; + + /// No description provided for @loanDuration. + /// + /// In fr, this message translates to: + /// **'Durée'** + String get loanDuration; + + /// No description provided for @loanEdit. + /// + /// In fr, this message translates to: + /// **'Modifier'** + String get loanEdit; + + /// No description provided for @loanEditItem. + /// + /// In fr, this message translates to: + /// **'Modifier l\'objet'** + String get loanEditItem; + + /// No description provided for @loanEditLoan. + /// + /// In fr, this message translates to: + /// **'Modifier le prêt'** + String get loanEditLoan; + + /// No description provided for @loanEditedRoom. + /// + /// In fr, this message translates to: + /// **'Salle modifiée'** + String get loanEditedRoom; + + /// No description provided for @loanEndDate. + /// + /// In fr, this message translates to: + /// **'Date de fin du prêt'** + String get loanEndDate; + + /// No description provided for @loanEnded. + /// + /// In fr, this message translates to: + /// **'Terminé'** + String get loanEnded; + + /// No description provided for @loanEnterDate. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer une date'** + String get loanEnterDate; + + /// No description provided for @loanExtendedLoan. + /// + /// In fr, this message translates to: + /// **'Prêt prolongé'** + String get loanExtendedLoan; + + /// No description provided for @loanExtendingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la prolongation'** + String get loanExtendingError; + + /// No description provided for @loanHistory. + /// + /// In fr, this message translates to: + /// **'Historique'** + String get loanHistory; + + /// No description provided for @loanIncorrectOrMissingFields. + /// + /// In fr, this message translates to: + /// **'Des champs sont manquants ou incorrects'** + String get loanIncorrectOrMissingFields; + + /// No description provided for @loanInvalidNumber. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer un nombre'** + String get loanInvalidNumber; + + /// No description provided for @loanInvalidDates. + /// + /// In fr, this message translates to: + /// **'Les dates ne sont pas valides'** + String get loanInvalidDates; + + /// No description provided for @loanItem. + /// + /// In fr, this message translates to: + /// **'Objet'** + String get loanItem; + + /// No description provided for @loanItems. + /// + /// In fr, this message translates to: + /// **'Objets'** + String get loanItems; + + /// No description provided for @loanItemHandling. + /// + /// In fr, this message translates to: + /// **'Gestion des objets'** + String get loanItemHandling; + + /// No description provided for @loanItemSelected. + /// + /// In fr, this message translates to: + /// **'objet sélectionné'** + String get loanItemSelected; + + /// No description provided for @loanItemsSelected. + /// + /// In fr, this message translates to: + /// **'objets sélectionnés'** + String get loanItemsSelected; + + /// No description provided for @loanLendingDuration. + /// + /// In fr, this message translates to: + /// **'Durée possible du prêt'** + String get loanLendingDuration; + + /// No description provided for @loanLoan. + /// + /// In fr, this message translates to: + /// **'Prêt'** + String get loanLoan; + + /// No description provided for @loanLoanHandling. + /// + /// In fr, this message translates to: + /// **'Gestion des prêts'** + String get loanLoanHandling; + + /// No description provided for @loanLooking. + /// + /// In fr, this message translates to: + /// **'Rechercher'** + String get loanLooking; + + /// No description provided for @loanName. + /// + /// In fr, this message translates to: + /// **'Nom'** + String get loanName; + + /// No description provided for @loanNext. + /// + /// In fr, this message translates to: + /// **'Suivant'** + String get loanNext; + + /// No description provided for @loanNo. + /// + /// In fr, this message translates to: + /// **'Non'** + String get loanNo; + + /// No description provided for @loanNoAssociationsFounded. + /// + /// In fr, this message translates to: + /// **'Aucune association trouvée'** + String get loanNoAssociationsFounded; + + /// No description provided for @loanNoAvailableItems. + /// + /// In fr, this message translates to: + /// **'Aucun objet disponible'** + String get loanNoAvailableItems; + + /// No description provided for @loanNoBorrower. + /// + /// In fr, this message translates to: + /// **'Aucun emprunteur'** + String get loanNoBorrower; + + /// No description provided for @loanNoItems. + /// + /// In fr, this message translates to: + /// **'Aucun objet'** + String get loanNoItems; + + /// No description provided for @loanNoItemSelected. + /// + /// In fr, this message translates to: + /// **'Aucun objet sélectionné'** + String get loanNoItemSelected; + + /// No description provided for @loanNoLoan. + /// + /// In fr, this message translates to: + /// **'Aucun prêt'** + String get loanNoLoan; + + /// No description provided for @loanNoReturnedDate. + /// + /// In fr, this message translates to: + /// **'Pas de date de retour'** + String get loanNoReturnedDate; + + /// No description provided for @loanQuantity. + /// + /// In fr, this message translates to: + /// **'Quantité'** + String get loanQuantity; + + /// No description provided for @loanNone. + /// + /// In fr, this message translates to: + /// **'Aucun'** + String get loanNone; + + /// No description provided for @loanNote. + /// + /// In fr, this message translates to: + /// **'Note'** + String get loanNote; + + /// No description provided for @loanNoValue. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer une valeur'** + String get loanNoValue; + + /// No description provided for @loanOnGoing. + /// + /// In fr, this message translates to: + /// **'En cours'** + String get loanOnGoing; + + /// No description provided for @loanOnGoingLoan. + /// + /// In fr, this message translates to: + /// **'Prêt en cours'** + String get loanOnGoingLoan; + + /// No description provided for @loanOthers. + /// + /// In fr, this message translates to: + /// **'autres'** + String get loanOthers; + + /// No description provided for @loanPaidCaution. + /// + /// In fr, this message translates to: + /// **'Caution payée'** + String get loanPaidCaution; + + /// No description provided for @loanPositiveNumber. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer un nombre positif'** + String get loanPositiveNumber; + + /// No description provided for @loanPrevious. + /// + /// In fr, this message translates to: + /// **'Précédent'** + String get loanPrevious; + + /// No description provided for @loanReturned. + /// + /// In fr, this message translates to: + /// **'Rendu'** + String get loanReturned; + + /// No description provided for @loanReturnedLoan. + /// + /// In fr, this message translates to: + /// **'Prêt rendu'** + String get loanReturnedLoan; + + /// No description provided for @loanReturningError. + /// + /// In fr, this message translates to: + /// **'Erreur lors du retour'** + String get loanReturningError; + + /// No description provided for @loanReturningLoan. + /// + /// In fr, this message translates to: + /// **'Retour'** + String get loanReturningLoan; + + /// No description provided for @loanReturnLoan. + /// + /// In fr, this message translates to: + /// **'Rendre le prêt ?'** + String get loanReturnLoan; + + /// No description provided for @loanReturnLoanDescription. + /// + /// In fr, this message translates to: + /// **'Voulez-vous rendre ce prêt ?'** + String get loanReturnLoanDescription; + + /// No description provided for @loanToReturn. + /// + /// In fr, this message translates to: + /// **'A rendre'** + String get loanToReturn; + + /// No description provided for @loanUnavailable. + /// + /// In fr, this message translates to: + /// **'Indisponible'** + String get loanUnavailable; + + /// No description provided for @loanUpdate. + /// + /// In fr, this message translates to: + /// **'Modifier'** + String get loanUpdate; + + /// No description provided for @loanUpdatedItem. + /// + /// In fr, this message translates to: + /// **'Objet modifié'** + String get loanUpdatedItem; + + /// No description provided for @loanUpdatedLoan. + /// + /// In fr, this message translates to: + /// **'Prêt modifié'** + String get loanUpdatedLoan; + + /// No description provided for @loanUpdatingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la modification'** + String get loanUpdatingError; + + /// No description provided for @loanYes. + /// + /// In fr, this message translates to: + /// **'Oui'** + String get loanYes; + + /// No description provided for @loginAppName. + /// + /// In fr, this message translates to: + /// **'MyECL'** + String get loginAppName; + + /// No description provided for @loginCreateAccount. + /// + /// In fr, this message translates to: + /// **'Créer un compte'** + String get loginCreateAccount; + + /// No description provided for @loginForgotPassword. + /// + /// In fr, this message translates to: + /// **'Mot de passe oublié ?'** + String get loginForgotPassword; + + /// No description provided for @loginFruitVegetableOrders. + /// + /// In fr, this message translates to: + /// **'Commandes de fruits et légumes'** + String get loginFruitVegetableOrders; + + /// No description provided for @loginInterfaceCustomization. + /// + /// In fr, this message translates to: + /// **'Personnalisation de l\'interface'** + String get loginInterfaceCustomization; + + /// No description provided for @loginLoginFailed. + /// + /// In fr, this message translates to: + /// **'Échec de la connexion'** + String get loginLoginFailed; + + /// No description provided for @loginMadeBy. + /// + /// In fr, this message translates to: + /// **'Développé par ProximApp'** + String get loginMadeBy; + + /// No description provided for @loginMaterialLoans. + /// + /// In fr, this message translates to: + /// **'Gestion des prêts de matériel'** + String get loginMaterialLoans; + + /// No description provided for @loginNewTermsElections. + /// + /// In fr, this message translates to: + /// **'L\'élection des nouveaux mandats'** + String get loginNewTermsElections; + + /// No description provided for @loginRaffles. + /// + /// In fr, this message translates to: + /// **'Tombolas'** + String get loginRaffles; + + /// No description provided for @loginSignIn. + /// + /// In fr, this message translates to: + /// **'Se connecter'** + String get loginSignIn; + + /// No description provided for @loginRegister. + /// + /// In fr, this message translates to: + /// **'S\'inscrire'** + String get loginRegister; + + /// No description provided for @loginShortDescription. + /// + /// In fr, this message translates to: + /// **'L\'application de l\'associatif'** + String get loginShortDescription; + + /// No description provided for @loginUpcomingEvents. + /// + /// In fr, this message translates to: + /// **'Les évènements à venir'** + String get loginUpcomingEvents; + + /// No description provided for @loginUpcomingScreenings. + /// + /// In fr, this message translates to: + /// **'Les prochaines séances'** + String get loginUpcomingScreenings; + + /// No description provided for @othersCheckInternetConnection. + /// + /// In fr, this message translates to: + /// **'Veuillez vérifier votre connexion internet'** + String get othersCheckInternetConnection; + + /// No description provided for @othersRetry. + /// + /// In fr, this message translates to: + /// **'Réessayer'** + String get othersRetry; + + /// No description provided for @othersTooOldVersion. + /// + /// In fr, this message translates to: + /// **'Votre version de l\'application est trop ancienne.\n\nVeuillez mettre à jour l\'application.'** + String get othersTooOldVersion; + + /// No description provided for @othersUnableToConnectToServer. + /// + /// In fr, this message translates to: + /// **'Impossible de se connecter au serveur'** + String get othersUnableToConnectToServer; + + /// No description provided for @othersVersion. + /// + /// In fr, this message translates to: + /// **'Version'** + String get othersVersion; + + /// No description provided for @othersNoModule. + /// + /// In fr, this message translates to: + /// **'Aucun module disponible, veuillez réessayer ultérieurement 😢😢'** + String get othersNoModule; + + /// No description provided for @othersAdmin. + /// + /// In fr, this message translates to: + /// **'Admin'** + String get othersAdmin; + + /// No description provided for @othersError. + /// + /// In fr, this message translates to: + /// **'Une erreur est survenue'** + String get othersError; + + /// No description provided for @othersNoValue. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer une valeur'** + String get othersNoValue; + + /// No description provided for @othersInvalidNumber. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer un nombre'** + String get othersInvalidNumber; + + /// No description provided for @othersNoDateError. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer une date'** + String get othersNoDateError; + + /// No description provided for @othersImageSizeTooBig. + /// + /// In fr, this message translates to: + /// **'La taille de l\'image ne doit pas dépasser 4 Mio'** + String get othersImageSizeTooBig; + + /// No description provided for @othersImageError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de l\'ajout de l\'image'** + String get othersImageError; + + /// No description provided for @paiementAccept. + /// + /// In fr, this message translates to: + /// **'Accepter'** + String get paiementAccept; + + /// No description provided for @paiementAccessPage. + /// + /// In fr, this message translates to: + /// **'Accéder à la page'** + String get paiementAccessPage; + + /// No description provided for @paiementAdd. + /// + /// In fr, this message translates to: + /// **'Ajouter'** + String get paiementAdd; + + /// No description provided for @paiementAddedSeller. + /// + /// In fr, this message translates to: + /// **'Vendeur ajouté'** + String get paiementAddedSeller; + + /// No description provided for @paiementAddingSellerError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de l\'ajout du vendeur'** + String get paiementAddingSellerError; + + /// No description provided for @paiementAddingStoreError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de l\'ajout du magasin'** + String get paiementAddingStoreError; + + /// No description provided for @paiementAddSeller. + /// + /// In fr, this message translates to: + /// **'Ajouter un vendeur'** + String get paiementAddSeller; + + /// No description provided for @paiementAddStore. + /// + /// In fr, this message translates to: + /// **'Ajouter un magasin'** + String get paiementAddStore; + + /// No description provided for @paiementAddThisDevice. + /// + /// In fr, this message translates to: + /// **'Ajouter cet appareil'** + String get paiementAddThisDevice; + + /// No description provided for @paiementAdmin. + /// + /// In fr, this message translates to: + /// **'Administrateur'** + String get paiementAdmin; + + /// No description provided for @paiementAmount. + /// + /// In fr, this message translates to: + /// **'Montant'** + String get paiementAmount; + + /// No description provided for @paiementAskDeviceActivation. + /// + /// In fr, this message translates to: + /// **'Demande d\'activation de l\'appareil'** + String get paiementAskDeviceActivation; + + /// No description provided for @paiementAStore. + /// + /// In fr, this message translates to: + /// **'un magasin'** + String get paiementAStore; + + /// No description provided for @paiementAt. + /// + /// In fr, this message translates to: + /// **'à'** + String get paiementAt; + + /// No description provided for @paiementAuthenticationRequired. + /// + /// In fr, this message translates to: + /// **'Authentification requise pour payer'** + String get paiementAuthenticationRequired; + + /// No description provided for @paiementAuthentificationFailed. + /// + /// In fr, this message translates to: + /// **'Échec de l\'authentification'** + String get paiementAuthentificationFailed; + + /// No description provided for @paiementBalanceAfterTopUp. + /// + /// In fr, this message translates to: + /// **'Solde après recharge :'** + String get paiementBalanceAfterTopUp; + + /// No description provided for @paiementBalanceAfterTransaction. + /// + /// In fr, this message translates to: + /// **'Solde après paiement : '** + String get paiementBalanceAfterTransaction; + + /// No description provided for @paiementBank. + /// + /// In fr, this message translates to: + /// **'Encaisser'** + String get paiementBank; + + /// No description provided for @paiementBillingSpace. + /// + /// In fr, this message translates to: + /// **'Espace facturation'** + String get paiementBillingSpace; + + /// No description provided for @paiementCameraPermissionRequired. + /// + /// In fr, this message translates to: + /// **'Permission d\'accès à la caméra requise'** + String get paiementCameraPermissionRequired; + + /// No description provided for @paiementCameraPerssionRequiredDescription. + /// + /// In fr, this message translates to: + /// **'Pour scanner un QR Code, vous devez autoriser l\'accès à la caméra.'** + String get paiementCameraPerssionRequiredDescription; + + /// No description provided for @paiementCanBank. + /// + /// In fr, this message translates to: + /// **'Peut encaisser'** + String get paiementCanBank; + + /// No description provided for @paiementCanCancelTransaction. + /// + /// In fr, this message translates to: + /// **'Peut annuler des transactions'** + String get paiementCanCancelTransaction; + + /// No description provided for @paiementCancel. + /// + /// In fr, this message translates to: + /// **'Annuler'** + String get paiementCancel; + + /// No description provided for @paiementCancelled. + /// + /// In fr, this message translates to: + /// **'Annulé'** + String get paiementCancelled; + + /// No description provided for @paiementCancelledTransaction. + /// + /// In fr, this message translates to: + /// **'Paiement annulé'** + String get paiementCancelledTransaction; + + /// No description provided for @paiementCancelTransaction. + /// + /// In fr, this message translates to: + /// **'Annuler la transaction'** + String get paiementCancelTransaction; + + /// No description provided for @paiementCancelTransactions. + /// + /// In fr, this message translates to: + /// **'Annuler les transactions'** + String get paiementCancelTransactions; + + /// No description provided for @paiementCanManageSellers. + /// + /// In fr, this message translates to: + /// **'Peut gérer les vendeurs'** + String get paiementCanManageSellers; + + /// No description provided for @paiementCanSeeHistory. + /// + /// In fr, this message translates to: + /// **'Peut voir l\'historique'** + String get paiementCanSeeHistory; + + /// No description provided for @paiementCantLaunchURL. + /// + /// In fr, this message translates to: + /// **'Impossible d\'ouvrir le lien'** + String get paiementCantLaunchURL; + + /// No description provided for @paiementClose. + /// + /// In fr, this message translates to: + /// **'Fermer'** + String get paiementClose; + + /// No description provided for @paiementCreate. + /// + /// In fr, this message translates to: + /// **'Créer'** + String get paiementCreate; + + /// No description provided for @paiementCreateInvoice. + /// + /// In fr, this message translates to: + /// **'Créer une facture'** + String get paiementCreateInvoice; + + /// No description provided for @paiementDecline. + /// + /// In fr, this message translates to: + /// **'Refuser'** + String get paiementDecline; + + /// No description provided for @paiementDeletedSeller. + /// + /// In fr, this message translates to: + /// **'Vendeur supprimé'** + String get paiementDeletedSeller; + + /// No description provided for @paiementDeleteInvoice. + /// + /// In fr, this message translates to: + /// **'Supprimer la facture'** + String get paiementDeleteInvoice; + + /// No description provided for @paiementDeleteSeller. + /// + /// In fr, this message translates to: + /// **'Supprimer le vendeur'** + String get paiementDeleteSeller; + + /// No description provided for @paiementDeleteSellerDescription. + /// + /// In fr, this message translates to: + /// **'Voulez-vous vraiment supprimer ce vendeur ?'** + String get paiementDeleteSellerDescription; + + /// No description provided for @paiementDeleteSuccessfully. + /// + /// In fr, this message translates to: + /// **'Supprimé avec succès'** + String get paiementDeleteSuccessfully; + + /// No description provided for @paiementDeleteStore. + /// + /// In fr, this message translates to: + /// **'Supprimer le magasin'** + String get paiementDeleteStore; + + /// No description provided for @paiementDeleteStoreDescription. + /// + /// In fr, this message translates to: + /// **'Voulez-vous vraiment supprimer ce magasin ?'** + String get paiementDeleteStoreDescription; + + /// No description provided for @paiementDeleteStoreError. + /// + /// In fr, this message translates to: + /// **'Impossible de supprimer le magasin'** + String get paiementDeleteStoreError; + + /// No description provided for @paiementDeletingSellerError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la suppression du vendeur'** + String get paiementDeletingSellerError; + + /// No description provided for @paiementDeviceActivationReceived. + /// + /// In fr, this message translates to: + /// **'La demande d\'activation est prise en compte, veuilliez consulter votre boite mail pour finaliser la démarche'** + String get paiementDeviceActivationReceived; + + /// No description provided for @paiementDeviceNotActivated. + /// + /// In fr, this message translates to: + /// **'Appareil non activé'** + String get paiementDeviceNotActivated; + + /// No description provided for @paiementDeviceNotActivatedDescription. + /// + /// In fr, this message translates to: + /// **'Votre appareil n\'est pas encore activé. \nPour l\'activer, veuillez vous rendre sur la page des appareils.'** + String get paiementDeviceNotActivatedDescription; + + /// No description provided for @paiementDeviceNotRegistered. + /// + /// In fr, this message translates to: + /// **'Appareil non enregistré'** + String get paiementDeviceNotRegistered; + + /// No description provided for @paiementDeviceNotRegisteredDescription. + /// + /// In fr, this message translates to: + /// **'Votre appareil n\'est pas encore enregistré. \nPour l\'enregistrer, veuillez vous rendre sur la page des appareils.'** + String get paiementDeviceNotRegisteredDescription; + + /// No description provided for @paiementDeviceRecoveryError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la récupération de l\'appareil'** + String get paiementDeviceRecoveryError; + + /// No description provided for @paiementDeviceRevoked. + /// + /// In fr, this message translates to: + /// **'Appareil révoqué'** + String get paiementDeviceRevoked; + + /// No description provided for @paiementDeviceRevokingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la révocation de l\'appareil'** + String get paiementDeviceRevokingError; + + /// No description provided for @paiementDevices. + /// + /// In fr, this message translates to: + /// **'Appareils'** + String get paiementDevices; + + /// No description provided for @paiementDoneTransaction. + /// + /// In fr, this message translates to: + /// **'Transaction effectuée'** + String get paiementDoneTransaction; + + /// No description provided for @paiementDownload. + /// + /// In fr, this message translates to: + /// **'Télécharger'** + String get paiementDownload; + + /// Modifier le magasin + /// + /// In fr, this message translates to: + /// **'Modifier le magasin {store}'** + String paiementEditStore(String store); + + /// No description provided for @paiementErrorDeleting. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la suppression'** + String get paiementErrorDeleting; + + /// No description provided for @paiementErrorUpdatingStatus. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la mise à jour du statut'** + String get paiementErrorUpdatingStatus; + + /// Text with a date range + /// + /// In fr, this message translates to: + /// **'Du {from} au {to}'** + String paiementFromTo(DateTime from, DateTime to); + + /// No description provided for @paiementGetBalanceError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la récupération du solde : '** + String get paiementGetBalanceError; + + /// No description provided for @paiementGetTransactionsError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la récupération des transactions : '** + String get paiementGetTransactionsError; + + /// No description provided for @paiementHandOver. + /// + /// In fr, this message translates to: + /// **'Passation'** + String get paiementHandOver; + + /// No description provided for @paiementHistory. + /// + /// In fr, this message translates to: + /// **'Historique'** + String get paiementHistory; + + /// No description provided for @paiementInvoiceCreatedSuccessfully. + /// + /// In fr, this message translates to: + /// **'Facture créée avec succès'** + String get paiementInvoiceCreatedSuccessfully; + + /// No description provided for @paiementInvoices. + /// + /// In fr, this message translates to: + /// **'Factures'** + String get paiementInvoices; + + /// Text with the number of invoices per page + /// + /// In fr, this message translates to: + /// **'{quantity} factures/page'** + String paiementInvoicesPerPage(int quantity); + + /// No description provided for @paiementLastTransactions. + /// + /// In fr, this message translates to: + /// **'Dernières transactions'** + String get paiementLastTransactions; + + /// No description provided for @paiementLimitedTo. + /// + /// In fr, this message translates to: + /// **'Limité à'** + String get paiementLimitedTo; + + /// No description provided for @paiementManagement. + /// + /// In fr, this message translates to: + /// **'Gestion'** + String get paiementManagement; + + /// No description provided for @paiementManageSellers. + /// + /// In fr, this message translates to: + /// **'Gérer les vendeurs'** + String get paiementManageSellers; + + /// No description provided for @paiementMarkPaid. + /// + /// In fr, this message translates to: + /// **'Marquer comme payé'** + String get paiementMarkPaid; + + /// No description provided for @paiementMarkReceived. + /// + /// In fr, this message translates to: + /// **'Marquer comme reçu'** + String get paiementMarkReceived; + + /// No description provided for @paiementMarkUnpaid. + /// + /// In fr, this message translates to: + /// **'Marquer comme non payé'** + String get paiementMarkUnpaid; + + /// No description provided for @paiementMaxAmount. + /// + /// In fr, this message translates to: + /// **'Le montant maximum de votre portefeuille est de'** + String get paiementMaxAmount; + + /// No description provided for @paiementMean. + /// + /// In fr, this message translates to: + /// **'Moyenne : '** + String get paiementMean; + + /// No description provided for @paiementModify. + /// + /// In fr, this message translates to: + /// **'Modifier'** + String get paiementModify; + + /// No description provided for @paiementModifyingStoreError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la modification du magasin'** + String get paiementModifyingStoreError; + + /// No description provided for @paiementModifySuccessfully. + /// + /// In fr, this message translates to: + /// **'Modifié avec succès'** + String get paiementModifySuccessfully; + + /// No description provided for @paiementNewCGU. + /// + /// In fr, this message translates to: + /// **'Nouvelles Conditions Générales d\'Utilisation'** + String get paiementNewCGU; + + /// No description provided for @paiementNext. + /// + /// In fr, this message translates to: + /// **'Suivant'** + String get paiementNext; + + /// No description provided for @paiementNextAccountable. + /// + /// In fr, this message translates to: + /// **'Prochain responsable'** + String get paiementNextAccountable; + + /// No description provided for @paiementNoInvoiceToCreate. + /// + /// In fr, this message translates to: + /// **'Aucune facture à créer'** + String get paiementNoInvoiceToCreate; + + /// No description provided for @paiementNoMembership. + /// + /// In fr, this message translates to: + /// **'Aucune adhésion'** + String get paiementNoMembership; + + /// No description provided for @paiementNoMembershipDescription. + /// + /// In fr, this message translates to: + /// **'Ce produit n\'est pas disponnible pour les non-adhérents. Confirmer l\'encaissement ?'** + String get paiementNoMembershipDescription; + + /// No description provided for @paiementNoThanks. + /// + /// In fr, this message translates to: + /// **'Non merci'** + String get paiementNoThanks; + + /// No description provided for @paiementNoTransaction. + /// + /// In fr, this message translates to: + /// **'Aucune transaction'** + String get paiementNoTransaction; + + /// No description provided for @paiementNoTransactionForThisMonth. + /// + /// In fr, this message translates to: + /// **'Aucune transaction pour ce mois'** + String get paiementNoTransactionForThisMonth; + + /// No description provided for @paiementOf. + /// + /// In fr, this message translates to: + /// **'de'** + String get paiementOf; + + /// No description provided for @paiementPaid. + /// + /// In fr, this message translates to: + /// **'Payé'** + String get paiementPaid; + + /// No description provided for @paiementPay. + /// + /// In fr, this message translates to: + /// **'Payer'** + String get paiementPay; + + /// No description provided for @paiementPayment. + /// + /// In fr, this message translates to: + /// **'Paiement'** + String get paiementPayment; + + /// No description provided for @paiementPayWithHA. + /// + /// In fr, this message translates to: + /// **'Payer avec HelloAsso'** + String get paiementPayWithHA; + + /// No description provided for @paiementPending. + /// + /// In fr, this message translates to: + /// **'En attente'** + String get paiementPending; + + /// No description provided for @paiementPersonalBalance. + /// + /// In fr, this message translates to: + /// **'Solde personnel'** + String get paiementPersonalBalance; + + /// No description provided for @paiementAddFunds. + /// + /// In fr, this message translates to: + /// **'Ajouter des fonds'** + String get paiementAddFunds; + + /// No description provided for @paiementInsufficientFunds. + /// + /// In fr, this message translates to: + /// **'Fonds insuffisants'** + String get paiementInsufficientFunds; + + /// No description provided for @paiementTimeRemaining. + /// + /// In fr, this message translates to: + /// **'Temps restant'** + String get paiementTimeRemaining; + + /// No description provided for @paiementHurryUp. + /// + /// In fr, this message translates to: + /// **'Dépêchez-vous !'** + String get paiementHurryUp; + + /// No description provided for @paiementCompletePayment. + /// + /// In fr, this message translates to: + /// **'Finaliser le paiement'** + String get paiementCompletePayment; + + /// No description provided for @paiementConfirmPayment. + /// + /// In fr, this message translates to: + /// **'Confirmer le paiement'** + String get paiementConfirmPayment; + + /// No description provided for @paiementPleaseAcceptPopup. + /// + /// In fr, this message translates to: + /// **'Veuillez autoriser les popups'** + String get paiementPleaseAcceptPopup; + + /// No description provided for @paiementPleaseAcceptTOS. + /// + /// In fr, this message translates to: + /// **'Veuillez accepter les Conditions Générales d\'Utilisation.'** + String get paiementPleaseAcceptTOS; + + /// No description provided for @paiementPleaseAddDevice. + /// + /// In fr, this message translates to: + /// **'Veuillez ajouter cet appareil pour payer'** + String get paiementPleaseAddDevice; + + /// No description provided for @paiementPleaseAuthenticate. + /// + /// In fr, this message translates to: + /// **'Veuillez vous authentifier'** + String get paiementPleaseAuthenticate; + + /// No description provided for @paiementPleaseEnterMinAmount. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer un montant supérieur à 1'** + String get paiementPleaseEnterMinAmount; + + /// No description provided for @paiementPleaseEnterValidAmount. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer un montant valide'** + String get paiementPleaseEnterValidAmount; + + /// No description provided for @paiementProceedSuccessfully. + /// + /// In fr, this message translates to: + /// **'Paiement effectué avec succès'** + String get paiementProceedSuccessfully; + + /// No description provided for @paiementQRCodeAlreadyUsed. + /// + /// In fr, this message translates to: + /// **'QR Code déjà utilisé'** + String get paiementQRCodeAlreadyUsed; + + /// No description provided for @paiementReactivateRevokedDeviceDescription. + /// + /// In fr, this message translates to: + /// **'Votre appareil a été révoqué. \nPour le réactiver, veuillez vous rendre sur la page des appareils.'** + String get paiementReactivateRevokedDeviceDescription; + + /// No description provided for @paiementReceived. + /// + /// In fr, this message translates to: + /// **'Reçu'** + String get paiementReceived; + + /// No description provided for @paiementRefund. + /// + /// In fr, this message translates to: + /// **'Remboursement'** + String get paiementRefund; + + /// No description provided for @paiementRefundAction. + /// + /// In fr, this message translates to: + /// **'Rembourser'** + String get paiementRefundAction; + + /// No description provided for @paiementRefundedThe. + /// + /// In fr, this message translates to: + /// **'Remboursé le'** + String get paiementRefundedThe; + + /// No description provided for @paiementRevokeDevice. + /// + /// In fr, this message translates to: + /// **'Révoquer l\'appareil ?'** + String get paiementRevokeDevice; + + /// No description provided for @paiementRevokeDeviceDescription. + /// + /// In fr, this message translates to: + /// **'Vous ne pourrez plus utiliser cet appareil pour les paiements'** + String get paiementRevokeDeviceDescription; + + /// No description provided for @paiementRightsOf. + /// + /// In fr, this message translates to: + /// **'Droits de'** + String get paiementRightsOf; + + /// No description provided for @paiementRightsUpdated. + /// + /// In fr, this message translates to: + /// **'Droits mis à jour'** + String get paiementRightsUpdated; + + /// No description provided for @paiementRightsUpdateError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la mise à jour des droits'** + String get paiementRightsUpdateError; + + /// No description provided for @paiementScan. + /// + /// In fr, this message translates to: + /// **'Scanner'** + String get paiementScan; + + /// No description provided for @paiementScanAlreadyUsedQRCode. + /// + /// In fr, this message translates to: + /// **'QR Code déjà utilisé'** + String get paiementScanAlreadyUsedQRCode; + + /// No description provided for @paiementScanCode. + /// + /// In fr, this message translates to: + /// **'Scanner un code'** + String get paiementScanCode; + + /// No description provided for @paiementScanNoMembership. + /// + /// In fr, this message translates to: + /// **'Pas d\'adhésion'** + String get paiementScanNoMembership; + + /// No description provided for @paiementScanNoMembershipConfirmation. + /// + /// In fr, this message translates to: + /// **'Ce produit n\'est pas disponnible pour les non-adhérents. Confirmer l\'encaissement ?'** + String get paiementScanNoMembershipConfirmation; + + /// No description provided for @paiementSeeHistory. + /// + /// In fr, this message translates to: + /// **'Voir l\'historique'** + String get paiementSeeHistory; + + /// No description provided for @paiementSelectStructure. + /// + /// In fr, this message translates to: + /// **'Choisir une structure'** + String get paiementSelectStructure; + + /// No description provided for @paiementSellerError. + /// + /// In fr, this message translates to: + /// **'Vous n\'êtes pas vendeur de ce magasin'** + String get paiementSellerError; + + /// No description provided for @paiementSellerRigths. + /// + /// In fr, this message translates to: + /// **'Droits du vendeur'** + String get paiementSellerRigths; + + /// No description provided for @paiementSellersOf. + /// + /// In fr, this message translates to: + /// **'Les vendeurs de'** + String get paiementSellersOf; + + /// No description provided for @paiementSettings. + /// + /// In fr, this message translates to: + /// **'Paramètres'** + String get paiementSettings; + + /// No description provided for @paiementSpent. + /// + /// In fr, this message translates to: + /// **'Déboursé'** + String get paiementSpent; + + /// No description provided for @paiementStats. + /// + /// In fr, this message translates to: + /// **'Stats'** + String get paiementStats; + + /// No description provided for @paiementStoreBalance. + /// + /// In fr, this message translates to: + /// **'Solde du magasin'** + String get paiementStoreBalance; + + /// No description provided for @paiementStoreDeleted. + /// + /// In fr, this message translates to: + /// **'Magasin supprimée'** + String get paiementStoreDeleted; + + /// Gestion de la structure + /// + /// In fr, this message translates to: + /// **'Gestion de {structure}'** + String paiementStructureManagement(String structure); + + /// No description provided for @paiementStoreName. + /// + /// In fr, this message translates to: + /// **'Nom du magasin'** + String get paiementStoreName; + + /// No description provided for @paiementStores. + /// + /// In fr, this message translates to: + /// **'Magasins'** + String get paiementStores; + + /// No description provided for @paiementStructureAdmin. + /// + /// In fr, this message translates to: + /// **'Administrateur de la structure'** + String get paiementStructureAdmin; + + /// No description provided for @paiementSuccededTransaction. + /// + /// In fr, this message translates to: + /// **'Paiement réussi'** + String get paiementSuccededTransaction; + + /// No description provided for @paiementConfirmYourPurchase. + /// + /// In fr, this message translates to: + /// **'Confirmer votre achat'** + String get paiementConfirmYourPurchase; + + /// No description provided for @paiementYourBalance. + /// + /// In fr, this message translates to: + /// **'Votre solde'** + String get paiementYourBalance; + + /// No description provided for @paiementPaymentSuccessful. + /// + /// In fr, this message translates to: + /// **'Paiement réussi !'** + String get paiementPaymentSuccessful; + + /// No description provided for @paiementPaymentCanceled. + /// + /// In fr, this message translates to: + /// **'Paiement annulé'** + String get paiementPaymentCanceled; + + /// No description provided for @paiementPaymentRequest. + /// + /// In fr, this message translates to: + /// **'Demande de paiement'** + String get paiementPaymentRequest; + + /// No description provided for @paiementPaymentRequestAccepted. + /// + /// In fr, this message translates to: + /// **'Demande de paiement acceptée'** + String get paiementPaymentRequestAccepted; + + /// No description provided for @paiementPaymentRequestRefused. + /// + /// In fr, this message translates to: + /// **'Demande de paiement refusée'** + String get paiementPaymentRequestRefused; + + /// No description provided for @paiementPaymentRequestError. + /// + /// In fr, this message translates to: + /// **'Erreur lors du traitement de la demande'** + String get paiementPaymentRequestError; + + /// No description provided for @paiementRefuse. + /// + /// In fr, this message translates to: + /// **'Refuser'** + String get paiementRefuse; + + /// No description provided for @paiementSuccessfullyAddedStore. + /// + /// In fr, this message translates to: + /// **'Magasin ajoutée avec succès'** + String get paiementSuccessfullyAddedStore; + + /// No description provided for @paiementSuccessfullyModifiedStore. + /// + /// In fr, this message translates to: + /// **'Magasin modifiée avec succès'** + String get paiementSuccessfullyModifiedStore; + + /// No description provided for @paiementThe. + /// + /// In fr, this message translates to: + /// **'Le'** + String get paiementThe; + + /// No description provided for @paiementThisDevice. + /// + /// In fr, this message translates to: + /// **'(cet appareil)'** + String get paiementThisDevice; + + /// No description provided for @paiementTopUp. + /// + /// In fr, this message translates to: + /// **'Recharge'** + String get paiementTopUp; + + /// No description provided for @paiementTopUpAction. + /// + /// In fr, this message translates to: + /// **'Recharger'** + String get paiementTopUpAction; + + /// No description provided for @paiementTotalDuringPeriod. + /// + /// In fr, this message translates to: + /// **'Total sur la période'** + String get paiementTotalDuringPeriod; + + /// No description provided for @paiementTransaction. + /// + /// In fr, this message translates to: + /// **'ransaction'** + String get paiementTransaction; + + /// No description provided for @paiementTransactionCancelled. + /// + /// In fr, this message translates to: + /// **'Transaction annulée'** + String get paiementTransactionCancelled; + + /// No description provided for @paiementTransactionCancelledDescription. + /// + /// In fr, this message translates to: + /// **'Voulez-vous vraiment annuler la transaction de'** + String get paiementTransactionCancelledDescription; + + /// No description provided for @paiementTransactionCancelledError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de l\'annulation de la transaction'** + String get paiementTransactionCancelledError; + + /// No description provided for @paiementTransferStructure. + /// + /// In fr, this message translates to: + /// **'Transfert de structure'** + String get paiementTransferStructure; + + /// No description provided for @paiementTransferStructureDescription. + /// + /// In fr, this message translates to: + /// **'Le nouveau responsable aura accès à toutes les fonctionnalités de gestion de la structure. Vous allez recevoir un email pour valider ce transfert. Le lien ne sera actif que pendant 20 minutes. Cette action est irréversible. Êtes-vous sûr de vouloir continuer ?'** + String get paiementTransferStructureDescription; + + /// No description provided for @paiementTransferStructureError. + /// + /// In fr, this message translates to: + /// **'Erreur lors du transfert de la structure'** + String get paiementTransferStructureError; + + /// No description provided for @paiementTransferStructureSuccess. + /// + /// In fr, this message translates to: + /// **'Transfert de structure demandé avec succès'** + String get paiementTransferStructureSuccess; + + /// No description provided for @paiementUnknownDevice. + /// + /// In fr, this message translates to: + /// **'Appareil inconnu'** + String get paiementUnknownDevice; + + /// No description provided for @paiementValidUntil. + /// + /// In fr, this message translates to: + /// **'Valide jusqu\'à'** + String get paiementValidUntil; + + /// No description provided for @paiementYouAreTransferingStructureTo. + /// + /// In fr, this message translates to: + /// **'Vous êtes sur le point de transférer la structure à '** + String get paiementYouAreTransferingStructureTo; + + /// No description provided for @phAddNewJournal. + /// + /// In fr, this message translates to: + /// **'Ajouter un nouveau journal'** + String get phAddNewJournal; + + /// No description provided for @phNameField. + /// + /// In fr, this message translates to: + /// **'Nom : '** + String get phNameField; + + /// No description provided for @phDateField. + /// + /// In fr, this message translates to: + /// **'Date : '** + String get phDateField; + + /// No description provided for @phDelete. + /// + /// In fr, this message translates to: + /// **'Voulez-vous vraiment supprimer ce journal ?'** + String get phDelete; + + /// No description provided for @phIrreversibleAction. + /// + /// In fr, this message translates to: + /// **'Cette action est irréversible'** + String get phIrreversibleAction; + + /// No description provided for @phToHeavyFile. + /// + /// In fr, this message translates to: + /// **'Fichier trop volumineux'** + String get phToHeavyFile; + + /// No description provided for @phAddPdfFile. + /// + /// In fr, this message translates to: + /// **'Ajouter un fichier PDF'** + String get phAddPdfFile; + + /// No description provided for @phEditPdfFile. + /// + /// In fr, this message translates to: + /// **'Modifier le fichier PDF'** + String get phEditPdfFile; + + /// No description provided for @phPhName. + /// + /// In fr, this message translates to: + /// **'Nom du PH'** + String get phPhName; + + /// No description provided for @phDate. + /// + /// In fr, this message translates to: + /// **'Date'** + String get phDate; + + /// No description provided for @phAdded. + /// + /// In fr, this message translates to: + /// **'Ajouté'** + String get phAdded; + + /// No description provided for @phEdited. + /// + /// In fr, this message translates to: + /// **'Modifié'** + String get phEdited; + + /// No description provided for @phAddingFileError. + /// + /// In fr, this message translates to: + /// **'Erreur d\'ajout'** + String get phAddingFileError; + + /// No description provided for @phMissingInformatonsOrPdf. + /// + /// In fr, this message translates to: + /// **'Informations manquantes ou fichier PDF manquant'** + String get phMissingInformatonsOrPdf; + + /// No description provided for @phAdd. + /// + /// In fr, this message translates to: + /// **'Ajouter'** + String get phAdd; + + /// No description provided for @phEdit. + /// + /// In fr, this message translates to: + /// **'Modifier'** + String get phEdit; + + /// No description provided for @phSeePreviousJournal. + /// + /// In fr, this message translates to: + /// **'Voir les anciens journaux'** + String get phSeePreviousJournal; + + /// No description provided for @phNoJournalInDatabase. + /// + /// In fr, this message translates to: + /// **'Pas encore de PH dans la base de donnée'** + String get phNoJournalInDatabase; + + /// No description provided for @phSuccesDowloading. + /// + /// In fr, this message translates to: + /// **'Téléchargé avec succès'** + String get phSuccesDowloading; + + /// No description provided for @phonebookAdd. + /// + /// In fr, this message translates to: + /// **'Ajouter'** + String get phonebookAdd; + + /// No description provided for @phonebookAddAssociation. + /// + /// In fr, this message translates to: + /// **'Ajouter une association'** + String get phonebookAddAssociation; + + /// No description provided for @phonebookAddAssociationGroupement. + /// + /// In fr, this message translates to: + /// **'Ajouter un groupement d\'association'** + String get phonebookAddAssociationGroupement; + + /// No description provided for @phonebookAddedAssociation. + /// + /// In fr, this message translates to: + /// **'Association ajoutée'** + String get phonebookAddedAssociation; + + /// No description provided for @phonebookAddedMember. + /// + /// In fr, this message translates to: + /// **'Membre ajouté'** + String get phonebookAddedMember; + + /// No description provided for @phonebookAddingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de l\'ajout'** + String get phonebookAddingError; + + /// No description provided for @phonebookAddMember. + /// + /// In fr, this message translates to: + /// **'Ajouter un membre'** + String get phonebookAddMember; + + /// No description provided for @phonebookAddRole. + /// + /// In fr, this message translates to: + /// **'Ajouter un rôle'** + String get phonebookAddRole; + + /// No description provided for @phonebookAdmin. + /// + /// In fr, this message translates to: + /// **'Admin'** + String get phonebookAdmin; + + /// No description provided for @phonebookAll. + /// + /// In fr, this message translates to: + /// **'Toutes'** + String get phonebookAll; + + /// No description provided for @phonebookApparentName. + /// + /// In fr, this message translates to: + /// **'Nom public du rôle :'** + String get phonebookApparentName; + + /// No description provided for @phonebookAssociation. + /// + /// In fr, this message translates to: + /// **'Association'** + String get phonebookAssociation; + + /// No description provided for @phonebookAssociationDetail. + /// + /// In fr, this message translates to: + /// **'Détail de l\'association :'** + String get phonebookAssociationDetail; + + /// No description provided for @phonebookAssociationGroupement. + /// + /// In fr, this message translates to: + /// **'Groupement d\'association'** + String get phonebookAssociationGroupement; + + /// No description provided for @phonebookAssociationKind. + /// + /// In fr, this message translates to: + /// **'Type d\'association :'** + String get phonebookAssociationKind; + + /// No description provided for @phonebookAssociationName. + /// + /// In fr, this message translates to: + /// **'Nom de l\'association'** + String get phonebookAssociationName; + + /// No description provided for @phonebookAssociations. + /// + /// In fr, this message translates to: + /// **'Associations'** + String get phonebookAssociations; + + /// No description provided for @phonebookCancel. + /// + /// In fr, this message translates to: + /// **'Annuler'** + String get phonebookCancel; + + /// Permet de changer le mandat d'une association + /// + /// In fr, this message translates to: + /// **'Passer au mandat {year}'** + String phonebookChangeTermYear(int year); + + /// No description provided for @phonebookChangeTermConfirm. + /// + /// In fr, this message translates to: + /// **'Êtes-vous sûr de vouloir changer tout le mandat ?\nCette action est irréversible !'** + String get phonebookChangeTermConfirm; + + /// No description provided for @phonebookClose. + /// + /// In fr, this message translates to: + /// **'Fermer'** + String get phonebookClose; + + /// No description provided for @phonebookConfirm. + /// + /// In fr, this message translates to: + /// **'Confirmer'** + String get phonebookConfirm; + + /// No description provided for @phonebookCopied. + /// + /// In fr, this message translates to: + /// **'Copié dans le presse-papier'** + String get phonebookCopied; + + /// No description provided for @phonebookDeactivateAssociation. + /// + /// In fr, this message translates to: + /// **'Désactiver l\'association'** + String get phonebookDeactivateAssociation; + + /// No description provided for @phonebookDeactivatedAssociation. + /// + /// In fr, this message translates to: + /// **'Association désactivée'** + String get phonebookDeactivatedAssociation; + + /// No description provided for @phonebookDeactivatedAssociationWarning. + /// + /// In fr, this message translates to: + /// **'Attention, cette association est désactivée, vous ne pouvez pas la modifier'** + String get phonebookDeactivatedAssociationWarning; + + /// Permet de désactiver une association + /// + /// In fr, this message translates to: + /// **'Désactiver l\'association {association} ?'** + String phonebookDeactivateSelectedAssociation(String association); + + /// No description provided for @phonebookDeactivatingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la désactivation'** + String get phonebookDeactivatingError; + + /// No description provided for @phonebookDetail. + /// + /// In fr, this message translates to: + /// **'Détail :'** + String get phonebookDetail; + + /// No description provided for @phonebookDelete. + /// + /// In fr, this message translates to: + /// **'Supprimer'** + String get phonebookDelete; + + /// No description provided for @phonebookDeleteAssociation. + /// + /// In fr, this message translates to: + /// **'Supprimer l\'association'** + String get phonebookDeleteAssociation; + + /// Permet de supprimer une association + /// + /// In fr, this message translates to: + /// **'Supprimer l\'association {association} ?'** + String phonebookDeleteSelectedAssociation(String association); + + /// No description provided for @phonebookDeleteAssociationDescription. + /// + /// In fr, this message translates to: + /// **'Ceci va supprimer l\'historique de l\'association'** + String get phonebookDeleteAssociationDescription; + + /// No description provided for @phonebookDeletedAssociation. + /// + /// In fr, this message translates to: + /// **'Association supprimée'** + String get phonebookDeletedAssociation; + + /// No description provided for @phonebookDeletedMember. + /// + /// In fr, this message translates to: + /// **'Membre supprimé'** + String get phonebookDeletedMember; + + /// No description provided for @phonebookDeleteRole. + /// + /// In fr, this message translates to: + /// **'Supprimer le rôle'** + String get phonebookDeleteRole; + + /// Permet de supprimer le rôle d'un utilisateur dans une association + /// + /// In fr, this message translates to: + /// **'Supprimer le rôle de l\'utilisateur {name} ?'** + String phonebookDeleteUserRole(String name); + + /// No description provided for @phonebookDeactivating. + /// + /// In fr, this message translates to: + /// **'Désactiver l\'association ?'** + String get phonebookDeactivating; + + /// No description provided for @phonebookDeleting. + /// + /// In fr, this message translates to: + /// **'Suppression'** + String get phonebookDeleting; + + /// No description provided for @phonebookDeletingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la suppression'** + String get phonebookDeletingError; + + /// No description provided for @phonebookDescription. + /// + /// In fr, this message translates to: + /// **'Description'** + String get phonebookDescription; + + /// No description provided for @phonebookEdit. + /// + /// In fr, this message translates to: + /// **'Modifier'** + String get phonebookEdit; + + /// No description provided for @phonebookEditAssociationGroupement. + /// + /// In fr, this message translates to: + /// **'Modifier le groupement d\'association'** + String get phonebookEditAssociationGroupement; + + /// No description provided for @phonebookEditAssociationGroups. + /// + /// In fr, this message translates to: + /// **'Gérer les groupes'** + String get phonebookEditAssociationGroups; + + /// No description provided for @phonebookEditAssociationInfo. + /// + /// In fr, this message translates to: + /// **'Modifier'** + String get phonebookEditAssociationInfo; + + /// No description provided for @phonebookEditAssociationMembers. + /// + /// In fr, this message translates to: + /// **'Gérer les membres'** + String get phonebookEditAssociationMembers; + + /// No description provided for @phonebookEditRole. + /// + /// In fr, this message translates to: + /// **'Modifier le rôle'** + String get phonebookEditRole; + + /// No description provided for @phonebookEditMembership. + /// + /// In fr, this message translates to: + /// **'Modifier le rôle'** + String get phonebookEditMembership; + + /// No description provided for @phonebookEmail. + /// + /// In fr, this message translates to: + /// **'Email :'** + String get phonebookEmail; + + /// No description provided for @phonebookEmailCopied. + /// + /// In fr, this message translates to: + /// **'Email copié dans le presse-papier'** + String get phonebookEmailCopied; + + /// No description provided for @phonebookEmptyApparentName. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer un nom de role'** + String get phonebookEmptyApparentName; + + /// No description provided for @phonebookEmptyFieldError. + /// + /// In fr, this message translates to: + /// **'Un champ n\'est pas rempli'** + String get phonebookEmptyFieldError; + + /// No description provided for @phonebookEmptyKindError. + /// + /// In fr, this message translates to: + /// **'Veuillez choisir un type d\'association'** + String get phonebookEmptyKindError; + + /// No description provided for @phonebookEmptyMember. + /// + /// In fr, this message translates to: + /// **'Aucun membre sélectionné'** + String get phonebookEmptyMember; + + /// No description provided for @phonebookErrorAssociationLoading. + /// + /// In fr, this message translates to: + /// **'Erreur lors du chargement de l\'association'** + String get phonebookErrorAssociationLoading; + + /// No description provided for @phonebookErrorAssociationNameEmpty. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer un nom d\'association'** + String get phonebookErrorAssociationNameEmpty; + + /// No description provided for @phonebookErrorAssociationPicture. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la modification de la photo d\'association'** + String get phonebookErrorAssociationPicture; + + /// No description provided for @phonebookErrorKindsLoading. + /// + /// In fr, this message translates to: + /// **'Erreur lors du chargement des types d\'association'** + String get phonebookErrorKindsLoading; + + /// No description provided for @phonebookErrorLoadAssociationList. + /// + /// In fr, this message translates to: + /// **'Erreur lors du chargement de la liste des associations'** + String get phonebookErrorLoadAssociationList; + + /// No description provided for @phonebookErrorLoadAssociationMember. + /// + /// In fr, this message translates to: + /// **'Erreur lors du chargement des membres de l\'association'** + String get phonebookErrorLoadAssociationMember; + + /// No description provided for @phonebookErrorLoadAssociationPicture. + /// + /// In fr, this message translates to: + /// **'Erreur lors du chargement de la photo d\'association'** + String get phonebookErrorLoadAssociationPicture; + + /// No description provided for @phonebookErrorLoadProfilePicture. + /// + /// In fr, this message translates to: + /// **'Erreur'** + String get phonebookErrorLoadProfilePicture; + + /// No description provided for @phonebookErrorRoleTagsLoading. + /// + /// In fr, this message translates to: + /// **'Erreur lors du chargement des tags de rôle'** + String get phonebookErrorRoleTagsLoading; + + /// No description provided for @phonebookExistingMembership. + /// + /// In fr, this message translates to: + /// **'Ce membre est déjà dans le mandat actuel'** + String get phonebookExistingMembership; + + /// No description provided for @phonebookFilter. + /// + /// In fr, this message translates to: + /// **'Filtrer'** + String get phonebookFilter; + + /// No description provided for @phonebookFilterDescription. + /// + /// In fr, this message translates to: + /// **'Filtrer les associations par type'** + String get phonebookFilterDescription; + + /// No description provided for @phonebookFirstname. + /// + /// In fr, this message translates to: + /// **'Prénom :'** + String get phonebookFirstname; + + /// No description provided for @phonebookGroupementDeleted. + /// + /// In fr, this message translates to: + /// **'Groupement d\'association supprimé'** + String get phonebookGroupementDeleted; + + /// No description provided for @phonebookGroupementDeleteError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la suppression du groupement d\'association'** + String get phonebookGroupementDeleteError; + + /// No description provided for @phonebookGroupementName. + /// + /// In fr, this message translates to: + /// **'Nom du groupement'** + String get phonebookGroupementName; + + /// Permet de gérer les groupes d'une association + /// + /// In fr, this message translates to: + /// **'Gérer les groupes de {association}'** + String phonebookGroups(String association); + + /// Année de mandat d'une association + /// + /// In fr, this message translates to: + /// **'Mandat {year}'** + String phonebookTerm(int year); + + /// No description provided for @phonebookTermChangingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors du changement de mandat'** + String get phonebookTermChangingError; + + /// No description provided for @phonebookMember. + /// + /// In fr, this message translates to: + /// **'Membre'** + String get phonebookMember; + + /// No description provided for @phonebookMemberReordered. + /// + /// In fr, this message translates to: + /// **'Membre réordonné'** + String get phonebookMemberReordered; + + /// Permet de gérer les membres d'une association + /// + /// In fr, this message translates to: + /// **'Gérer les membres de {association}'** + String phonebookMembers(String association); + + /// No description provided for @phonebookMembershipAssociationError. + /// + /// In fr, this message translates to: + /// **'Veuillez choisir une association'** + String get phonebookMembershipAssociationError; + + /// No description provided for @phonebookMembershipRole. + /// + /// In fr, this message translates to: + /// **'Rôle :'** + String get phonebookMembershipRole; + + /// No description provided for @phonebookMembershipRoleError. + /// + /// In fr, this message translates to: + /// **'Veuillez choisir un rôle'** + String get phonebookMembershipRoleError; + + /// Permet de modifier le rôle d'un membre dans une association + /// + /// In fr, this message translates to: + /// **'Modifier le rôle de {name}'** + String phonebookModifyMembership(String name); + + /// No description provided for @phonebookName. + /// + /// In fr, this message translates to: + /// **'Nom :'** + String get phonebookName; + + /// No description provided for @phonebookNameCopied. + /// + /// In fr, this message translates to: + /// **'Nom et prénom copié dans le presse-papier'** + String get phonebookNameCopied; + + /// No description provided for @phonebookNamePure. + /// + /// In fr, this message translates to: + /// **'Nom'** + String get phonebookNamePure; + + /// No description provided for @phonebookNewTerm. + /// + /// In fr, this message translates to: + /// **'Nouveau mandat'** + String get phonebookNewTerm; + + /// No description provided for @phonebookNewTermConfirmed. + /// + /// In fr, this message translates to: + /// **'Mandat changé'** + String get phonebookNewTermConfirmed; + + /// No description provided for @phonebookNickname. + /// + /// In fr, this message translates to: + /// **'Surnom :'** + String get phonebookNickname; + + /// No description provided for @phonebookNicknameCopied. + /// + /// In fr, this message translates to: + /// **'Surnom copié dans le presse-papier'** + String get phonebookNicknameCopied; + + /// No description provided for @phonebookNoAssociationFound. + /// + /// In fr, this message translates to: + /// **'Aucune association trouvée'** + String get phonebookNoAssociationFound; + + /// No description provided for @phonebookNoMember. + /// + /// In fr, this message translates to: + /// **'Aucun membre'** + String get phonebookNoMember; + + /// No description provided for @phonebookNoMemberRole. + /// + /// In fr, this message translates to: + /// **'Aucun role trouvé'** + String get phonebookNoMemberRole; + + /// No description provided for @phonebookNoRoleTags. + /// + /// In fr, this message translates to: + /// **'Aucun tag de rôle trouvé'** + String get phonebookNoRoleTags; + + /// No description provided for @phonebookPhone. + /// + /// In fr, this message translates to: + /// **'Téléphone :'** + String get phonebookPhone; + + /// No description provided for @phonebookPhonebook. + /// + /// In fr, this message translates to: + /// **'Annuaire'** + String get phonebookPhonebook; + + /// No description provided for @phonebookPhonebookSearch. + /// + /// In fr, this message translates to: + /// **'Rechercher'** + String get phonebookPhonebookSearch; + + /// No description provided for @phonebookPhonebookSearchAssociation. + /// + /// In fr, this message translates to: + /// **'Association'** + String get phonebookPhonebookSearchAssociation; + + /// No description provided for @phonebookPhonebookSearchField. + /// + /// In fr, this message translates to: + /// **'Rechercher :'** + String get phonebookPhonebookSearchField; + + /// No description provided for @phonebookPhonebookSearchName. + /// + /// In fr, this message translates to: + /// **'Nom/Prénom/Surnom'** + String get phonebookPhonebookSearchName; + + /// No description provided for @phonebookPhonebookSearchRole. + /// + /// In fr, this message translates to: + /// **'Poste'** + String get phonebookPhonebookSearchRole; + + /// No description provided for @phonebookPresidentRoleTag. + /// + /// In fr, this message translates to: + /// **'Prez\''** + String get phonebookPresidentRoleTag; + + /// No description provided for @phonebookPromoNotGiven. + /// + /// In fr, this message translates to: + /// **'Promo non renseignée'** + String get phonebookPromoNotGiven; + + /// Année de promotion d'un membre + /// + /// In fr, this message translates to: + /// **'Promotion {year}'** + String phonebookPromotion(int year); + + /// No description provided for @phonebookReorderingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors du réordonnement'** + String get phonebookReorderingError; + + /// No description provided for @phonebookResearch. + /// + /// In fr, this message translates to: + /// **'Rechercher'** + String get phonebookResearch; + + /// No description provided for @phonebookRolePure. + /// + /// In fr, this message translates to: + /// **'Rôle'** + String get phonebookRolePure; + + /// No description provided for @phonebookSearchUser. + /// + /// In fr, this message translates to: + /// **'Rechercher un utilisateur'** + String get phonebookSearchUser; + + /// No description provided for @phonebookTooHeavyAssociationPicture. + /// + /// In fr, this message translates to: + /// **'L\'image est trop lourde (max 4Mo)'** + String get phonebookTooHeavyAssociationPicture; + + /// No description provided for @phonebookUpdateGroups. + /// + /// In fr, this message translates to: + /// **'Mettre à jour les groupes'** + String get phonebookUpdateGroups; + + /// No description provided for @phonebookUpdatedAssociation. + /// + /// In fr, this message translates to: + /// **'Association modifiée'** + String get phonebookUpdatedAssociation; + + /// No description provided for @phonebookUpdatedAssociationPicture. + /// + /// In fr, this message translates to: + /// **'La photo d\'association a été changée'** + String get phonebookUpdatedAssociationPicture; + + /// No description provided for @phonebookUpdatedGroups. + /// + /// In fr, this message translates to: + /// **'Groupes mis à jour'** + String get phonebookUpdatedGroups; + + /// No description provided for @phonebookUpdatedMember. + /// + /// In fr, this message translates to: + /// **'Membre modifié'** + String get phonebookUpdatedMember; + + /// No description provided for @phonebookUpdatingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la modification'** + String get phonebookUpdatingError; + + /// No description provided for @phonebookValidation. + /// + /// In fr, this message translates to: + /// **'Valider'** + String get phonebookValidation; + + /// No description provided for @purchasesPurchases. + /// + /// In fr, this message translates to: + /// **'Achats'** + String get purchasesPurchases; + + /// No description provided for @purchasesResearch. + /// + /// In fr, this message translates to: + /// **'Rechercher'** + String get purchasesResearch; + + /// No description provided for @purchasesNoPurchasesFound. + /// + /// In fr, this message translates to: + /// **'Aucun achat trouvé'** + String get purchasesNoPurchasesFound; + + /// No description provided for @purchasesNoTickets. + /// + /// In fr, this message translates to: + /// **'Aucun ticket'** + String get purchasesNoTickets; + + /// No description provided for @purchasesTicketsError. + /// + /// In fr, this message translates to: + /// **'Erreur lors du chargement des tickets'** + String get purchasesTicketsError; + + /// No description provided for @purchasesPurchasesError. + /// + /// In fr, this message translates to: + /// **'Erreur lors du chargement des achats'** + String get purchasesPurchasesError; + + /// No description provided for @purchasesNoPurchases. + /// + /// In fr, this message translates to: + /// **'Aucun achat'** + String get purchasesNoPurchases; + + /// No description provided for @purchasesTimes. + /// + /// In fr, this message translates to: + /// **'fois'** + String get purchasesTimes; + + /// No description provided for @purchasesAlreadyUsed. + /// + /// In fr, this message translates to: + /// **'Déjà utilisé'** + String get purchasesAlreadyUsed; + + /// No description provided for @purchasesNotPaid. + /// + /// In fr, this message translates to: + /// **'Non validé'** + String get purchasesNotPaid; + + /// No description provided for @purchasesPleaseSelectProduct. + /// + /// In fr, this message translates to: + /// **'Veuillez sélectionner un produit'** + String get purchasesPleaseSelectProduct; + + /// No description provided for @purchasesProducts. + /// + /// In fr, this message translates to: + /// **'Produits'** + String get purchasesProducts; + + /// No description provided for @purchasesCancel. + /// + /// In fr, this message translates to: + /// **'Annuler'** + String get purchasesCancel; + + /// No description provided for @purchasesValidate. + /// + /// In fr, this message translates to: + /// **'Valider'** + String get purchasesValidate; + + /// No description provided for @purchasesLeftScan. + /// + /// In fr, this message translates to: + /// **'Scans restants'** + String get purchasesLeftScan; + + /// No description provided for @purchasesTag. + /// + /// In fr, this message translates to: + /// **'Tag'** + String get purchasesTag; + + /// No description provided for @purchasesHistory. + /// + /// In fr, this message translates to: + /// **'Historique'** + String get purchasesHistory; + + /// No description provided for @purchasesPleaseSelectSeller. + /// + /// In fr, this message translates to: + /// **'Veuillez sélectionner un vendeur'** + String get purchasesPleaseSelectSeller; + + /// No description provided for @purchasesNoTagGiven. + /// + /// In fr, this message translates to: + /// **'Attention, aucun tag n\'a été entré'** + String get purchasesNoTagGiven; + + /// No description provided for @purchasesTickets. + /// + /// In fr, this message translates to: + /// **'Tickets'** + String get purchasesTickets; + + /// No description provided for @purchasesNoScannableProducts. + /// + /// In fr, this message translates to: + /// **'Aucun produit scannable'** + String get purchasesNoScannableProducts; + + /// No description provided for @purchasesLoading. + /// + /// In fr, this message translates to: + /// **'En attente de scan'** + String get purchasesLoading; + + /// No description provided for @purchasesScan. + /// + /// In fr, this message translates to: + /// **'Scanner'** + String get purchasesScan; + + /// No description provided for @raffleRaffle. + /// + /// In fr, this message translates to: + /// **'Tombola'** + String get raffleRaffle; + + /// No description provided for @rafflePrize. + /// + /// In fr, this message translates to: + /// **'Lot'** + String get rafflePrize; + + /// No description provided for @rafflePrizes. + /// + /// In fr, this message translates to: + /// **'Lots'** + String get rafflePrizes; + + /// No description provided for @raffleActualRaffles. + /// + /// In fr, this message translates to: + /// **'Tombola en cours'** + String get raffleActualRaffles; + + /// No description provided for @rafflePastRaffles. + /// + /// In fr, this message translates to: + /// **'Tombola passés'** + String get rafflePastRaffles; + + /// No description provided for @raffleYourTickets. + /// + /// In fr, this message translates to: + /// **'Tous vos tickets'** + String get raffleYourTickets; + + /// No description provided for @raffleCreateMenu. + /// + /// In fr, this message translates to: + /// **'Menu de Création'** + String get raffleCreateMenu; + + /// No description provided for @raffleNextRaffles. + /// + /// In fr, this message translates to: + /// **'Prochaines tombolas'** + String get raffleNextRaffles; + + /// No description provided for @raffleNoTicket. + /// + /// In fr, this message translates to: + /// **'Vous n\'avez pas de ticket'** + String get raffleNoTicket; + + /// No description provided for @raffleSeeRaffleDetail. + /// + /// In fr, this message translates to: + /// **'Voir lots/tickets'** + String get raffleSeeRaffleDetail; + + /// No description provided for @raffleActualPrize. + /// + /// In fr, this message translates to: + /// **'Lots actuels'** + String get raffleActualPrize; + + /// No description provided for @raffleMajorPrize. + /// + /// In fr, this message translates to: + /// **'Lot Majeurs'** + String get raffleMajorPrize; + + /// No description provided for @raffleTakeTickets. + /// + /// In fr, this message translates to: + /// **'Prendre vos tickets'** + String get raffleTakeTickets; + + /// No description provided for @raffleNoTicketBuyable. + /// + /// In fr, this message translates to: + /// **'Vous ne pouvez pas achetez de billets pour l\'instant'** + String get raffleNoTicketBuyable; + + /// No description provided for @raffleNoCurrentPrize. + /// + /// In fr, this message translates to: + /// **'Il n\'y a aucun lots actuellement'** + String get raffleNoCurrentPrize; + + /// No description provided for @raffleModifTombola. + /// + /// In fr, this message translates to: + /// **'Vous pouvez modifiez vos tombolas ou en créer de nouvelles, toute décision doit ensuite être prise par les admins'** + String get raffleModifTombola; + + /// No description provided for @raffleCreateYourRaffle. + /// + /// In fr, this message translates to: + /// **'Votre menu de création de tombolas'** + String get raffleCreateYourRaffle; + + /// No description provided for @rafflePossiblePrice. + /// + /// In fr, this message translates to: + /// **'Prix possible'** + String get rafflePossiblePrice; + + /// No description provided for @raffleInformation. + /// + /// In fr, this message translates to: + /// **'Information et Statistiques'** + String get raffleInformation; + + /// No description provided for @raffleAccounts. + /// + /// In fr, this message translates to: + /// **'Comptes'** + String get raffleAccounts; + + /// No description provided for @raffleAdd. + /// + /// In fr, this message translates to: + /// **'Ajouter'** + String get raffleAdd; + + /// No description provided for @raffleUpdatedAmount. + /// + /// In fr, this message translates to: + /// **'Montant mis à jour'** + String get raffleUpdatedAmount; + + /// No description provided for @raffleUpdatingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la mise à jour'** + String get raffleUpdatingError; + + /// No description provided for @raffleDeletedPrize. + /// + /// In fr, this message translates to: + /// **'Lot supprimé'** + String get raffleDeletedPrize; + + /// No description provided for @raffleDeletingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la suppression'** + String get raffleDeletingError; + + /// No description provided for @raffleQuantity. + /// + /// In fr, this message translates to: + /// **'Quantité'** + String get raffleQuantity; + + /// No description provided for @raffleClose. + /// + /// In fr, this message translates to: + /// **'Fermer'** + String get raffleClose; + + /// No description provided for @raffleOpen. + /// + /// In fr, this message translates to: + /// **'Ouvrir'** + String get raffleOpen; + + /// No description provided for @raffleAddTypeTicketSimple. + /// + /// In fr, this message translates to: + /// **'Ajouter'** + String get raffleAddTypeTicketSimple; + + /// No description provided for @raffleAddingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de l\'ajout'** + String get raffleAddingError; + + /// No description provided for @raffleEditTypeTicketSimple. + /// + /// In fr, this message translates to: + /// **'Modifier'** + String get raffleEditTypeTicketSimple; + + /// No description provided for @raffleFillField. + /// + /// In fr, this message translates to: + /// **'Le champ ne peut pas être vide'** + String get raffleFillField; + + /// No description provided for @raffleWaiting. + /// + /// In fr, this message translates to: + /// **'Chargement'** + String get raffleWaiting; + + /// No description provided for @raffleEditingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la modification'** + String get raffleEditingError; + + /// No description provided for @raffleAddedTicket. + /// + /// In fr, this message translates to: + /// **'Ticket ajouté'** + String get raffleAddedTicket; + + /// No description provided for @raffleEditedTicket. + /// + /// In fr, this message translates to: + /// **'Ticket modifié'** + String get raffleEditedTicket; + + /// No description provided for @raffleAlreadyExistTicket. + /// + /// In fr, this message translates to: + /// **'Le ticket existe déjà'** + String get raffleAlreadyExistTicket; + + /// No description provided for @raffleNumberExpected. + /// + /// In fr, this message translates to: + /// **'Un entier est attendu'** + String get raffleNumberExpected; + + /// No description provided for @raffleDeletedTicket. + /// + /// In fr, this message translates to: + /// **'Ticket supprimé'** + String get raffleDeletedTicket; + + /// No description provided for @raffleAddPrize. + /// + /// In fr, this message translates to: + /// **'Ajouter'** + String get raffleAddPrize; + + /// No description provided for @raffleEditPrize. + /// + /// In fr, this message translates to: + /// **'Modifier'** + String get raffleEditPrize; + + /// No description provided for @raffleOpenRaffle. + /// + /// In fr, this message translates to: + /// **'Ouvrir la tombola'** + String get raffleOpenRaffle; + + /// No description provided for @raffleCloseRaffle. + /// + /// In fr, this message translates to: + /// **'Fermer la tombola'** + String get raffleCloseRaffle; + + /// No description provided for @raffleOpenRaffleDescription. + /// + /// In fr, this message translates to: + /// **'Vous allez ouvrir la tombola, les utilisateurs pourront acheter des tickets. Vous ne pourrez plus modifier la tombola. Êtes-vous sûr de vouloir continuer ?'** + String get raffleOpenRaffleDescription; + + /// No description provided for @raffleCloseRaffleDescription. + /// + /// In fr, this message translates to: + /// **'Vous allez fermer la tombola, les utilisateurs ne pourront plus acheter de tickets. Êtes-vous sûr de vouloir continuer ?'** + String get raffleCloseRaffleDescription; + + /// No description provided for @raffleNoCurrentRaffle. + /// + /// In fr, this message translates to: + /// **'Il n\'y a aucune tombola en cours'** + String get raffleNoCurrentRaffle; + + /// No description provided for @raffleBoughtTicket. + /// + /// In fr, this message translates to: + /// **'Ticket acheté'** + String get raffleBoughtTicket; + + /// No description provided for @raffleDrawingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors du tirage'** + String get raffleDrawingError; + + /// No description provided for @raffleInvalidPrice. + /// + /// In fr, this message translates to: + /// **'Le prix doit être supérieur à 0'** + String get raffleInvalidPrice; + + /// No description provided for @raffleMustBePositive. + /// + /// In fr, this message translates to: + /// **'Le nombre doit être strictement positif'** + String get raffleMustBePositive; + + /// No description provided for @raffleDraw. + /// + /// In fr, this message translates to: + /// **'Tirer'** + String get raffleDraw; + + /// No description provided for @raffleDrawn. + /// + /// In fr, this message translates to: + /// **'Tiré'** + String get raffleDrawn; + + /// No description provided for @raffleError. + /// + /// In fr, this message translates to: + /// **'Erreur'** + String get raffleError; + + /// No description provided for @raffleGathered. + /// + /// In fr, this message translates to: + /// **'Récolté'** + String get raffleGathered; + + /// No description provided for @raffleTickets. + /// + /// In fr, this message translates to: + /// **'Tickets'** + String get raffleTickets; + + /// No description provided for @raffleTicket. + /// + /// In fr, this message translates to: + /// **'ticket'** + String get raffleTicket; + + /// No description provided for @raffleWinner. + /// + /// In fr, this message translates to: + /// **'Gagnant'** + String get raffleWinner; + + /// No description provided for @raffleNoPrize. + /// + /// In fr, this message translates to: + /// **'Aucun lot'** + String get raffleNoPrize; + + /// No description provided for @raffleDeletePrize. + /// + /// In fr, this message translates to: + /// **'Supprimer le lot'** + String get raffleDeletePrize; + + /// No description provided for @raffleDeletePrizeDescription. + /// + /// In fr, this message translates to: + /// **'Vous allez supprimer le lot, êtes-vous sûr de vouloir continuer ?'** + String get raffleDeletePrizeDescription; + + /// No description provided for @raffleDrawing. + /// + /// In fr, this message translates to: + /// **'Tirage'** + String get raffleDrawing; + + /// No description provided for @raffleDrawingDescription. + /// + /// In fr, this message translates to: + /// **'Tirer le gagnant du lot ?'** + String get raffleDrawingDescription; + + /// No description provided for @raffleDeleteTicket. + /// + /// In fr, this message translates to: + /// **'Supprimer le ticket'** + String get raffleDeleteTicket; + + /// No description provided for @raffleDeleteTicketDescription. + /// + /// In fr, this message translates to: + /// **'Vous allez supprimer le ticket, êtes-vous sûr de vouloir continuer ?'** + String get raffleDeleteTicketDescription; + + /// No description provided for @raffleWinningTickets. + /// + /// In fr, this message translates to: + /// **'Tickets gagnants'** + String get raffleWinningTickets; + + /// No description provided for @raffleNoWinningTicketYet. + /// + /// In fr, this message translates to: + /// **'Les tickets gagnants seront affichés ici'** + String get raffleNoWinningTicketYet; + + /// No description provided for @raffleName. + /// + /// In fr, this message translates to: + /// **'Nom'** + String get raffleName; + + /// No description provided for @raffleDescription. + /// + /// In fr, this message translates to: + /// **'Description'** + String get raffleDescription; + + /// No description provided for @raffleBuyThisTicket. + /// + /// In fr, this message translates to: + /// **'Acheter ce ticket'** + String get raffleBuyThisTicket; + + /// No description provided for @raffleLockedRaffle. + /// + /// In fr, this message translates to: + /// **'Tombola verrouillée'** + String get raffleLockedRaffle; + + /// No description provided for @raffleUnavailableRaffle. + /// + /// In fr, this message translates to: + /// **'Tombola indisponible'** + String get raffleUnavailableRaffle; + + /// No description provided for @raffleNotEnoughMoney. + /// + /// In fr, this message translates to: + /// **'Vous n\'avez pas assez d\'argent'** + String get raffleNotEnoughMoney; + + /// No description provided for @raffleWinnable. + /// + /// In fr, this message translates to: + /// **'gagnable'** + String get raffleWinnable; + + /// No description provided for @raffleNoDescription. + /// + /// In fr, this message translates to: + /// **'Aucune description'** + String get raffleNoDescription; + + /// No description provided for @raffleAmount. + /// + /// In fr, this message translates to: + /// **'Solde'** + String get raffleAmount; + + /// No description provided for @raffleLoading. + /// + /// In fr, this message translates to: + /// **'Chargement'** + String get raffleLoading; + + /// No description provided for @raffleTicketNumber. + /// + /// In fr, this message translates to: + /// **'Nombre de ticket'** + String get raffleTicketNumber; + + /// No description provided for @rafflePrice. + /// + /// In fr, this message translates to: + /// **'Prix'** + String get rafflePrice; + + /// No description provided for @raffleEditRaffle. + /// + /// In fr, this message translates to: + /// **'Modifier la tombola'** + String get raffleEditRaffle; + + /// No description provided for @raffleEdit. + /// + /// In fr, this message translates to: + /// **'Modifier'** + String get raffleEdit; + + /// No description provided for @raffleAddPackTicket. + /// + /// In fr, this message translates to: + /// **'Ajouter un pack de ticket'** + String get raffleAddPackTicket; + + /// No description provided for @recommendationRecommendation. + /// + /// In fr, this message translates to: + /// **'Bons plans'** + String get recommendationRecommendation; + + /// No description provided for @recommendationTitle. + /// + /// In fr, this message translates to: + /// **'Titre'** + String get recommendationTitle; + + /// No description provided for @recommendationLogo. + /// + /// In fr, this message translates to: + /// **'Logo'** + String get recommendationLogo; + + /// No description provided for @recommendationCode. + /// + /// In fr, this message translates to: + /// **'Code'** + String get recommendationCode; + + /// No description provided for @recommendationSummary. + /// + /// In fr, this message translates to: + /// **'Court résumé'** + String get recommendationSummary; + + /// No description provided for @recommendationDescription. + /// + /// In fr, this message translates to: + /// **'Description'** + String get recommendationDescription; + + /// No description provided for @recommendationAdd. + /// + /// In fr, this message translates to: + /// **'Ajouter'** + String get recommendationAdd; + + /// No description provided for @recommendationEdit. + /// + /// In fr, this message translates to: + /// **'Modifier'** + String get recommendationEdit; + + /// No description provided for @recommendationDelete. + /// + /// In fr, this message translates to: + /// **'Supprimer'** + String get recommendationDelete; + + /// No description provided for @recommendationAddImage. + /// + /// In fr, this message translates to: + /// **'Veuillez ajouter une image'** + String get recommendationAddImage; + + /// No description provided for @recommendationAddedRecommendation. + /// + /// In fr, this message translates to: + /// **'Bon plan ajouté'** + String get recommendationAddedRecommendation; + + /// No description provided for @recommendationEditedRecommendation. + /// + /// In fr, this message translates to: + /// **'Bon plan modifié'** + String get recommendationEditedRecommendation; + + /// No description provided for @recommendationDeleteRecommendationConfirmation. + /// + /// In fr, this message translates to: + /// **'Êtes-vous sûr de vouloir supprimer ce bon plan ?'** + String get recommendationDeleteRecommendationConfirmation; + + /// No description provided for @recommendationDeleteRecommendation. + /// + /// In fr, this message translates to: + /// **'Suppresion'** + String get recommendationDeleteRecommendation; + + /// No description provided for @recommendationDeletingRecommendationError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la suppression'** + String get recommendationDeletingRecommendationError; + + /// No description provided for @recommendationDeletedRecommendation. + /// + /// In fr, this message translates to: + /// **'Bon plan supprimé'** + String get recommendationDeletedRecommendation; + + /// No description provided for @recommendationIncorrectOrMissingFields. + /// + /// In fr, this message translates to: + /// **'Champs incorrects ou manquants'** + String get recommendationIncorrectOrMissingFields; + + /// No description provided for @recommendationEditingError. + /// + /// In fr, this message translates to: + /// **'Échec de la modification'** + String get recommendationEditingError; + + /// No description provided for @recommendationAddingError. + /// + /// In fr, this message translates to: + /// **'Échec de l\'ajout'** + String get recommendationAddingError; + + /// No description provided for @recommendationCopiedCode. + /// + /// In fr, this message translates to: + /// **'Code de réduction copié'** + String get recommendationCopiedCode; + + /// No description provided for @seedLibraryAdd. + /// + /// In fr, this message translates to: + /// **'Ajouter'** + String get seedLibraryAdd; + + /// No description provided for @seedLibraryAddedPlant. + /// + /// In fr, this message translates to: + /// **'Plante ajoutée'** + String get seedLibraryAddedPlant; + + /// No description provided for @seedLibraryAddedSpecies. + /// + /// In fr, this message translates to: + /// **'Espèce ajoutée'** + String get seedLibraryAddedSpecies; + + /// No description provided for @seedLibraryAddingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de l\'ajout'** + String get seedLibraryAddingError; + + /// No description provided for @seedLibraryAddPlant. + /// + /// In fr, this message translates to: + /// **'Déposer une plante'** + String get seedLibraryAddPlant; + + /// No description provided for @seedLibraryAddSpecies. + /// + /// In fr, this message translates to: + /// **'Ajouter une espèce'** + String get seedLibraryAddSpecies; + + /// No description provided for @seedLibraryAll. + /// + /// In fr, this message translates to: + /// **'Toutes'** + String get seedLibraryAll; + + /// No description provided for @seedLibraryAncestor. + /// + /// In fr, this message translates to: + /// **'Ancêtre'** + String get seedLibraryAncestor; + + /// No description provided for @seedLibraryAround. + /// + /// In fr, this message translates to: + /// **'environ'** + String get seedLibraryAround; + + /// No description provided for @seedLibraryAutumn. + /// + /// In fr, this message translates to: + /// **'Automne'** + String get seedLibraryAutumn; + + /// No description provided for @seedLibraryBorrowedPlant. + /// + /// In fr, this message translates to: + /// **'Plante empruntée'** + String get seedLibraryBorrowedPlant; + + /// No description provided for @seedLibraryBorrowingDate. + /// + /// In fr, this message translates to: + /// **'Date d\'emprunt :'** + String get seedLibraryBorrowingDate; + + /// No description provided for @seedLibraryBorrowPlant. + /// + /// In fr, this message translates to: + /// **'Emprunter la plante'** + String get seedLibraryBorrowPlant; + + /// No description provided for @seedLibraryCard. + /// + /// In fr, this message translates to: + /// **'Carte'** + String get seedLibraryCard; + + /// No description provided for @seedLibraryChoosingAncestor. + /// + /// In fr, this message translates to: + /// **'Veuillez choisir un ancêtre'** + String get seedLibraryChoosingAncestor; + + /// No description provided for @seedLibraryChoosingSpecies. + /// + /// In fr, this message translates to: + /// **'Veuillez choisir une espèce'** + String get seedLibraryChoosingSpecies; + + /// No description provided for @seedLibraryChoosingSpeciesOrAncestor. + /// + /// In fr, this message translates to: + /// **'Veuillez choisir une espèce ou un ancêtre'** + String get seedLibraryChoosingSpeciesOrAncestor; + + /// No description provided for @seedLibraryContact. + /// + /// In fr, this message translates to: + /// **'Contact :'** + String get seedLibraryContact; + + /// No description provided for @seedLibraryDays. + /// + /// In fr, this message translates to: + /// **'jours'** + String get seedLibraryDays; + + /// No description provided for @seedLibraryDeadMsg. + /// + /// In fr, this message translates to: + /// **'Voulez-vous déclarer la plante morte ?'** + String get seedLibraryDeadMsg; + + /// No description provided for @seedLibraryDeadPlant. + /// + /// In fr, this message translates to: + /// **'Plante morte'** + String get seedLibraryDeadPlant; + + /// No description provided for @seedLibraryDeathDate. + /// + /// In fr, this message translates to: + /// **'Date de mort'** + String get seedLibraryDeathDate; + + /// No description provided for @seedLibraryDeletedSpecies. + /// + /// In fr, this message translates to: + /// **'Espèce supprimée'** + String get seedLibraryDeletedSpecies; + + /// No description provided for @seedLibraryDeleteSpecies. + /// + /// In fr, this message translates to: + /// **'Supprimer l\'espèce ?'** + String get seedLibraryDeleteSpecies; + + /// No description provided for @seedLibraryDeleting. + /// + /// In fr, this message translates to: + /// **'Suppression'** + String get seedLibraryDeleting; + + /// No description provided for @seedLibraryDeletingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la suppression'** + String get seedLibraryDeletingError; + + /// No description provided for @seedLibraryDepositNotAvailable. + /// + /// In fr, this message translates to: + /// **'Le dépôt de plantes n\'est pas possible sans emprunter une plante au préalable'** + String get seedLibraryDepositNotAvailable; + + /// No description provided for @seedLibraryDescription. + /// + /// In fr, this message translates to: + /// **'Description'** + String get seedLibraryDescription; + + /// No description provided for @seedLibraryDifficulty. + /// + /// In fr, this message translates to: + /// **'Difficulté :'** + String get seedLibraryDifficulty; + + /// No description provided for @seedLibraryEdit. + /// + /// In fr, this message translates to: + /// **'Modifier'** + String get seedLibraryEdit; + + /// No description provided for @seedLibraryEditedPlant. + /// + /// In fr, this message translates to: + /// **'Plante modifiée'** + String get seedLibraryEditedPlant; + + /// No description provided for @seedLibraryEditInformation. + /// + /// In fr, this message translates to: + /// **'Modifier les informations'** + String get seedLibraryEditInformation; + + /// No description provided for @seedLibraryEditingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la modification'** + String get seedLibraryEditingError; + + /// No description provided for @seedLibraryEditSpecies. + /// + /// In fr, this message translates to: + /// **'Modifier l\'espèce'** + String get seedLibraryEditSpecies; + + /// No description provided for @seedLibraryEmptyDifficultyError. + /// + /// In fr, this message translates to: + /// **'Veuillez choisir une difficulté'** + String get seedLibraryEmptyDifficultyError; + + /// No description provided for @seedLibraryEmptyFieldError. + /// + /// In fr, this message translates to: + /// **'Veuillez remplir tous les champs'** + String get seedLibraryEmptyFieldError; + + /// No description provided for @seedLibraryEmptyTypeError. + /// + /// In fr, this message translates to: + /// **'Veuillez choisir un type de plante'** + String get seedLibraryEmptyTypeError; + + /// No description provided for @seedLibraryEndMonth. + /// + /// In fr, this message translates to: + /// **'Mois de fin :'** + String get seedLibraryEndMonth; + + /// No description provided for @seedLibraryFacebookUrl. + /// + /// In fr, this message translates to: + /// **'Lien Facebook'** + String get seedLibraryFacebookUrl; + + /// No description provided for @seedLibraryFilters. + /// + /// In fr, this message translates to: + /// **'Filtres'** + String get seedLibraryFilters; + + /// No description provided for @seedLibraryForum. + /// + /// In fr, this message translates to: + /// **'Oskour maman j\'ai tué ma plante - Forum d\'aide'** + String get seedLibraryForum; + + /// No description provided for @seedLibraryForumUrl. + /// + /// In fr, this message translates to: + /// **'Lien Forum'** + String get seedLibraryForumUrl; + + /// No description provided for @seedLibraryHelpSheets. + /// + /// In fr, this message translates to: + /// **'Fiches sur les plantes'** + String get seedLibraryHelpSheets; + + /// No description provided for @seedLibraryInformation. + /// + /// In fr, this message translates to: + /// **'Informations :'** + String get seedLibraryInformation; + + /// No description provided for @seedLibraryMaturationTime. + /// + /// In fr, this message translates to: + /// **'Temps de maturation'** + String get seedLibraryMaturationTime; + + /// No description provided for @seedLibraryMonthJan. + /// + /// In fr, this message translates to: + /// **'Janvier'** + String get seedLibraryMonthJan; + + /// No description provided for @seedLibraryMonthFeb. + /// + /// In fr, this message translates to: + /// **'Février'** + String get seedLibraryMonthFeb; + + /// No description provided for @seedLibraryMonthMar. + /// + /// In fr, this message translates to: + /// **'Mars'** + String get seedLibraryMonthMar; + + /// No description provided for @seedLibraryMonthApr. + /// + /// In fr, this message translates to: + /// **'Avril'** + String get seedLibraryMonthApr; + + /// No description provided for @seedLibraryMonthMay. + /// + /// In fr, this message translates to: + /// **'Mai'** + String get seedLibraryMonthMay; + + /// No description provided for @seedLibraryMonthJun. + /// + /// In fr, this message translates to: + /// **'Juin'** + String get seedLibraryMonthJun; + + /// No description provided for @seedLibraryMonthJul. + /// + /// In fr, this message translates to: + /// **'Juillet'** + String get seedLibraryMonthJul; + + /// No description provided for @seedLibraryMonthAug. + /// + /// In fr, this message translates to: + /// **'Août'** + String get seedLibraryMonthAug; + + /// No description provided for @seedLibraryMonthSep. + /// + /// In fr, this message translates to: + /// **'Septembre'** + String get seedLibraryMonthSep; + + /// No description provided for @seedLibraryMonthOct. + /// + /// In fr, this message translates to: + /// **'Octobre'** + String get seedLibraryMonthOct; + + /// No description provided for @seedLibraryMonthNov. + /// + /// In fr, this message translates to: + /// **'Novembre'** + String get seedLibraryMonthNov; + + /// No description provided for @seedLibraryMonthDec. + /// + /// In fr, this message translates to: + /// **'Décembre'** + String get seedLibraryMonthDec; + + /// No description provided for @seedLibraryMyPlants. + /// + /// In fr, this message translates to: + /// **'Mes plantes'** + String get seedLibraryMyPlants; + + /// No description provided for @seedLibraryName. + /// + /// In fr, this message translates to: + /// **'Nom'** + String get seedLibraryName; + + /// No description provided for @seedLibraryNbSeedsRecommended. + /// + /// In fr, this message translates to: + /// **'Nombre de graines recommandées'** + String get seedLibraryNbSeedsRecommended; + + /// No description provided for @seedLibraryNbSeedsRecommendedError. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer un nombre de graines recommandé supérieur à 0'** + String get seedLibraryNbSeedsRecommendedError; + + /// No description provided for @seedLibraryNoDateError. + /// + /// In fr, this message translates to: + /// **'Veuillez entrer une date'** + String get seedLibraryNoDateError; + + /// No description provided for @seedLibraryNoFilteredPlants. + /// + /// In fr, this message translates to: + /// **'Aucune plante ne correspond à votre recherche. Essayez d\'autres filtres.'** + String get seedLibraryNoFilteredPlants; + + /// No description provided for @seedLibraryNoMorePlant. + /// + /// In fr, this message translates to: + /// **'Aucune plante n\'est disponible'** + String get seedLibraryNoMorePlant; + + /// No description provided for @seedLibraryNoPersonalPlants. + /// + /// In fr, this message translates to: + /// **'Vous n\'avez pas encore de plantes dans votre grainothèque. Vous pouvez en ajouter en allant dans les stocks.'** + String get seedLibraryNoPersonalPlants; + + /// No description provided for @seedLibraryNoSpecies. + /// + /// In fr, this message translates to: + /// **'Aucune espèce trouvée'** + String get seedLibraryNoSpecies; + + /// No description provided for @seedLibraryNoStockPlants. + /// + /// In fr, this message translates to: + /// **'Aucune plante disponible dans le stock'** + String get seedLibraryNoStockPlants; + + /// No description provided for @seedLibraryNotes. + /// + /// In fr, this message translates to: + /// **'Notes'** + String get seedLibraryNotes; + + /// No description provided for @seedLibraryOk. + /// + /// In fr, this message translates to: + /// **'OK'** + String get seedLibraryOk; + + /// No description provided for @seedLibraryPlantationPeriod. + /// + /// In fr, this message translates to: + /// **'Période de plantation :'** + String get seedLibraryPlantationPeriod; + + /// No description provided for @seedLibraryPlantationType. + /// + /// In fr, this message translates to: + /// **'Type de plantation :'** + String get seedLibraryPlantationType; + + /// No description provided for @seedLibraryPlantDetail. + /// + /// In fr, this message translates to: + /// **'Détail de la plante'** + String get seedLibraryPlantDetail; + + /// No description provided for @seedLibraryPlantingDate. + /// + /// In fr, this message translates to: + /// **'Date de plantation'** + String get seedLibraryPlantingDate; + + /// No description provided for @seedLibraryPlantingNow. + /// + /// In fr, this message translates to: + /// **'Je la plante maintenant'** + String get seedLibraryPlantingNow; + + /// No description provided for @seedLibraryPrefix. + /// + /// In fr, this message translates to: + /// **'Préfixe'** + String get seedLibraryPrefix; + + /// No description provided for @seedLibraryPrefixError. + /// + /// In fr, this message translates to: + /// **'Prefixe déjà utilisé'** + String get seedLibraryPrefixError; + + /// No description provided for @seedLibraryPrefixLengthError. + /// + /// In fr, this message translates to: + /// **'Le préfixe doit faire 3 caractères'** + String get seedLibraryPrefixLengthError; + + /// No description provided for @seedLibraryPropagationMethod. + /// + /// In fr, this message translates to: + /// **'Méthode de propagation :'** + String get seedLibraryPropagationMethod; + + /// No description provided for @seedLibraryReference. + /// + /// In fr, this message translates to: + /// **'Référence :'** + String get seedLibraryReference; + + /// No description provided for @seedLibraryRemovedPlant. + /// + /// In fr, this message translates to: + /// **'Plante supprimée'** + String get seedLibraryRemovedPlant; + + /// No description provided for @seedLibraryRemovingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la suppression'** + String get seedLibraryRemovingError; + + /// No description provided for @seedLibraryResearch. + /// + /// In fr, this message translates to: + /// **'Recherche'** + String get seedLibraryResearch; + + /// No description provided for @seedLibrarySaveChanges. + /// + /// In fr, this message translates to: + /// **'Sauvegarder les modifications'** + String get seedLibrarySaveChanges; + + /// No description provided for @seedLibrarySeason. + /// + /// In fr, this message translates to: + /// **'Saison :'** + String get seedLibrarySeason; + + /// No description provided for @seedLibrarySeed. + /// + /// In fr, this message translates to: + /// **'Graine'** + String get seedLibrarySeed; + + /// No description provided for @seedLibrarySeeds. + /// + /// In fr, this message translates to: + /// **'graines'** + String get seedLibrarySeeds; + + /// No description provided for @seedLibrarySeedDeposit. + /// + /// In fr, this message translates to: + /// **'Dépôt de plantes'** + String get seedLibrarySeedDeposit; + + /// No description provided for @seedLibrarySeedLibrary. + /// + /// In fr, this message translates to: + /// **'Grainothèque'** + String get seedLibrarySeedLibrary; + + /// No description provided for @seedLibrarySeedQuantitySimple. + /// + /// In fr, this message translates to: + /// **'Quantité de graines'** + String get seedLibrarySeedQuantitySimple; + + /// No description provided for @seedLibrarySeedQuantity. + /// + /// In fr, this message translates to: + /// **'Quantité de graines :'** + String get seedLibrarySeedQuantity; + + /// No description provided for @seedLibraryShowDeadPlants. + /// + /// In fr, this message translates to: + /// **'Afficher les plantes mortes'** + String get seedLibraryShowDeadPlants; + + /// No description provided for @seedLibrarySpecies. + /// + /// In fr, this message translates to: + /// **'Espèce :'** + String get seedLibrarySpecies; + + /// No description provided for @seedLibrarySpeciesHelp. + /// + /// In fr, this message translates to: + /// **'Aide sur l\'espèce'** + String get seedLibrarySpeciesHelp; + + /// No description provided for @seedLibrarySpeciesPlural. + /// + /// In fr, this message translates to: + /// **'Espèces'** + String get seedLibrarySpeciesPlural; + + /// No description provided for @seedLibrarySpeciesSimple. + /// + /// In fr, this message translates to: + /// **'Espèce'** + String get seedLibrarySpeciesSimple; + + /// No description provided for @seedLibrarySpeciesType. + /// + /// In fr, this message translates to: + /// **'Type d\'espèce :'** + String get seedLibrarySpeciesType; + + /// No description provided for @seedLibrarySpring. + /// + /// In fr, this message translates to: + /// **'Printemps'** + String get seedLibrarySpring; + + /// No description provided for @seedLibraryStartMonth. + /// + /// In fr, this message translates to: + /// **'Mois de début :'** + String get seedLibraryStartMonth; + + /// No description provided for @seedLibraryStock. + /// + /// In fr, this message translates to: + /// **'Stock disponible'** + String get seedLibraryStock; + + /// No description provided for @seedLibrarySummer. + /// + /// In fr, this message translates to: + /// **'Été'** + String get seedLibrarySummer; + + /// No description provided for @seedLibraryStocks. + /// + /// In fr, this message translates to: + /// **'Stocks'** + String get seedLibraryStocks; + + /// No description provided for @seedLibraryTimeUntilMaturation. + /// + /// In fr, this message translates to: + /// **'Temps avant maturation :'** + String get seedLibraryTimeUntilMaturation; + + /// No description provided for @seedLibraryType. + /// + /// In fr, this message translates to: + /// **'Type :'** + String get seedLibraryType; + + /// No description provided for @seedLibraryUnableToOpen. + /// + /// In fr, this message translates to: + /// **'Impossible d\'ouvrir le lien'** + String get seedLibraryUnableToOpen; + + /// No description provided for @seedLibraryUpdate. + /// + /// In fr, this message translates to: + /// **'Modifier'** + String get seedLibraryUpdate; + + /// No description provided for @seedLibraryUpdatedInformation. + /// + /// In fr, this message translates to: + /// **'Informations modifiées'** + String get seedLibraryUpdatedInformation; + + /// No description provided for @seedLibraryUpdatedSpecies. + /// + /// In fr, this message translates to: + /// **'Espèce modifiée'** + String get seedLibraryUpdatedSpecies; + + /// No description provided for @seedLibraryUpdatedPlant. + /// + /// In fr, this message translates to: + /// **'Plante modifiée'** + String get seedLibraryUpdatedPlant; + + /// No description provided for @seedLibraryUpdatingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la modification'** + String get seedLibraryUpdatingError; + + /// No description provided for @seedLibraryWinter. + /// + /// In fr, this message translates to: + /// **'Hiver'** + String get seedLibraryWinter; + + /// No description provided for @seedLibraryWriteReference. + /// + /// In fr, this message translates to: + /// **'Veuillez écrire la référence suivante : '** + String get seedLibraryWriteReference; + + /// No description provided for @settingsAccount. + /// + /// In fr, this message translates to: + /// **'Compte'** + String get settingsAccount; + + /// No description provided for @settingsAddProfilePicture. + /// + /// In fr, this message translates to: + /// **'Ajouter une photo'** + String get settingsAddProfilePicture; + + /// No description provided for @settingsAdmin. + /// + /// In fr, this message translates to: + /// **'Administrateur'** + String get settingsAdmin; + + /// No description provided for @settingsAskHelp. + /// + /// In fr, this message translates to: + /// **'Demander de l\'aide'** + String get settingsAskHelp; + + /// No description provided for @settingsAssociation. + /// + /// In fr, this message translates to: + /// **'Association'** + String get settingsAssociation; + + /// No description provided for @settingsBirthday. + /// + /// In fr, this message translates to: + /// **'Date de naissance'** + String get settingsBirthday; + + /// No description provided for @settingsBugs. + /// + /// In fr, this message translates to: + /// **'Bugs'** + String get settingsBugs; + + /// No description provided for @settingsChangePassword. + /// + /// In fr, this message translates to: + /// **'Changer de mot de passe'** + String get settingsChangePassword; + + /// No description provided for @settingsChangingPassword. + /// + /// In fr, this message translates to: + /// **'Voulez-vous vraiment changer votre mot de passe ?'** + String get settingsChangingPassword; + + /// No description provided for @settingsConfirmPassword. + /// + /// In fr, this message translates to: + /// **'Confirmer le mot de passe'** + String get settingsConfirmPassword; + + /// No description provided for @settingsCopied. + /// + /// In fr, this message translates to: + /// **'Copié !'** + String get settingsCopied; + + /// No description provided for @settingsDarkMode. + /// + /// In fr, this message translates to: + /// **'Mode sombre'** + String get settingsDarkMode; + + /// No description provided for @settingsDarkModeOff. + /// + /// In fr, this message translates to: + /// **'Désactivé'** + String get settingsDarkModeOff; + + /// No description provided for @settingsDeleteLogs. + /// + /// In fr, this message translates to: + /// **'Supprimer les logs ?'** + String get settingsDeleteLogs; + + /// No description provided for @settingsDeleteNotificationLogs. + /// + /// In fr, this message translates to: + /// **'Supprimer les logs des notifications ?'** + String get settingsDeleteNotificationLogs; + + /// No description provided for @settingsDetelePersonalData. + /// + /// In fr, this message translates to: + /// **'Supprimer mes données personnelles'** + String get settingsDetelePersonalData; + + /// No description provided for @settingsDetelePersonalDataDesc. + /// + /// In fr, this message translates to: + /// **'Cette action notifie l\'administrateur que vous souhaitez supprimer vos données personnelles.'** + String get settingsDetelePersonalDataDesc; + + /// No description provided for @settingsDeleting. + /// + /// In fr, this message translates to: + /// **'Suppresion'** + String get settingsDeleting; + + /// No description provided for @settingsEdit. + /// + /// In fr, this message translates to: + /// **'Modifier'** + String get settingsEdit; + + /// No description provided for @settingsEditAccount. + /// + /// In fr, this message translates to: + /// **'Modifier mon profil'** + String get settingsEditAccount; + + /// No description provided for @settingsEmail. + /// + /// In fr, this message translates to: + /// **'Email'** + String get settingsEmail; + + /// No description provided for @settingsEmptyField. + /// + /// In fr, this message translates to: + /// **'Ce champ ne peut pas être vide'** + String get settingsEmptyField; + + /// No description provided for @settingsErrorProfilePicture. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la modification de la photo de profil'** + String get settingsErrorProfilePicture; + + /// No description provided for @settingsErrorSendingDemand. + /// + /// In fr, this message translates to: + /// **'Erreur lors de l\'envoi de la demande'** + String get settingsErrorSendingDemand; + + /// No description provided for @settingsEventsIcal. + /// + /// In fr, this message translates to: + /// **'Lien Ical des événements'** + String get settingsEventsIcal; + + /// No description provided for @settingsExpectingDate. + /// + /// In fr, this message translates to: + /// **'Date de naissance attendue'** + String get settingsExpectingDate; + + /// No description provided for @settingsFirstname. + /// + /// In fr, this message translates to: + /// **'Prénom'** + String get settingsFirstname; + + /// No description provided for @settingsFloor. + /// + /// In fr, this message translates to: + /// **'Étage'** + String get settingsFloor; + + /// No description provided for @settingsHelp. + /// + /// In fr, this message translates to: + /// **'Aide'** + String get settingsHelp; + + /// No description provided for @settingsIcalCopied. + /// + /// In fr, this message translates to: + /// **'Lien Ical copié !'** + String get settingsIcalCopied; + + /// No description provided for @settingsLanguage. + /// + /// In fr, this message translates to: + /// **'Langue'** + String get settingsLanguage; + + /// No description provided for @settingsLanguageVar. + /// + /// In fr, this message translates to: + /// **'Français 🇫🇷'** + String get settingsLanguageVar; + + /// No description provided for @settingsLogs. + /// + /// In fr, this message translates to: + /// **'Logs'** + String get settingsLogs; + + /// No description provided for @settingsModules. + /// + /// In fr, this message translates to: + /// **'Modules'** + String get settingsModules; + + /// No description provided for @settingsMyIcs. + /// + /// In fr, this message translates to: + /// **'Mon lien Ical'** + String get settingsMyIcs; + + /// No description provided for @settingsName. + /// + /// In fr, this message translates to: + /// **'Nom'** + String get settingsName; + + /// No description provided for @settingsNewPassword. + /// + /// In fr, this message translates to: + /// **'Nouveau mot de passe'** + String get settingsNewPassword; + + /// No description provided for @settingsNickname. + /// + /// In fr, this message translates to: + /// **'Surnom'** + String get settingsNickname; + + /// No description provided for @settingsNotifications. + /// + /// In fr, this message translates to: + /// **'Notifications'** + String get settingsNotifications; + + /// No description provided for @settingsOldPassword. + /// + /// In fr, this message translates to: + /// **'Ancien mot de passe'** + String get settingsOldPassword; + + /// No description provided for @settingsPasswordChanged. + /// + /// In fr, this message translates to: + /// **'Mot de passe changé'** + String get settingsPasswordChanged; + + /// No description provided for @settingsPasswordsNotMatch. + /// + /// In fr, this message translates to: + /// **'Les mots de passe ne correspondent pas'** + String get settingsPasswordsNotMatch; + + /// No description provided for @settingsPersonalData. + /// + /// In fr, this message translates to: + /// **'Données personnelles'** + String get settingsPersonalData; + + /// No description provided for @settingsPersonalisation. + /// + /// In fr, this message translates to: + /// **'Personnalisation'** + String get settingsPersonalisation; + + /// No description provided for @settingsPhone. + /// + /// In fr, this message translates to: + /// **'Téléphone'** + String get settingsPhone; + + /// No description provided for @settingsProfilePicture. + /// + /// In fr, this message translates to: + /// **'Photo de profil'** + String get settingsProfilePicture; + + /// No description provided for @settingsPromo. + /// + /// In fr, this message translates to: + /// **'Promotion'** + String get settingsPromo; + + /// No description provided for @settingsRepportBug. + /// + /// In fr, this message translates to: + /// **'Signaler un bug'** + String get settingsRepportBug; + + /// No description provided for @settingsSave. + /// + /// In fr, this message translates to: + /// **'Enregistrer'** + String get settingsSave; + + /// No description provided for @settingsSecurity. + /// + /// In fr, this message translates to: + /// **'Sécurité'** + String get settingsSecurity; + + /// No description provided for @settingsSendedDemand. + /// + /// In fr, this message translates to: + /// **'Demande envoyée'** + String get settingsSendedDemand; + + /// No description provided for @settingsSettings. + /// + /// In fr, this message translates to: + /// **'Paramètres'** + String get settingsSettings; + + /// No description provided for @settingsTooHeavyProfilePicture. + /// + /// In fr, this message translates to: + /// **'L\'image est trop lourde (max 4Mo)'** + String get settingsTooHeavyProfilePicture; + + /// No description provided for @settingsUpdatedProfile. + /// + /// In fr, this message translates to: + /// **'Profil modifié'** + String get settingsUpdatedProfile; + + /// No description provided for @settingsUpdatedProfilePicture. + /// + /// In fr, this message translates to: + /// **'Photo de profil modifiée'** + String get settingsUpdatedProfilePicture; + + /// No description provided for @settingsUpdateNotification. + /// + /// In fr, this message translates to: + /// **'Mettre à jour les notifications'** + String get settingsUpdateNotification; + + /// No description provided for @settingsUpdatingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la modification du profil'** + String get settingsUpdatingError; + + /// No description provided for @settingsVersion. + /// + /// In fr, this message translates to: + /// **'Version'** + String get settingsVersion; + + /// No description provided for @settingsPasswordStrength. + /// + /// In fr, this message translates to: + /// **'Force du mot de passe'** + String get settingsPasswordStrength; + + /// No description provided for @settingsPasswordStrengthVeryWeak. + /// + /// In fr, this message translates to: + /// **'Très faible'** + String get settingsPasswordStrengthVeryWeak; + + /// No description provided for @settingsPasswordStrengthWeak. + /// + /// In fr, this message translates to: + /// **'Faible'** + String get settingsPasswordStrengthWeak; + + /// No description provided for @settingsPasswordStrengthMedium. + /// + /// In fr, this message translates to: + /// **'Moyen'** + String get settingsPasswordStrengthMedium; + + /// No description provided for @settingsPasswordStrengthStrong. + /// + /// In fr, this message translates to: + /// **'Fort'** + String get settingsPasswordStrengthStrong; + + /// No description provided for @settingsPasswordStrengthVeryStrong. + /// + /// In fr, this message translates to: + /// **'Très fort'** + String get settingsPasswordStrengthVeryStrong; + + /// No description provided for @settingsPhoneNumber. + /// + /// In fr, this message translates to: + /// **'Numéro de téléphone'** + String get settingsPhoneNumber; + + /// No description provided for @settingsValidate. + /// + /// In fr, this message translates to: + /// **'Valider'** + String get settingsValidate; + + /// No description provided for @settingsEditedAccount. + /// + /// In fr, this message translates to: + /// **'Compte modifié avec succès'** + String get settingsEditedAccount; + + /// No description provided for @settingsFailedToEditAccount. + /// + /// In fr, this message translates to: + /// **'Échec de la modification du compte'** + String get settingsFailedToEditAccount; + + /// No description provided for @settingsChooseLanguage. + /// + /// In fr, this message translates to: + /// **'Choix de la langue'** + String get settingsChooseLanguage; + + /// Affiche le nombre de notifications actives sur le total des notifications disponibles, avec gestion du pluriel + /// + /// In fr, this message translates to: + /// **'{active}/{total} {active, plural, zero {activée} one {activée} other {activées}}'** + String settingsNotificationCounter(int active, int total); + + /// No description provided for @settingsEvent. + /// + /// In fr, this message translates to: + /// **'Événement'** + String get settingsEvent; + + /// No description provided for @settingsIcal. + /// + /// In fr, this message translates to: + /// **'Lien Ical'** + String get settingsIcal; + + /// No description provided for @settingsSynncWithCalendar. + /// + /// In fr, this message translates to: + /// **'Synchroniser avec votre calendrier'** + String get settingsSynncWithCalendar; + + /// No description provided for @settingsIcalLinkCopied. + /// + /// In fr, this message translates to: + /// **'Lien Ical copié dans le presse-papier'** + String get settingsIcalLinkCopied; + + /// No description provided for @settingsProfile. + /// + /// In fr, this message translates to: + /// **'Profil'** + String get settingsProfile; + + /// No description provided for @settingsConnexion. + /// + /// In fr, this message translates to: + /// **'Connexion'** + String get settingsConnexion; + + /// No description provided for @settingsLogOut. + /// + /// In fr, this message translates to: + /// **'Se déconnecter'** + String get settingsLogOut; + + /// No description provided for @settingsLogOutDescription. + /// + /// In fr, this message translates to: + /// **'Êtes-vous sûr de vouloir vous déconnecter ?'** + String get settingsLogOutDescription; + + /// No description provided for @settingsLogOutSuccess. + /// + /// In fr, this message translates to: + /// **'Déconnexion réussie'** + String get settingsLogOutSuccess; + + /// No description provided for @settingsDeleteMyAccount. + /// + /// In fr, this message translates to: + /// **'Supprimer mon compte'** + String get settingsDeleteMyAccount; + + /// No description provided for @settingsDeleteMyAccountDescription. + /// + /// In fr, this message translates to: + /// **'Cette action notifie l\'administrateur que vous souhaitez supprimer votre compte.'** + String get settingsDeleteMyAccountDescription; + + /// No description provided for @settingsDeletionAsked. + /// + /// In fr, this message translates to: + /// **'Demande de suppression de compte envoyée'** + String get settingsDeletionAsked; + + /// No description provided for @settingsDeleteMyAccountError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la demande de suppression de compte'** + String get settingsDeleteMyAccountError; + + /// No description provided for @voteAdd. + /// + /// In fr, this message translates to: + /// **'Ajouter'** + String get voteAdd; + + /// No description provided for @voteAddMember. + /// + /// In fr, this message translates to: + /// **'Ajouter un membre'** + String get voteAddMember; + + /// No description provided for @voteAddedPretendance. + /// + /// In fr, this message translates to: + /// **'Liste ajoutée'** + String get voteAddedPretendance; + + /// No description provided for @voteAddedSection. + /// + /// In fr, this message translates to: + /// **'Section ajoutée'** + String get voteAddedSection; + + /// No description provided for @voteAddingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de l\'ajout'** + String get voteAddingError; + + /// No description provided for @voteAddPretendance. + /// + /// In fr, this message translates to: + /// **'Ajouter une liste'** + String get voteAddPretendance; + + /// No description provided for @voteAddSection. + /// + /// In fr, this message translates to: + /// **'Ajouter une section'** + String get voteAddSection; + + /// No description provided for @voteAll. + /// + /// In fr, this message translates to: + /// **'Tous'** + String get voteAll; + + /// No description provided for @voteAlreadyAddedMember. + /// + /// In fr, this message translates to: + /// **'Membre déjà ajouté'** + String get voteAlreadyAddedMember; + + /// No description provided for @voteAlreadyVoted. + /// + /// In fr, this message translates to: + /// **'Vote enregistré'** + String get voteAlreadyVoted; + + /// No description provided for @voteChooseList. + /// + /// In fr, this message translates to: + /// **'Choisir une liste'** + String get voteChooseList; + + /// No description provided for @voteClear. + /// + /// In fr, this message translates to: + /// **'Réinitialiser'** + String get voteClear; + + /// No description provided for @voteClearVotes. + /// + /// In fr, this message translates to: + /// **'Réinitialiser les votes'** + String get voteClearVotes; + + /// No description provided for @voteClosedVote. + /// + /// In fr, this message translates to: + /// **'Votes clos'** + String get voteClosedVote; + + /// No description provided for @voteCloseVote. + /// + /// In fr, this message translates to: + /// **'Fermer les votes'** + String get voteCloseVote; + + /// No description provided for @voteConfirmVote. + /// + /// In fr, this message translates to: + /// **'Confirmer le vote'** + String get voteConfirmVote; + + /// No description provided for @voteCountVote. + /// + /// In fr, this message translates to: + /// **'Dépouiller les votes'** + String get voteCountVote; + + /// No description provided for @voteDelete. + /// + /// In fr, this message translates to: + /// **'Supprimer'** + String get voteDelete; + + /// No description provided for @voteDeletedAll. + /// + /// In fr, this message translates to: + /// **'Tout supprimé'** + String get voteDeletedAll; + + /// No description provided for @voteDeletedPipo. + /// + /// In fr, this message translates to: + /// **'Listes pipos supprimées'** + String get voteDeletedPipo; + + /// No description provided for @voteDeletedSection. + /// + /// In fr, this message translates to: + /// **'Section supprimée'** + String get voteDeletedSection; + + /// No description provided for @voteDeleteAll. + /// + /// In fr, this message translates to: + /// **'Supprimer tout'** + String get voteDeleteAll; + + /// No description provided for @voteDeleteAllDescription. + /// + /// In fr, this message translates to: + /// **'Voulez-vous vraiment supprimer tout ?'** + String get voteDeleteAllDescription; + + /// No description provided for @voteDeletePipo. + /// + /// In fr, this message translates to: + /// **'Supprimer les listes pipos'** + String get voteDeletePipo; + + /// No description provided for @voteDeletePipoDescription. + /// + /// In fr, this message translates to: + /// **'Voulez-vous vraiment supprimer les listes pipos ?'** + String get voteDeletePipoDescription; + + /// No description provided for @voteDeletePretendance. + /// + /// In fr, this message translates to: + /// **'Supprimer la liste'** + String get voteDeletePretendance; + + /// No description provided for @voteDeletePretendanceDesc. + /// + /// In fr, this message translates to: + /// **'Voulez-vous vraiment supprimer cette liste ?'** + String get voteDeletePretendanceDesc; + + /// No description provided for @voteDeleteSection. + /// + /// In fr, this message translates to: + /// **'Supprimer la section'** + String get voteDeleteSection; + + /// No description provided for @voteDeleteSectionDescription. + /// + /// In fr, this message translates to: + /// **'Voulez-vous vraiment supprimer cette section ?'** + String get voteDeleteSectionDescription; + + /// No description provided for @voteDeletingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la suppression'** + String get voteDeletingError; + + /// No description provided for @voteDescription. + /// + /// In fr, this message translates to: + /// **'Description'** + String get voteDescription; + + /// No description provided for @voteEdit. + /// + /// In fr, this message translates to: + /// **'Modifier'** + String get voteEdit; + + /// No description provided for @voteEditedPretendance. + /// + /// In fr, this message translates to: + /// **'Liste modifiée'** + String get voteEditedPretendance; + + /// No description provided for @voteEditedSection. + /// + /// In fr, this message translates to: + /// **'Section modifiée'** + String get voteEditedSection; + + /// No description provided for @voteEditingError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la modification'** + String get voteEditingError; + + /// No description provided for @voteErrorClosingVotes. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la fermeture des votes'** + String get voteErrorClosingVotes; + + /// No description provided for @voteErrorCountingVotes. + /// + /// In fr, this message translates to: + /// **'Erreur lors du dépouillement des votes'** + String get voteErrorCountingVotes; + + /// No description provided for @voteErrorResetingVotes. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la réinitialisation des votes'** + String get voteErrorResetingVotes; + + /// No description provided for @voteErrorOpeningVotes. + /// + /// In fr, this message translates to: + /// **'Erreur lors de l\'ouverture des votes'** + String get voteErrorOpeningVotes; + + /// No description provided for @voteIncorrectOrMissingFields. + /// + /// In fr, this message translates to: + /// **'Champs incorrects ou manquants'** + String get voteIncorrectOrMissingFields; + + /// No description provided for @voteMembers. + /// + /// In fr, this message translates to: + /// **'Membres'** + String get voteMembers; + + /// No description provided for @voteName. + /// + /// In fr, this message translates to: + /// **'Nom'** + String get voteName; + + /// No description provided for @voteNoPretendanceList. + /// + /// In fr, this message translates to: + /// **'Aucune liste de prétendance'** + String get voteNoPretendanceList; + + /// No description provided for @voteNoSection. + /// + /// In fr, this message translates to: + /// **'Aucune section'** + String get voteNoSection; + + /// No description provided for @voteCanNotVote. + /// + /// In fr, this message translates to: + /// **'Vous ne pouvez pas voter'** + String get voteCanNotVote; + + /// No description provided for @voteNoSectionList. + /// + /// In fr, this message translates to: + /// **'Aucune section'** + String get voteNoSectionList; + + /// No description provided for @voteNotOpenedVote. + /// + /// In fr, this message translates to: + /// **'Vote non ouvert'** + String get voteNotOpenedVote; + + /// No description provided for @voteOnGoingCount. + /// + /// In fr, this message translates to: + /// **'Dépouillement en cours'** + String get voteOnGoingCount; + + /// No description provided for @voteOpenVote. + /// + /// In fr, this message translates to: + /// **'Ouvrir les votes'** + String get voteOpenVote; + + /// No description provided for @votePipo. + /// + /// In fr, this message translates to: + /// **'Pipo'** + String get votePipo; + + /// No description provided for @votePretendance. + /// + /// In fr, this message translates to: + /// **'Listes'** + String get votePretendance; + + /// No description provided for @votePretendanceDeleted. + /// + /// In fr, this message translates to: + /// **'Prétendance supprimée'** + String get votePretendanceDeleted; + + /// No description provided for @votePretendanceNotDeleted. + /// + /// In fr, this message translates to: + /// **'Erreur lors de la suppression'** + String get votePretendanceNotDeleted; + + /// No description provided for @voteProgram. + /// + /// In fr, this message translates to: + /// **'Programme'** + String get voteProgram; + + /// No description provided for @votePublish. + /// + /// In fr, this message translates to: + /// **'Publier'** + String get votePublish; + + /// No description provided for @votePublishVoteDescription. + /// + /// In fr, this message translates to: + /// **'Voulez-vous vraiment publier les votes ?'** + String get votePublishVoteDescription; + + /// No description provided for @voteResetedVotes. + /// + /// In fr, this message translates to: + /// **'Votes réinitialisés'** + String get voteResetedVotes; + + /// No description provided for @voteResetVote. + /// + /// In fr, this message translates to: + /// **'Réinitialiser les votes'** + String get voteResetVote; + + /// No description provided for @voteResetVoteDescription. + /// + /// In fr, this message translates to: + /// **'Que voulez-vous faire ?'** + String get voteResetVoteDescription; + + /// No description provided for @voteRole. + /// + /// In fr, this message translates to: + /// **'Rôle'** + String get voteRole; + + /// No description provided for @voteSectionDescription. + /// + /// In fr, this message translates to: + /// **'Description de la section'** + String get voteSectionDescription; + + /// No description provided for @voteSection. + /// + /// In fr, this message translates to: + /// **'Section'** + String get voteSection; + + /// No description provided for @voteSectionName. + /// + /// In fr, this message translates to: + /// **'Nom de la section'** + String get voteSectionName; + + /// No description provided for @voteSeeMore. + /// + /// In fr, this message translates to: + /// **'Voir plus'** + String get voteSeeMore; + + /// No description provided for @voteSelected. + /// + /// In fr, this message translates to: + /// **'Sélectionné'** + String get voteSelected; + + /// No description provided for @voteShowVotes. + /// + /// In fr, this message translates to: + /// **'Voir les votes'** + String get voteShowVotes; + + /// No description provided for @voteVote. + /// + /// In fr, this message translates to: + /// **'Vote'** + String get voteVote; + + /// No description provided for @voteVoteError. + /// + /// In fr, this message translates to: + /// **'Erreur lors de l\'enregistrement du vote'** + String get voteVoteError; + + /// No description provided for @voteVoteFor. + /// + /// In fr, this message translates to: + /// **'Voter pour '** + String get voteVoteFor; + + /// No description provided for @voteVoteNotStarted. + /// + /// In fr, this message translates to: + /// **'Vote non ouvert'** + String get voteVoteNotStarted; + + /// No description provided for @voteVoters. + /// + /// In fr, this message translates to: + /// **'Groupes votants'** + String get voteVoters; + + /// No description provided for @voteVoteSuccess. + /// + /// In fr, this message translates to: + /// **'Vote enregistré'** + String get voteVoteSuccess; + + /// No description provided for @voteVotes. + /// + /// In fr, this message translates to: + /// **'Voix'** + String get voteVotes; + + /// No description provided for @voteVotesClosed. + /// + /// In fr, this message translates to: + /// **'Votes clos'** + String get voteVotesClosed; + + /// No description provided for @voteVotesCounted. + /// + /// In fr, this message translates to: + /// **'Votes dépouillés'** + String get voteVotesCounted; + + /// No description provided for @voteVotesOpened. + /// + /// In fr, this message translates to: + /// **'Votes ouverts'** + String get voteVotesOpened; + + /// No description provided for @voteWarning. + /// + /// In fr, this message translates to: + /// **'Attention'** + String get voteWarning; + + /// No description provided for @voteWarningMessage. + /// + /// In fr, this message translates to: + /// **'La sélection ne sera pas sauvegardée.\nVoulez-vous continuer ?'** + String get voteWarningMessage; + + /// No description provided for @moduleAdvert. + /// + /// In fr, this message translates to: + /// **'Feed'** + String get moduleAdvert; + + /// No description provided for @moduleAdvertDescription. + /// + /// In fr, this message translates to: + /// **'Gérer les feeds'** + String get moduleAdvertDescription; + + /// No description provided for @moduleAmap. + /// + /// In fr, this message translates to: + /// **'AMAP'** + String get moduleAmap; + + /// No description provided for @moduleAmapDescription. + /// + /// In fr, this message translates to: + /// **'Gérer les livraisons et les produits'** + String get moduleAmapDescription; + + /// No description provided for @moduleBooking. + /// + /// In fr, this message translates to: + /// **'Réservation'** + String get moduleBooking; + + /// No description provided for @moduleBookingDescription. + /// + /// In fr, this message translates to: + /// **'Gérer les réservations, les salles et les managers'** + String get moduleBookingDescription; + + /// No description provided for @moduleCalendar. + /// + /// In fr, this message translates to: + /// **'Calendrier'** + String get moduleCalendar; + + /// No description provided for @moduleCalendarDescription. + /// + /// In fr, this message translates to: + /// **'Consulter les événements et les activités'** + String get moduleCalendarDescription; + + /// No description provided for @moduleCentralisation. + /// + /// In fr, this message translates to: + /// **'Centralisation'** + String get moduleCentralisation; + + /// No description provided for @moduleCentralisationDescription. + /// + /// In fr, this message translates to: + /// **'Gérer la centralisation des données'** + String get moduleCentralisationDescription; + + /// No description provided for @moduleCinema. + /// + /// In fr, this message translates to: + /// **'Cinéma'** + String get moduleCinema; + + /// No description provided for @moduleCinemaDescription. + /// + /// In fr, this message translates to: + /// **'Gérer les séances de cinéma'** + String get moduleCinemaDescription; + + /// No description provided for @moduleEvent. + /// + /// In fr, this message translates to: + /// **'Événement'** + String get moduleEvent; + + /// No description provided for @moduleEventDescription. + /// + /// In fr, this message translates to: + /// **'Gérer les événements et les participants'** + String get moduleEventDescription; + + /// No description provided for @moduleFlappyBird. + /// + /// In fr, this message translates to: + /// **'Flappy Bird'** + String get moduleFlappyBird; + + /// No description provided for @moduleFlappyBirdDescription. + /// + /// In fr, this message translates to: + /// **'Jouer à Flappy Bird et consulter le classement'** + String get moduleFlappyBirdDescription; + + /// No description provided for @moduleLoan. + /// + /// In fr, this message translates to: + /// **'Prêt'** + String get moduleLoan; + + /// No description provided for @moduleLoanDescription. + /// + /// In fr, this message translates to: + /// **'Gérer les prêts et les articles'** + String get moduleLoanDescription; + + /// No description provided for @modulePhonebook. + /// + /// In fr, this message translates to: + /// **'Annuaire'** + String get modulePhonebook; + + /// No description provided for @modulePhonebookDescription. + /// + /// In fr, this message translates to: + /// **'Gérer les associations, les membres et les administrateurs'** + String get modulePhonebookDescription; + + /// No description provided for @modulePurchases. + /// + /// In fr, this message translates to: + /// **'Achats'** + String get modulePurchases; + + /// No description provided for @modulePurchasesDescription. + /// + /// In fr, this message translates to: + /// **'Gérer les achats, les tickets et l\'historique'** + String get modulePurchasesDescription; + + /// No description provided for @moduleRaffle. + /// + /// In fr, this message translates to: + /// **'Tombola'** + String get moduleRaffle; + + /// No description provided for @moduleRaffleDescription. + /// + /// In fr, this message translates to: + /// **'Gérer les tombolas, les prix et les tickets'** + String get moduleRaffleDescription; + + /// No description provided for @moduleRecommendation. + /// + /// In fr, this message translates to: + /// **'Bons plans'** + String get moduleRecommendation; + + /// No description provided for @moduleRecommendationDescription. + /// + /// In fr, this message translates to: + /// **'Gérer les recommandations, les informations et les administrateurs'** + String get moduleRecommendationDescription; + + /// No description provided for @moduleSeedLibrary. + /// + /// In fr, this message translates to: + /// **'Grainothèque'** + String get moduleSeedLibrary; + + /// No description provided for @moduleSeedLibraryDescription. + /// + /// In fr, this message translates to: + /// **'Gérer les graines, les espèces et les stocks'** + String get moduleSeedLibraryDescription; + + /// No description provided for @moduleVote. + /// + /// In fr, this message translates to: + /// **'Vote'** + String get moduleVote; + + /// No description provided for @moduleVoteDescription. + /// + /// In fr, this message translates to: + /// **'Gérer les votes, les sections et les candidats'** + String get moduleVoteDescription; + + /// No description provided for @modulePh. + /// + /// In fr, this message translates to: + /// **'PH'** + String get modulePh; + + /// No description provided for @modulePhDescription. + /// + /// In fr, this message translates to: + /// **'Gérer les PH, les formulaires et les administrateurs'** + String get modulePhDescription; + + /// No description provided for @moduleSettings. + /// + /// In fr, this message translates to: + /// **'Paramètres'** + String get moduleSettings; + + /// No description provided for @moduleSettingsDescription. + /// + /// In fr, this message translates to: + /// **'Gérer les paramètres de l\'application'** + String get moduleSettingsDescription; + + /// No description provided for @moduleFeed. + /// + /// In fr, this message translates to: + /// **'Events'** + String get moduleFeed; + + /// No description provided for @moduleFeedDescription. + /// + /// In fr, this message translates to: + /// **'Consulter les événements'** + String get moduleFeedDescription; + + /// No description provided for @moduleStyleGuide. + /// + /// In fr, this message translates to: + /// **'StyleGuide'** + String get moduleStyleGuide; + + /// No description provided for @moduleStyleGuideDescription. + /// + /// In fr, this message translates to: + /// **'Explore the UI components and styles used in Titan'** + String get moduleStyleGuideDescription; + + /// No description provided for @moduleAdmin. + /// + /// In fr, this message translates to: + /// **'Admin'** + String get moduleAdmin; + + /// No description provided for @moduleAdminDescription. + /// + /// In fr, this message translates to: + /// **'Gérer les utilisateurs, groupes et structures'** + String get moduleAdminDescription; + + /// No description provided for @moduleOthers. + /// + /// In fr, this message translates to: + /// **'Autres'** + String get moduleOthers; + + /// No description provided for @moduleOthersDescription. + /// + /// In fr, this message translates to: + /// **'Afficher les autres modules'** + String get moduleOthersDescription; + + /// No description provided for @modulePayment. + /// + /// In fr, this message translates to: + /// **'Paiement'** + String get modulePayment; + + /// No description provided for @modulePaymentDescription. + /// + /// In fr, this message translates to: + /// **'Gérer les paiements, les statistiques et les appareils'** + String get modulePaymentDescription; + + /// No description provided for @toolInvalidNumber. + /// + /// In fr, this message translates to: + /// **'Chiffre invalide'** + String get toolInvalidNumber; + + /// No description provided for @toolDateRequired. + /// + /// In fr, this message translates to: + /// **'Date requise'** + String get toolDateRequired; + + /// No description provided for @toolSuccess. + /// + /// In fr, this message translates to: + /// **'Succès'** + String get toolSuccess; +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => + ['en', 'fr'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': + return AppLocalizationsEn(); + case 'fr': + return AppLocalizationsFr(); + } + + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.', + ); +} diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart new file mode 100644 index 0000000000..601a6c0542 --- /dev/null +++ b/lib/l10n/app_localizations_en.dart @@ -0,0 +1,4526 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get dateToday => 'Today'; + + @override + String get dateYesterday => 'Yesterday'; + + @override + String get dateTomorrow => 'Tomorrow'; + + @override + String get dateAt => 'at'; + + @override + String get dateFrom => 'from'; + + @override + String get dateTo => 'to'; + + @override + String get dateBetweenDays => 'to'; + + @override + String get dateStarting => 'Starting'; + + @override + String get dateLast => 'Last'; + + @override + String get dateUntil => 'Until'; + + @override + String get feedFilterAll => 'All'; + + @override + String get feedFilterPending => 'Pending'; + + @override + String get feedFilterApproved => 'Approved'; + + @override + String get feedFilterRejected => 'Rejected'; + + @override + String get feedEmptyAll => 'No events available'; + + @override + String get feedEmptyPending => 'No events pending approval'; + + @override + String get feedEmptyApproved => 'No approved events'; + + @override + String get feedEmptyRejected => 'No rejected events'; + + @override + String get feedEventManagement => 'Event Management'; + + @override + String get feedTitle => 'Title'; + + @override + String get feedLocation => 'Location'; + + @override + String get feedSGDate => 'SG Date'; + + @override + String get feedSGExternalLink => 'SG External link'; + + @override + String get feedCreateEvent => 'Create an event'; + + @override + String get feedNotification => 'Send a notification'; + + @override + String get feedPleaseSelectAnAssociation => 'Please select an association'; + + @override + String get feedReject => 'Reject'; + + @override + String get feedApprove => 'Approve'; + + @override + String get feedEnded => 'Ended'; + + @override + String get feedOngoing => 'Ongoing'; + + @override + String get feedFilter => 'Filter'; + + @override + String get feedAssociation => 'Association'; + + @override + String feedAssociationEvent(String name) { + return '$name event'; + } + + @override + String get feedEditEvent => 'Edit event'; + + @override + String get feedManageAssociationEvents => 'Manage association events'; + + @override + String get feedNews => 'Calendar'; + + @override + String get feedNewsType => 'News type'; + + @override + String get feedNoAssociationEvents => 'No association events'; + + @override + String get feedApply => 'Apply'; + + @override + String get feedAdmin => 'Administration'; + + @override + String get feedCreateAnEvent => 'Create an event'; + + @override + String get feedManageRequests => 'Manage requests'; + + @override + String get feedNoNewsAvailable => 'No news available'; + + @override + String get feedRefresh => 'Refresh'; + + @override + String get feedPleaseProvideASGExternalLink => + 'Please provide a SG external link'; + + @override + String get feedPleaseProvideASGDate => 'Please provide a SG date'; + + @override + String feedShotgunIn(String time) { + return 'Shotgun in $time'; + } + + @override + String feedVoteIn(String time) { + return 'Vote in $time'; + } + + @override + String get feedCantOpenLink => 'Can\'t open link'; + + @override + String get feedGetReady => 'Get ready!'; + + @override + String get eventActionCampaign => 'You can vote'; + + @override + String get eventActionEvent => 'You are invited'; + + @override + String get eventActionCampaignSubtitle => 'Vote now'; + + @override + String get eventActionEventSubtitle => 'Answer the invitation'; + + @override + String get eventActionCampaignButton => 'Vote'; + + @override + String get eventActionEventButton => 'Reserve'; + + @override + String get eventActionCampaignValidated => 'I voted!'; + + @override + String get eventActionEventValidated => 'I\'m coming!'; + + @override + String get adminAccountTypes => 'Account types'; + + @override + String get adminAdd => 'Add'; + + @override + String get adminAddGroup => 'Add group'; + + @override + String get adminAddMember => 'Add member'; + + @override + String get adminAddedGroup => 'Group created'; + + @override + String get adminAddedLoaner => 'Lender added'; + + @override + String get adminAddedMember => 'Member added'; + + @override + String get adminAddingError => 'Error while adding'; + + @override + String get adminAddingMember => 'Adding a member'; + + @override + String get adminAddLoaningGroup => 'Add loaning group'; + + @override + String get adminAddSchool => 'Add school'; + + @override + String get adminAddStructure => 'Add structure'; + + @override + String get adminAddedSchool => 'School created'; + + @override + String get adminAddedStructure => 'Structure added'; + + @override + String get adminEditedStructure => 'Structure edited'; + + @override + String get adminAdministration => 'Administration'; + + @override + String get adminAssociationMembership => 'Membership'; + + @override + String get adminAssociationMembershipName => 'Membership name'; + + @override + String get adminAssociationsMemberships => 'Memberships'; + + @override + String adminBankAccountHolder(String bankAccountHolder) { + return 'Bank account holder: $bankAccountHolder'; + } + + @override + String get adminBankAccountHolderModified => 'Bank account holder modified'; + + @override + String get adminBankDetails => 'Bank details'; + + @override + String get adminBic => 'BIC'; + + @override + String get adminBicError => 'BIC must be 11 characters'; + + @override + String get adminCity => 'City'; + + @override + String get adminClearFilters => 'Clear filters'; + + @override + String get adminCountry => 'Country'; + + @override + String get adminCreateAssociationMembership => 'Create membership'; + + @override + String get adminCreatedAssociationMembership => 'Membership created'; + + @override + String get adminCreationError => 'Error during creation'; + + @override + String get adminDateError => 'Start date must be before end date'; + + @override + String get adminDefineAsBankAccountHolder => 'Define as bank account holder'; + + @override + String get adminDelete => 'Delete'; + + @override + String get adminDeleteAssociationMember => 'Delete member?'; + + @override + String get adminDeleteAssociationMemberConfirmation => + 'Are you sure you want to delete this member?'; + + @override + String get adminDeleteAssociationMembership => 'Delete membership?'; + + @override + String get adminDeletedAssociationMembership => 'Membership deleted'; + + @override + String get adminDeleteGroup => 'Delete group?'; + + @override + String get adminDeletedGroup => 'Group deleted'; + + @override + String get adminDeleteSchool => 'Delete school?'; + + @override + String get adminDeletedSchool => 'School deleted'; + + @override + String get adminDeleting => 'Deleting'; + + @override + String get adminDeletingError => 'Error while deleting'; + + @override + String get adminDescription => 'Description'; + + @override + String get adminEdit => 'Edit'; + + @override + String get adminEditStructure => 'Edit structure'; + + @override + String get adminEditMembership => 'Edit membership'; + + @override + String get adminEmptyDate => 'Empty date'; + + @override + String get adminEmptyFieldError => 'Name cannot be empty'; + + @override + String get adminEmailFailed => + 'Unable to send email to the following addresses'; + + @override + String get adminEmailRegex => 'Email Regex'; + + @override + String get adminEmptyUser => 'Empty user'; + + @override + String get adminEndDate => 'End date'; + + @override + String get adminEndDateMaximal => 'Maximum end date'; + + @override + String get adminEndDateMinimal => 'Minimum end date'; + + @override + String get adminError => 'Error'; + + @override + String get adminFilters => 'Filters'; + + @override + String get adminGroup => 'Group'; + + @override + String get adminGroups => 'Groups'; + + @override + String get adminIban => 'IBAN'; + + @override + String get adminIbanError => 'IBAN must be 27 characters'; + + @override + String get adminLoaningGroup => 'Loaning group'; + + @override + String get adminLooking => 'Searching'; + + @override + String get adminManager => 'Structure administrator'; + + @override + String get adminMaximum => 'Maximum'; + + @override + String get adminMembers => 'Members'; + + @override + String get adminMembershipAddingError => + 'Error while adding (likely due to overlapping dates)'; + + @override + String get adminMemberships => 'Memberships'; + + @override + String get adminMembershipUpdatingError => + 'Error while updating (likely due to overlapping dates)'; + + @override + String get adminMinimum => 'Minimum'; + + @override + String get adminModifyModuleVisibility => 'Module visibility'; + + @override + String get adminName => 'Name'; + + @override + String get adminNoGroup => 'No group'; + + @override + String get adminNoManager => 'No manager selected'; + + @override + String get adminNoMember => 'No member'; + + @override + String get adminNoMoreLoaner => 'No lender available'; + + @override + String get adminNoSchool => 'No school'; + + @override + String get adminRemoveGroupMember => 'Remove member from group?'; + + @override + String get adminResearch => 'Search'; + + @override + String get adminSchools => 'Schools'; + + @override + String get adminShortId => 'Short ID (3 letters)'; + + @override + String get adminShortIdError => 'Short ID must be 3 characters'; + + @override + String get adminSiegeAddress => 'Head office address'; + + @override + String get adminSiret => 'SIRET'; + + @override + String get adminSiretError => 'SIRET must be 14 digits'; + + @override + String get adminStreet => 'Street and number'; + + @override + String get adminStructures => 'Structures'; + + @override + String get adminStartDate => 'Start date'; + + @override + String get adminStartDateMaximal => 'Maximum start date'; + + @override + String get adminStartDateMinimal => 'Minimum start date'; + + @override + String get adminUndefinedBankAccountHolder => + 'Bank account holder not defined'; + + @override + String get adminUpdatedAssociationMembership => 'Membership updated'; + + @override + String get adminUpdatedGroup => 'Group updated'; + + @override + String get adminUpdatedMembership => 'Membership updated'; + + @override + String get adminUpdatingError => 'Error while updating'; + + @override + String get adminUser => 'User'; + + @override + String get adminValidateFilters => 'Apply filters'; + + @override + String get adminVisibilities => 'Visibilities'; + + @override + String get adminZipcode => 'Zip code'; + + @override + String get adminGroupNotification => 'Group notifications'; + + @override + String adminNotifyGroup(String groupName) { + return 'Send a notification'; + } + + @override + String get adminTitle => 'Title'; + + @override + String get adminContent => 'Content'; + + @override + String get adminSend => 'Send'; + + @override + String get adminNotificationSent => 'Notification sent'; + + @override + String get adminFailedToSendNotification => 'Failed to send notification'; + + @override + String get adminGroupsManagement => 'Groups management'; + + @override + String get adminEditGroup => 'Edit group'; + + @override + String get adminManageMembers => 'Manage members'; + + @override + String get adminDeleteGroupConfirmation => + 'Are you sure you want to delete this group?'; + + @override + String get adminFailedToDeleteGroup => 'Failed to delete group'; + + @override + String get adminUsersAndGroups => 'Users and groups'; + + @override + String get adminUsersManagement => 'Users management'; + + @override + String get adminUsersManagementDescription => + 'Manage users, groups, and associations'; + + @override + String get adminManageUserGroups => 'Manage user groups'; + + @override + String get adminSendNotificationToGroup => 'Send notification to group'; + + @override + String get adminPaiementModule => 'Payment module'; + + @override + String get adminPaiement => 'Payment'; + + @override + String get adminManagePaiementStructures => 'Manage payment structures'; + + @override + String get adminManageUsersAssociationMemberships => + 'Manage users\' association memberships'; + + @override + String get adminAssociationMembershipsManagement => + 'Association memberships management'; + + @override + String get adminChooseGroupManager => + 'Choose a group to manage this membership'; + + @override + String get adminSelectManager => 'Select a manager'; + + @override + String get adminImportList => 'Import a list'; + + @override + String get adminImportUsersDescription => + 'Import users from a CSV file. The CSV file must contain one email address per line.'; + + @override + String get adminFailedToInviteUsers => 'Failed to invite users'; + + @override + String get adminDeleteUsers => 'Delete users'; + + @override + String get adminAdmin => 'Admin'; + + @override + String get adminAssociations => 'Associations'; + + @override + String get adminManageAssociations => 'Manage associations'; + + @override + String get adminAddAssociation => 'Add association'; + + @override + String get adminAssociationName => 'Association name'; + + @override + String get adminSelectGroupAssociationManager => + 'Select a group to manage this association'; + + @override + String adminEditAssociation(String associationName) { + return 'Edit association : $associationName'; + } + + @override + String adminManagerGroup(String groupName) { + return 'Manager group : $groupName'; + } + + @override + String get adminAssociationCreated => 'Association created'; + + @override + String get adminAssociationUpdated => 'Association updated'; + + @override + String get adminAssociationCreationError => + 'Error while creating association'; + + @override + String get adminAssociationUpdateError => 'Error while updating association'; + + @override + String get adminInvite => 'Invite'; + + @override + String get adminInvitedUsers => 'Invited users'; + + @override + String get adminInviteUsers => 'Invite users'; + + @override + String adminInviteUsersCounter(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count users', + one: '$count user', + zero: 'No user', + ); + return '$_temp0 in the CSV file'; + } + + @override + String get adminUpdatedAssociationLogo => 'Association logo updated'; + + @override + String get adminTooHeavyLogo => 'Logo too heavy, maximum size is 4MB'; + + @override + String get adminFailedToUpdateAssociationLogo => + 'Failed to update association logo'; + + @override + String get adminChooseGroup => 'Choose a group'; + + @override + String get adminChooseAssociationManagerGroup => + 'Choose a group to manage this association'; + + @override + String get advertAdd => 'Add'; + + @override + String get advertAddedAdvert => 'Advert published'; + + @override + String get advertAddedAnnouncer => 'Announcer added'; + + @override + String get advertAddingError => 'Error while adding'; + + @override + String get advertAdmin => 'Admin'; + + @override + String get advertAdvert => 'Advert'; + + @override + String get advertChoosingAnnouncer => 'Please choose an announcer'; + + @override + String get advertChoosingPoster => 'Please choose an image'; + + @override + String get advertContent => 'Content'; + + @override + String get advertDeleteAdvert => 'Delete ad?'; + + @override + String get advertDeleteAnnouncer => 'Delete announcer?'; + + @override + String get advertDeleting => 'Deleting'; + + @override + String get advertEdit => 'Edit'; + + @override + String get advertEditedAdvert => 'Advert edited'; + + @override + String get advertEditingError => 'Error while editing'; + + @override + String get advertGroupAdvert => 'Group'; + + @override + String get advertIncorrectOrMissingFields => 'Incorrect or missing fields'; + + @override + String get advertInvalidNumber => 'Please enter a number'; + + @override + String get advertManagement => 'Management'; + + @override + String get advertModifyAnnouncingGroup => 'Edit announcement group'; + + @override + String get advertNoMoreAnnouncer => 'No more announcers available'; + + @override + String get advertNoValue => 'Please enter a value'; + + @override + String get advertPositiveNumber => 'Please enter a positive number'; + + @override + String get advertPublishToFeed => 'Publish to feed?'; + + @override + String get advertNotification => 'Send a notification'; + + @override + String get advertRemovedAnnouncer => 'Announcer removed'; + + @override + String get advertRemovingError => 'Error during removal'; + + @override + String get advertTags => 'Tags'; + + @override + String get advertTitle => 'Title'; + + @override + String get advertMonthJan => 'Jan'; + + @override + String get advertMonthFeb => 'Feb'; + + @override + String get advertMonthMar => 'Mar'; + + @override + String get advertMonthApr => 'Apr'; + + @override + String get advertMonthMay => 'May'; + + @override + String get advertMonthJun => 'Jun'; + + @override + String get advertMonthJul => 'Jul'; + + @override + String get advertMonthAug => 'Aug'; + + @override + String get advertMonthSep => 'Sep'; + + @override + String get advertMonthOct => 'Oct'; + + @override + String get advertMonthNov => 'Nov'; + + @override + String get advertMonthDec => 'Dec'; + + @override + String get amapAccounts => 'Accounts'; + + @override + String get amapAdd => 'Add'; + + @override + String get amapAddDelivery => 'Add delivery'; + + @override + String get amapAddedCommand => 'Order added'; + + @override + String get amapAddedOrder => 'Order added'; + + @override + String get amapAddedProduct => 'Product added'; + + @override + String get amapAddedUser => 'User added'; + + @override + String get amapAddProduct => 'Add product'; + + @override + String get amapAddUser => 'Add user'; + + @override + String get amapAddingACommand => 'Add an order'; + + @override + String get amapAddingCommand => 'Add the order'; + + @override + String get amapAddingError => 'Error while adding'; + + @override + String get amapAddingProduct => 'Add a product'; + + @override + String get amapAddOrder => 'Add an order'; + + @override + String get amapAdmin => 'Admin'; + + @override + String get amapAlreadyExistCommand => 'An order already exists for this date'; + + @override + String get amapAmap => 'Amap'; + + @override + String get amapAmount => 'Balance'; + + @override + String get amapArchive => 'Archive'; + + @override + String get amapArchiveDelivery => 'Archive'; + + @override + String get amapArchivingDelivery => 'Archiving delivery'; + + @override + String get amapCategory => 'Category'; + + @override + String get amapCloseDelivery => 'Lock'; + + @override + String get amapCommandDate => 'Order date'; + + @override + String get amapCommandProducts => 'Order products'; + + @override + String get amapConfirm => 'Confirm'; + + @override + String get amapContact => 'Association contacts'; + + @override + String get amapCreateCategory => 'Create category'; + + @override + String get amapDelete => 'Delete'; + + @override + String get amapDeleteDelivery => 'Delete delivery?'; + + @override + String get amapDeleteDeliveryDescription => + 'Are you sure you want to delete this delivery?'; + + @override + String get amapDeletedDelivery => 'Delivery deleted'; + + @override + String get amapDeletedOrder => 'Order deleted'; + + @override + String get amapDeletedProduct => 'Product deleted'; + + @override + String get amapDeleteProduct => 'Delete product?'; + + @override + String get amapDeleteProductDescription => + 'Are you sure you want to delete this product?'; + + @override + String get amapDeleting => 'Deleting'; + + @override + String get amapDeletingDelivery => 'Delete delivery?'; + + @override + String get amapDeletingError => 'Error while deleting'; + + @override + String get amapDeletingOrder => 'Delete order?'; + + @override + String get amapDeletingProduct => 'Delete product?'; + + @override + String get amapDeliver => 'Delivery completed?'; + + @override + String get amapDeliveries => 'Deliveries'; + + @override + String get amapDeliveringDelivery => 'Are all orders delivered?'; + + @override + String get amapDelivery => 'Delivery'; + + @override + String get amapDeliveryArchived => 'Delivery archived'; + + @override + String get amapDeliveryDate => 'Delivery date'; + + @override + String get amapDeliveryDelivered => 'Delivery completed'; + + @override + String get amapDeliveryHistory => 'Delivery history'; + + @override + String get amapDeliveryList => 'Delivery list'; + + @override + String get amapDeliveryLocked => 'Delivery locked'; + + @override + String get amapDeliveryOn => 'Delivery on'; + + @override + String get amapDeliveryOpened => 'Delivery opened'; + + @override + String get amapDeliveryNotArchived => 'Delivery not archived'; + + @override + String get amapDeliveryNotLocked => 'Delivery not locked'; + + @override + String get amapDeliveryNotDelivered => 'Delivery not completed'; + + @override + String get amapDeliveryNotOpened => 'Delivery not opened'; + + @override + String get amapEditDelivery => 'Edit delivery'; + + @override + String get amapEditedCommand => 'Order edited'; + + @override + String get amapEditingError => 'Error while editing'; + + @override + String get amapEditProduct => 'Edit product'; + + @override + String get amapEndingDelivery => 'End of delivery'; + + @override + String get amapError => 'Error'; + + @override + String get amapErrorLink => 'Error opening link'; + + @override + String get amapErrorLoadingUser => 'Error loading users'; + + @override + String get amapEvening => 'Evening'; + + @override + String get amapExpectingNumber => 'Please enter a number'; + + @override + String get amapFillField => 'Please fill out this field'; + + @override + String get amapHandlingAccount => 'Manage accounts'; + + @override + String get amapLoading => 'Loading...'; + + @override + String get amapLoadingError => 'Loading error'; + + @override + String get amapLock => 'Lock'; + + @override + String get amapLocked => 'Locked'; + + @override + String get amapLockedDelivery => 'Delivery locked'; + + @override + String get amapLockedOrder => 'Order locked'; + + @override + String get amapLooking => 'Search'; + + @override + String get amapLockingDelivery => 'Lock delivery?'; + + @override + String get amapMidDay => 'Midday'; + + @override + String get amapMyOrders => 'My orders'; + + @override + String get amapName => 'Name'; + + @override + String get amapNextStep => 'Next step'; + + @override + String get amapNoProduct => 'No product'; + + @override + String get amapNoCurrentOrder => 'No current order'; + + @override + String get amapNoMoney => 'Not enough money'; + + @override + String get amapNoOpennedDelivery => 'No open delivery'; + + @override + String get amapNoOrder => 'No order'; + + @override + String get amapNoSelectedDelivery => 'No delivery selected'; + + @override + String get amapNotEnoughMoney => 'Not enough money'; + + @override + String get amapNotPlannedDelivery => 'No scheduled delivery'; + + @override + String get amapOneOrder => 'order'; + + @override + String get amapOpenDelivery => 'Open'; + + @override + String get amapOpened => 'Opened'; + + @override + String get amapOpenningDelivery => 'Open delivery?'; + + @override + String get amapOrder => 'Order'; + + @override + String get amapOrders => 'Orders'; + + @override + String get amapPickChooseCategory => + 'Please enter a value or choose an existing category'; + + @override + String get amapPickDeliveryMoment => 'Choose a delivery time'; + + @override + String get amapPresentation => 'Presentation'; + + @override + String get amapPresentation1 => + 'The AMAP (association for the preservation of small-scale farming) is a service offered by the Planet&Co association of ECL. You can receive products (fruit and vegetable baskets, juices, jams...) directly on campus!\n\nOrders must be placed before Friday at 9 PM and are delivered on campus on Tuesday from 1:00 PM to 1:45 PM (or from 6:15 PM to 6:30 PM if you can\'t come at midday) in the M16 hall.\n\nYou can only order if your balance allows it. You can top up your balance via the Lydia collection or by cheque during office hours.\n\nLydia top-up link: '; + + @override + String get amapPresentation2 => + '\n\nFeel free to contact us if you have any issues!'; + + @override + String get amapPrice => 'Price'; + + @override + String get amapProduct => 'product'; + + @override + String get amapProducts => 'Products'; + + @override + String get amapProductInDelivery => 'Product in an unfinished delivery'; + + @override + String get amapQuantity => 'Quantity'; + + @override + String get amapRequiredDate => 'Date is required'; + + @override + String get amapSeeMore => 'See more'; + + @override + String get amapThe => 'The'; + + @override + String get amapUnlock => 'Unlock'; + + @override + String get amapUnlockedDelivery => 'Delivery unlocked'; + + @override + String get amapUnlockingDelivery => 'Unlock delivery?'; + + @override + String get amapUpdate => 'Edit'; + + @override + String get amapUpdatedAmount => 'Balance updated'; + + @override + String get amapUpdatedOrder => 'Order updated'; + + @override + String get amapUpdatedProduct => 'Product updated'; + + @override + String get amapUpdatingError => 'Update failed'; + + @override + String get amapUsersNotFound => 'No users found'; + + @override + String get amapWaiting => 'Pending'; + + @override + String get bookingAdd => 'Add'; + + @override + String get bookingAddBookingPage => 'Request'; + + @override + String get bookingAddRoom => 'Add room'; + + @override + String get bookingAddBooking => 'Add booking'; + + @override + String get bookingAddedBooking => 'Request added'; + + @override + String get bookingAddedRoom => 'Room added'; + + @override + String get bookingAddedManager => 'Manager added'; + + @override + String get bookingAddingError => 'Error while adding'; + + @override + String get bookingAddManager => 'Add manager'; + + @override + String get bookingAdminPage => 'Admin'; + + @override + String get bookingAllDay => 'All day'; + + @override + String get bookingBookedFor => 'Booked for'; + + @override + String get bookingBooking => 'Booking'; + + @override + String get bookingBookingCreated => 'Booking created'; + + @override + String get bookingBookingDemand => 'Booking request'; + + @override + String get bookingBookingNote => 'Booking note'; + + @override + String get bookingBookingPage => 'Booking'; + + @override + String get bookingBookingReason => 'Booking reason'; + + @override + String get bookingBy => 'by'; + + @override + String get bookingConfirm => 'Confirm'; + + @override + String get bookingConfirmation => 'Confirmation'; + + @override + String get bookingConfirmBooking => 'Confirm the booking?'; + + @override + String get bookingConfirmed => 'Confirmed'; + + @override + String get bookingDates => 'Dates'; + + @override + String get bookingDecline => 'Decline'; + + @override + String get bookingDeclineBooking => 'Decline the booking?'; + + @override + String get bookingDeclined => 'Declined'; + + @override + String get bookingDelete => 'Delete'; + + @override + String get bookingDeleting => 'Deleting'; + + @override + String get bookingDeleteBooking => 'Deleting'; + + @override + String get bookingDeleteBookingConfirmation => + 'Are you sure you want to delete this booking?'; + + @override + String get bookingDeletedBooking => 'Booking deleted'; + + @override + String get bookingDeletedRoom => 'Room deleted'; + + @override + String get bookingDeletedManager => 'Manager deleted'; + + @override + String get bookingDeleteRoomConfirmation => + 'Are you sure you want to delete this room?\n\nThe room must have no current or upcoming bookings to be deleted'; + + @override + String get bookingDeleteManagerConfirmation => + 'Are you sure you want to delete this manager?\n\nThe manager must not be associated with any room to be deleted'; + + @override + String get bookingDeletingBooking => 'Delete the booking?'; + + @override + String get bookingDeletingError => 'Error while deleting'; + + @override + String get bookingDeletingRoom => 'Delete the room?'; + + @override + String get bookingEdit => 'Edit'; + + @override + String get bookingEditBooking => 'Edit a booking'; + + @override + String get bookingEditionError => 'Error while editing'; + + @override + String get bookingEditedBooking => 'Booking edited'; + + @override + String get bookingEditedRoom => 'Room edited'; + + @override + String get bookingEditedManager => 'Manager edited'; + + @override + String get bookingEditManager => 'Edit or delete a manager'; + + @override + String get bookingEditRoom => 'Edit or delete a room'; + + @override + String get bookingEndDate => 'End date'; + + @override + String get bookingEndHour => 'End hour'; + + @override + String get bookingEntity => 'For whom?'; + + @override + String get bookingError => 'Error'; + + @override + String get bookingEventEvery => 'Every'; + + @override + String get bookingHistoryPage => 'History'; + + @override + String get bookingIncorrectOrMissingFields => 'Incorrect or missing fields'; + + @override + String get bookingInterval => 'Interval'; + + @override + String get bookingInvalidIntervalError => 'Invalid interval'; + + @override + String get bookingInvalidDates => 'Invalid dates'; + + @override + String get bookingInvalidRoom => 'Invalid room'; + + @override + String get bookingKeysRequested => 'Keys requested'; + + @override + String get bookingManagement => 'Management'; + + @override + String get bookingManager => 'Manager'; + + @override + String get bookingManagerName => 'Manager name'; + + @override + String get bookingMultipleDay => 'Multiple days'; + + @override + String get bookingMyBookings => 'My bookings'; + + @override + String get bookingNecessaryKey => 'Key needed'; + + @override + String get bookingNext => 'Next'; + + @override + String get bookingNo => 'No'; + + @override + String get bookingNoCurrentBooking => 'No current booking'; + + @override + String get bookingNoDateError => 'Please choose a date'; + + @override + String get bookingNoAppointmentInReccurence => + 'No slot exists with these recurrence settings'; + + @override + String get bookingNoDaySelected => 'No day selected'; + + @override + String get bookingNoDescriptionError => 'Please enter a description'; + + @override + String get bookingNoKeys => 'No keys'; + + @override + String get bookingNoNoteError => 'Please enter a note'; + + @override + String get bookingNoPhoneRegistered => 'Number not provided'; + + @override + String get bookingNoReasonError => 'Please enter a reason'; + + @override + String get bookingNoRoomFoundError => 'No room registered'; + + @override + String get bookingNoRoomFound => 'No room found'; + + @override + String get bookingNote => 'Note'; + + @override + String get bookingOther => 'Other'; + + @override + String get bookingPending => 'Pending'; + + @override + String get bookingPrevious => 'Previous'; + + @override + String get bookingReason => 'Reason'; + + @override + String get bookingRecurrence => 'Recurrence'; + + @override + String get bookingRecurrenceDays => 'Recurrence days'; + + @override + String get bookingRecurrenceEndDate => 'Recurrence end date'; + + @override + String get bookingRecurrent => 'Recurrent'; + + @override + String get bookingRegisteredRooms => 'Registered rooms'; + + @override + String get bookingRoom => 'Room'; + + @override + String get bookingRoomName => 'Room name'; + + @override + String get bookingStartDate => 'Start date'; + + @override + String get bookingStartHour => 'Start hour'; + + @override + String get bookingWeeks => 'Weeks'; + + @override + String get bookingYes => 'Yes'; + + @override + String get bookingWeekDayMon => 'Monday'; + + @override + String get bookingWeekDayTue => 'Tuesday'; + + @override + String get bookingWeekDayWed => 'Wednesday'; + + @override + String get bookingWeekDayThu => 'Thursday'; + + @override + String get bookingWeekDayFri => 'Friday'; + + @override + String get bookingWeekDaySat => 'Saturday'; + + @override + String get bookingWeekDaySun => 'Sunday'; + + @override + String get cinemaAdd => 'Add'; + + @override + String get cinemaAddedSession => 'Session added'; + + @override + String get cinemaAddingError => 'Error while adding'; + + @override + String get cinemaAddSession => 'Add a session'; + + @override + String get cinemaCinema => 'Cinema'; + + @override + String get cinemaDeleteSession => 'Delete the session?'; + + @override + String get cinemaDeleting => 'Deleting'; + + @override + String get cinemaDuration => 'Duration'; + + @override + String get cinemaEdit => 'Edit'; + + @override + String get cinemaEditedSession => 'Session edited'; + + @override + String get cinemaEditingError => 'Error while editing'; + + @override + String get cinemaEditSession => 'Edit the session'; + + @override + String get cinemaEmptyUrl => 'Please enter a URL'; + + @override + String get cinemaImportFromTMDB => 'Import from TMDB'; + + @override + String get cinemaIncomingSession => 'Now showing'; + + @override + String get cinemaIncorrectOrMissingFields => 'Incorrect or missing fields'; + + @override + String get cinemaInvalidUrl => 'Invalid URL'; + + @override + String get cinemaGenre => 'Genre'; + + @override + String get cinemaName => 'Name'; + + @override + String get cinemaNoDateError => 'Please enter a date'; + + @override + String get cinemaNoDuration => 'Please enter a duration'; + + @override + String get cinemaNoOverview => 'No synopsis'; + + @override + String get cinemaNoPoster => 'No poster'; + + @override + String get cinemaNoSession => 'No session'; + + @override + String get cinemaOverview => 'Synopsis'; + + @override + String get cinemaPosterUrl => 'Poster URL'; + + @override + String get cinemaSessionDate => 'Session day'; + + @override + String get cinemaStartHour => 'Start hour'; + + @override + String get cinemaTagline => 'Tagline'; + + @override + String get cinemaThe => 'The'; + + @override + String get drawerAdmin => 'Administration'; + + @override + String get drawerAndroidAppLink => + 'https://play.google.com/store/apps/details?id=fr.myecl.titan'; + + @override + String get drawerCopied => 'Copied!'; + + @override + String get drawerDownloadAppOnMobileDevice => + 'This site is the web version of the MyECL app. We invite you to download the app. Use this site only if you have problems with the app.\n'; + + @override + String get drawerIosAppLink => + 'https://apps.apple.com/fr/app/myecl/id6444443430'; + + @override + String get drawerLoginOut => 'Do you want to log out?'; + + @override + String get drawerLogOut => 'Log out'; + + @override + String get drawerOr => ' or '; + + @override + String get drawerSettings => 'Settings'; + + @override + String get eventAdd => 'Add'; + + @override + String get eventAddEvent => 'Add an event'; + + @override + String get eventAddedEvent => 'Event added'; + + @override + String get eventAddingError => 'Error while adding'; + + @override + String get eventAllDay => 'All day'; + + @override + String get eventConfirm => 'Confirm'; + + @override + String get eventConfirmEvent => 'Confirm the event?'; + + @override + String get eventConfirmation => 'Confirmation'; + + @override + String get eventConfirmed => 'Confirmed'; + + @override + String get eventDates => 'Dates'; + + @override + String get eventDecline => 'Decline'; + + @override + String get eventDeclineEvent => 'Decline the event?'; + + @override + String get eventDeclined => 'Declined'; + + @override + String get eventDelete => 'Delete'; + + @override + String eventDeleteConfirm(String name) { + return 'Delete the event $name?'; + } + + @override + String get eventDeletedEvent => 'Event deleted'; + + @override + String get eventDeleting => 'Deleting'; + + @override + String get eventDeletingError => 'Error while deleting'; + + @override + String get eventDeletingEvent => 'Delete the event?'; + + @override + String get eventDescription => 'Description'; + + @override + String get eventEdit => 'Edit'; + + @override + String get eventEditEvent => 'Edit an event'; + + @override + String get eventEditedEvent => 'Event edited'; + + @override + String get eventEditingError => 'Error while editing'; + + @override + String get eventEndDate => 'End date'; + + @override + String get eventEndHour => 'End hour'; + + @override + String get eventError => 'Error'; + + @override + String get eventEventList => 'Event list'; + + @override + String get eventEventType => 'Event type'; + + @override + String get eventEvery => 'Every'; + + @override + String get eventHistory => 'History'; + + @override + String get eventIncorrectOrMissingFields => + 'Some fields are incorrect or missing'; + + @override + String get eventInterval => 'Interval'; + + @override + String get eventInvalidDates => 'End date must be after start date'; + + @override + String get eventInvalidIntervalError => 'Please enter a valid interval'; + + @override + String get eventLocation => 'Location'; + + @override + String get eventModifiedEvent => 'Event modified'; + + @override + String get eventModifyingError => 'Error while modifying'; + + @override + String get eventMyEvents => 'My events'; + + @override + String get eventName => 'Name'; + + @override + String get eventNext => 'Next'; + + @override + String get eventNo => 'No'; + + @override + String get eventNoCurrentEvent => 'No current event'; + + @override + String get eventNoDateError => 'Please enter a date'; + + @override + String get eventNoDaySelected => 'No day selected'; + + @override + String get eventNoDescriptionError => 'Please enter a description'; + + @override + String get eventNoEvent => 'No event'; + + @override + String get eventNoNameError => 'Please enter a name'; + + @override + String get eventNoOrganizerError => 'Please enter an organizer'; + + @override + String get eventNoPlaceError => 'Please enter a location'; + + @override + String get eventNoPhoneRegistered => 'Number not provided'; + + @override + String get eventNoRuleError => 'Please enter a recurrence rule'; + + @override + String get eventOrganizer => 'Organizer'; + + @override + String get eventOther => 'Other'; + + @override + String get eventPending => 'Pending'; + + @override + String get eventPrevious => 'Previous'; + + @override + String get eventRecurrence => 'Recurrence'; + + @override + String get eventRecurrenceDays => 'Recurrence days'; + + @override + String get eventRecurrenceEndDate => 'Recurrence end date'; + + @override + String get eventRecurrenceRule => 'Recurrence rule'; + + @override + String get eventRoom => 'Room'; + + @override + String get eventStartDate => 'Start date'; + + @override + String get eventStartHour => 'Start hour'; + + @override + String get eventTitle => 'Events'; + + @override + String get eventYes => 'Yes'; + + @override + String get eventEventEvery => 'Every'; + + @override + String get eventWeeks => 'weeks'; + + @override + String get eventDayMon => 'Monday'; + + @override + String get eventDayTue => 'Tuesday'; + + @override + String get eventDayWed => 'Wednesday'; + + @override + String get eventDayThu => 'Thursday'; + + @override + String get eventDayFri => 'Friday'; + + @override + String get eventDaySat => 'Saturday'; + + @override + String get eventDaySun => 'Sunday'; + + @override + String get globalConfirm => 'Confirm'; + + @override + String get globalCancel => 'Cancel'; + + @override + String get globalIrreversibleAction => 'This action is irreversible'; + + @override + String globalOptionnal(String text) { + return '$text (Optional)'; + } + + @override + String get homeCalendar => 'Calendar'; + + @override + String get homeEventOf => 'Events of'; + + @override + String get homeIncomingEvents => 'Upcoming events'; + + @override + String get homeLastInfos => 'Latest announcements'; + + @override + String get homeNoEvents => 'No events'; + + @override + String get homeTranslateDayShortMon => 'Mon'; + + @override + String get homeTranslateDayShortTue => 'Tue'; + + @override + String get homeTranslateDayShortWed => 'Wed'; + + @override + String get homeTranslateDayShortThu => 'Thu'; + + @override + String get homeTranslateDayShortFri => 'Fri'; + + @override + String get homeTranslateDayShortSat => 'Sat'; + + @override + String get homeTranslateDayShortSun => 'Sun'; + + @override + String get loanAdd => 'Add'; + + @override + String get loanAddLoan => 'Add a loan'; + + @override + String get loanAddObject => 'Add an object'; + + @override + String get loanAddedLoan => 'Loan added'; + + @override + String get loanAddedObject => 'Object added'; + + @override + String get loanAddedRoom => 'Room added'; + + @override + String get loanAddingError => 'Error while adding'; + + @override + String get loanAdmin => 'Administrator'; + + @override + String get loanAvailable => 'Available'; + + @override + String get loanAvailableMultiple => 'Available'; + + @override + String get loanBorrowed => 'Borrowed'; + + @override + String get loanBorrowedMultiple => 'Borrowed'; + + @override + String get loanAnd => 'and'; + + @override + String get loanAssociation => 'Association'; + + @override + String get loanAvailableItems => 'Available items'; + + @override + String get loanBeginDate => 'Loan start date'; + + @override + String get loanBorrower => 'Borrower'; + + @override + String get loanCaution => 'Deposit'; + + @override + String get loanCancel => 'Cancel'; + + @override + String get loanConfirm => 'Confirm'; + + @override + String get loanConfirmation => 'Confirmation'; + + @override + String get loanDates => 'Dates'; + + @override + String get loanDays => 'Days'; + + @override + String get loanDelay => 'Extension delay'; + + @override + String get loanDelete => 'Delete'; + + @override + String get loanDeletingLoan => 'Delete the loan?'; + + @override + String get loanDeletedItem => 'Object deleted'; + + @override + String get loanDeletedLoan => 'Loan deleted'; + + @override + String get loanDeleting => 'Deleting'; + + @override + String get loanDeletingError => 'Error while deleting'; + + @override + String get loanDeletingItem => 'Delete the object?'; + + @override + String get loanDuration => 'Duration'; + + @override + String get loanEdit => 'Edit'; + + @override + String get loanEditItem => 'Edit the object'; + + @override + String get loanEditLoan => 'Edit the loan'; + + @override + String get loanEditedRoom => 'Room edited'; + + @override + String get loanEndDate => 'Loan end date'; + + @override + String get loanEnded => 'Ended'; + + @override + String get loanEnterDate => 'Please enter a date'; + + @override + String get loanExtendedLoan => 'Extended loan'; + + @override + String get loanExtendingError => 'Error while extending'; + + @override + String get loanHistory => 'History'; + + @override + String get loanIncorrectOrMissingFields => + 'Some fields are missing or incorrect'; + + @override + String get loanInvalidNumber => 'Please enter a number'; + + @override + String get loanInvalidDates => 'Dates are not valid'; + + @override + String get loanItem => 'Item'; + + @override + String get loanItems => 'Items'; + + @override + String get loanItemHandling => 'Item management'; + + @override + String get loanItemSelected => 'selected item'; + + @override + String get loanItemsSelected => 'selected items'; + + @override + String get loanLendingDuration => 'Possible loan duration'; + + @override + String get loanLoan => 'Loan'; + + @override + String get loanLoanHandling => 'Loan management'; + + @override + String get loanLooking => 'Searching'; + + @override + String get loanName => 'Name'; + + @override + String get loanNext => 'Next'; + + @override + String get loanNo => 'No'; + + @override + String get loanNoAssociationsFounded => 'No associations found'; + + @override + String get loanNoAvailableItems => 'No available items'; + + @override + String get loanNoBorrower => 'No borrower'; + + @override + String get loanNoItems => 'No items'; + + @override + String get loanNoItemSelected => 'No item selected'; + + @override + String get loanNoLoan => 'No loan'; + + @override + String get loanNoReturnedDate => 'No return date'; + + @override + String get loanQuantity => 'Quantity'; + + @override + String get loanNone => 'None'; + + @override + String get loanNote => 'Note'; + + @override + String get loanNoValue => 'Please enter a value'; + + @override + String get loanOnGoing => 'Ongoing'; + + @override + String get loanOnGoingLoan => 'Ongoing loan'; + + @override + String get loanOthers => 'others'; + + @override + String get loanPaidCaution => 'Deposit paid'; + + @override + String get loanPositiveNumber => 'Please enter a positive number'; + + @override + String get loanPrevious => 'Previous'; + + @override + String get loanReturned => 'Returned'; + + @override + String get loanReturnedLoan => 'Returned loan'; + + @override + String get loanReturningError => 'Error while returning'; + + @override + String get loanReturningLoan => 'Return'; + + @override + String get loanReturnLoan => 'Return the loan?'; + + @override + String get loanReturnLoanDescription => 'Do you want to return this loan?'; + + @override + String get loanToReturn => 'To return'; + + @override + String get loanUnavailable => 'Unavailable'; + + @override + String get loanUpdate => 'Edit'; + + @override + String get loanUpdatedItem => 'Item updated'; + + @override + String get loanUpdatedLoan => 'Loan updated'; + + @override + String get loanUpdatingError => 'Error while updating'; + + @override + String get loanYes => 'Yes'; + + @override + String get loginAppName => 'MyECL'; + + @override + String get loginCreateAccount => 'Create an account'; + + @override + String get loginForgotPassword => 'Forgot password?'; + + @override + String get loginFruitVegetableOrders => 'Fruit and vegetable orders'; + + @override + String get loginInterfaceCustomization => 'Interface customization'; + + @override + String get loginLoginFailed => 'Login failed'; + + @override + String get loginMadeBy => 'Developped by ProximApp'; + + @override + String get loginMaterialLoans => 'Material loans management'; + + @override + String get loginNewTermsElections => 'New terms elections'; + + @override + String get loginRaffles => 'Raffles'; + + @override + String get loginSignIn => 'Sign in'; + + @override + String get loginRegister => 'Register'; + + @override + String get loginShortDescription => 'The associative application'; + + @override + String get loginUpcomingEvents => 'Upcoming events'; + + @override + String get loginUpcomingScreenings => 'Upcoming screenings'; + + @override + String get othersCheckInternetConnection => + 'Please check your internet connection'; + + @override + String get othersRetry => 'Retry'; + + @override + String get othersTooOldVersion => + 'Your app version is too old.\n\nPlease update the app.'; + + @override + String get othersUnableToConnectToServer => 'Unable to connect to the server'; + + @override + String get othersVersion => 'Version'; + + @override + String get othersNoModule => + 'No modules available, please try again later 😢😢'; + + @override + String get othersAdmin => 'Admin'; + + @override + String get othersError => 'An error occurred'; + + @override + String get othersNoValue => 'Please enter a value'; + + @override + String get othersInvalidNumber => 'Please enter a number'; + + @override + String get othersNoDateError => 'Please enter a date'; + + @override + String get othersImageSizeTooBig => 'Image size must not exceed 4 MB'; + + @override + String get othersImageError => 'Error adding the image'; + + @override + String get paiementAccept => 'Accept'; + + @override + String get paiementAccessPage => 'Access the page'; + + @override + String get paiementAdd => 'Add'; + + @override + String get paiementAddedSeller => 'Seller added'; + + @override + String get paiementAddingSellerError => 'Error while adding seller'; + + @override + String get paiementAddingStoreError => 'Error while adding the store'; + + @override + String get paiementAddSeller => 'Add seller'; + + @override + String get paiementAddStore => 'Add store'; + + @override + String get paiementAddThisDevice => 'Add this device'; + + @override + String get paiementAdmin => 'Administrator'; + + @override + String get paiementAmount => 'Amount'; + + @override + String get paiementAskDeviceActivation => 'Device activation request'; + + @override + String get paiementAStore => 'a store'; + + @override + String get paiementAt => 'at'; + + @override + String get paiementAuthenticationRequired => 'Authentication required to pay'; + + @override + String get paiementAuthentificationFailed => 'Authentication failed'; + + @override + String get paiementBalanceAfterTopUp => 'Balance after top-up:'; + + @override + String get paiementBalanceAfterTransaction => 'Balance after payment: '; + + @override + String get paiementBank => 'Collect'; + + @override + String get paiementBillingSpace => 'Billing page'; + + @override + String get paiementCameraPermissionRequired => 'Camera permission required'; + + @override + String get paiementCameraPerssionRequiredDescription => + 'To scan a QR Code, you must allow camera access.'; + + @override + String get paiementCanBank => 'Can collect payments'; + + @override + String get paiementCanCancelTransaction => 'Can cancel transactions'; + + @override + String get paiementCancel => 'Cancel'; + + @override + String get paiementCancelled => 'Cancelled'; + + @override + String get paiementCancelledTransaction => 'Payment cancelled'; + + @override + String get paiementCancelTransaction => 'Cancel transaction'; + + @override + String get paiementCancelTransactions => 'Cancel transactions'; + + @override + String get paiementCanManageSellers => 'Can manage sellers'; + + @override + String get paiementCanSeeHistory => 'Can view history'; + + @override + String get paiementCantLaunchURL => 'Can\'t open link'; + + @override + String get paiementClose => 'Close'; + + @override + String get paiementCreate => 'Create'; + + @override + String get paiementCreateInvoice => 'Create new invoice'; + + @override + String get paiementDecline => 'Decline'; + + @override + String get paiementDeletedSeller => 'Seller deleted'; + + @override + String get paiementDeleteInvoice => 'Delete invoice'; + + @override + String get paiementDeleteSeller => 'Delete seller'; + + @override + String get paiementDeleteSellerDescription => + 'Are you sure you want to delete this seller?'; + + @override + String get paiementDeleteSuccessfully => 'Successfully deleted'; + + @override + String get paiementDeleteStore => 'Delete store'; + + @override + String get paiementDeleteStoreDescription => + 'Are you sure you want to delete this store?'; + + @override + String get paiementDeleteStoreError => 'Unable to delete the store'; + + @override + String get paiementDeletingSellerError => 'Error while deleting seller'; + + @override + String get paiementDeviceActivationReceived => + 'The activation request has been received, please check your email to finalize the process'; + + @override + String get paiementDeviceNotActivated => 'Device not activated'; + + @override + String get paiementDeviceNotActivatedDescription => + 'Your device is not yet activated. \nTo activate it, please go to the devices page.'; + + @override + String get paiementDeviceNotRegistered => 'Device not registered'; + + @override + String get paiementDeviceNotRegisteredDescription => + 'Your device is not registered yet. \nTo register it, please go to the devices page.'; + + @override + String get paiementDeviceRecoveryError => 'Error while retrieving device'; + + @override + String get paiementDeviceRevoked => 'Device revoked'; + + @override + String get paiementDeviceRevokingError => 'Error while revoking device'; + + @override + String get paiementDevices => 'Devices'; + + @override + String get paiementDoneTransaction => 'Transaction completed'; + + @override + String get paiementDownload => 'Download'; + + @override + String paiementEditStore(String store) { + return 'Edit store $store'; + } + + @override + String get paiementErrorDeleting => 'Error while deleting'; + + @override + String get paiementErrorUpdatingStatus => 'Error while updating the status'; + + @override + String paiementFromTo(DateTime from, DateTime to) { + final intl.DateFormat fromDateFormat = intl.DateFormat.yMd(localeName); + final String fromString = fromDateFormat.format(from); + final intl.DateFormat toDateFormat = intl.DateFormat.yMd(localeName); + final String toString = toDateFormat.format(to); + + return 'From $fromString to $toString'; + } + + @override + String get paiementGetBalanceError => 'Error while retrieving balance: '; + + @override + String get paiementGetTransactionsError => + 'Error while retrieving transactions: '; + + @override + String get paiementHandOver => 'Handover'; + + @override + String get paiementHistory => 'History'; + + @override + String get paiementInvoiceCreatedSuccessfully => + 'Invoice created successfully'; + + @override + String get paiementInvoices => 'Invoices'; + + @override + String paiementInvoicesPerPage(int quantity) { + return '$quantity invoices/page'; + } + + @override + String get paiementLastTransactions => 'Latest transactions'; + + @override + String get paiementLimitedTo => 'Limited to'; + + @override + String get paiementManagement => 'Management'; + + @override + String get paiementManageSellers => 'Manage sellers'; + + @override + String get paiementMarkPaid => 'Mark as paid'; + + @override + String get paiementMarkReceived => 'Mark as received'; + + @override + String get paiementMarkUnpaid => 'Mark as unpaid'; + + @override + String get paiementMaxAmount => 'The maximum wallet amount is'; + + @override + String get paiementMean => 'Average: '; + + @override + String get paiementModify => 'Edit'; + + @override + String get paiementModifyingStoreError => 'Error while updating the store'; + + @override + String get paiementModifySuccessfully => 'Successfully modified'; + + @override + String get paiementNewCGU => 'New Terms of Service'; + + @override + String get paiementNext => 'Next'; + + @override + String get paiementNextAccountable => 'Next responsible'; + + @override + String get paiementNoInvoiceToCreate => 'No invoice to create'; + + @override + String get paiementNoMembership => 'No membership'; + + @override + String get paiementNoMembershipDescription => + 'This product is not available to non-members. Confirm the payment?'; + + @override + String get paiementNoThanks => 'No thanks'; + + @override + String get paiementNoTransaction => 'No transaction'; + + @override + String get paiementNoTransactionForThisMonth => + 'No transactions for this month'; + + @override + String get paiementOf => 'of'; + + @override + String get paiementPaid => 'Paid'; + + @override + String get paiementPay => 'Pay'; + + @override + String get paiementPayment => 'Payment'; + + @override + String get paiementPayWithHA => 'Pay with HelloAsso'; + + @override + String get paiementPending => 'Pending'; + + @override + String get paiementPersonalBalance => 'Personal balance'; + + @override + String get paiementAddFunds => 'Add Funds'; + + @override + String get paiementInsufficientFunds => 'Insufficient Funds'; + + @override + String get paiementTimeRemaining => 'Time Remaining'; + + @override + String get paiementHurryUp => 'Hurry up!'; + + @override + String get paiementCompletePayment => 'Complete payment'; + + @override + String get paiementConfirmPayment => 'Confirm Payment'; + + @override + String get paiementPleaseAcceptPopup => 'Please allow popups'; + + @override + String get paiementPleaseAcceptTOS => 'Please accept the Terms of Service.'; + + @override + String get paiementPleaseAddDevice => 'Please add this device to pay'; + + @override + String get paiementPleaseAuthenticate => 'Please authenticate'; + + @override + String get paiementPleaseEnterMinAmount => + 'Please enter an amount greater than 1'; + + @override + String get paiementPleaseEnterValidAmount => 'Please enter a valid amount'; + + @override + String get paiementProceedSuccessfully => 'Payment completed successfully'; + + @override + String get paiementQRCodeAlreadyUsed => 'QR Code already used'; + + @override + String get paiementReactivateRevokedDeviceDescription => + 'Your device has been revoked. \nTo reactivate it, please go to the devices page.'; + + @override + String get paiementReceived => 'Received'; + + @override + String get paiementRefund => 'Refund'; + + @override + String get paiementRefundAction => 'Refund'; + + @override + String get paiementRefundedThe => 'Refunded on'; + + @override + String get paiementRevokeDevice => 'Revoke device?'; + + @override + String get paiementRevokeDeviceDescription => + 'You will no longer be able to use this device for payments'; + + @override + String get paiementRightsOf => 'Rights of'; + + @override + String get paiementRightsUpdated => 'Rights updated'; + + @override + String get paiementRightsUpdateError => 'Error while updating rights'; + + @override + String get paiementScan => 'Scan'; + + @override + String get paiementScanAlreadyUsedQRCode => 'QR Code already used'; + + @override + String get paiementScanCode => 'Scan a code'; + + @override + String get paiementScanNoMembership => 'No membership'; + + @override + String get paiementScanNoMembershipConfirmation => + 'This product is not available to non-members. Confirm the payment?'; + + @override + String get paiementSeeHistory => 'View history'; + + @override + String get paiementSelectStructure => 'Select a structure'; + + @override + String get paiementSellerError => 'You are not a seller of this store'; + + @override + String get paiementSellerRigths => 'Seller rights'; + + @override + String get paiementSellersOf => 'Sellers of'; + + @override + String get paiementSettings => 'Settings'; + + @override + String get paiementSpent => 'Spent'; + + @override + String get paiementStats => 'Stats'; + + @override + String get paiementStoreBalance => 'Store balance'; + + @override + String get paiementStoreDeleted => 'Store deleted'; + + @override + String paiementStructureManagement(String structure) { + return '$structure management'; + } + + @override + String get paiementStoreName => 'Store name'; + + @override + String get paiementStores => 'Stores'; + + @override + String get paiementStructureAdmin => 'Structure administrator'; + + @override + String get paiementSuccededTransaction => 'Successful payment'; + + @override + String get paiementConfirmYourPurchase => 'Confirm your purchase'; + + @override + String get paiementYourBalance => 'Your balance'; + + @override + String get paiementPaymentSuccessful => 'Payment successful!'; + + @override + String get paiementPaymentCanceled => 'Payment canceled'; + + @override + String get paiementPaymentRequest => 'Payment request'; + + @override + String get paiementPaymentRequestAccepted => 'Payment request accepted'; + + @override + String get paiementPaymentRequestRefused => 'Payment request refused'; + + @override + String get paiementPaymentRequestError => 'Error processing payment request'; + + @override + String get paiementRefuse => 'Refuse'; + + @override + String get paiementSuccessfullyAddedStore => 'Store successfully added'; + + @override + String get paiementSuccessfullyModifiedStore => 'Store successfully updated'; + + @override + String get paiementThe => 'The'; + + @override + String get paiementThisDevice => '(this device)'; + + @override + String get paiementTopUp => 'Top-up'; + + @override + String get paiementTopUpAction => 'Top-up'; + + @override + String get paiementTotalDuringPeriod => 'Total during the period'; + + @override + String get paiementTransaction => 'Transaction'; + + @override + String get paiementTransactionCancelled => 'Transaction cancelled'; + + @override + String get paiementTransactionCancelledDescription => + 'Are you sure you want to cancel the transaction of'; + + @override + String get paiementTransactionCancelledError => + 'Error while cancelling the transaction'; + + @override + String get paiementTransferStructure => 'Structure transfer'; + + @override + String get paiementTransferStructureDescription => + 'The new manager will have access to all structure management features. You will receive an email to confirm this transfer. The link will only be active for 20 minutes. This action is irreversible. Are you sure you want to continue?'; + + @override + String get paiementTransferStructureError => + 'Error while transferring structure'; + + @override + String get paiementTransferStructureSuccess => + 'Structure transfer requested successfully'; + + @override + String get paiementUnknownDevice => 'Unknown device'; + + @override + String get paiementValidUntil => 'Valid until'; + + @override + String get paiementYouAreTransferingStructureTo => + 'You are about to transfer the structure to '; + + @override + String get phAddNewJournal => 'Add a new journal'; + + @override + String get phNameField => 'Name: '; + + @override + String get phDateField => 'Date: '; + + @override + String get phDelete => 'Are you sure you want to delete this journal?'; + + @override + String get phIrreversibleAction => 'This action is irreversible'; + + @override + String get phToHeavyFile => 'File too large'; + + @override + String get phAddPdfFile => 'Add a PDF file'; + + @override + String get phEditPdfFile => 'Edit PDF file'; + + @override + String get phPhName => 'PH name'; + + @override + String get phDate => 'Date'; + + @override + String get phAdded => 'Added'; + + @override + String get phEdited => 'Edited'; + + @override + String get phAddingFileError => 'Add error'; + + @override + String get phMissingInformatonsOrPdf => 'Missing information or PDF file'; + + @override + String get phAdd => 'Add'; + + @override + String get phEdit => 'Edit'; + + @override + String get phSeePreviousJournal => 'See previous journals'; + + @override + String get phNoJournalInDatabase => 'No PH yet in database'; + + @override + String get phSuccesDowloading => 'Successfully downloaded'; + + @override + String get phonebookAdd => 'Add'; + + @override + String get phonebookAddAssociation => 'Add an association'; + + @override + String get phonebookAddAssociationGroupement => + 'Add an association groupement'; + + @override + String get phonebookAddedAssociation => 'Association added'; + + @override + String get phonebookAddedMember => 'Member added'; + + @override + String get phonebookAddingError => 'Error adding'; + + @override + String get phonebookAddMember => 'Add a member'; + + @override + String get phonebookAddRole => 'Add a role'; + + @override + String get phonebookAdmin => 'Admin'; + + @override + String get phonebookAll => 'All'; + + @override + String get phonebookApparentName => 'Public role name:'; + + @override + String get phonebookAssociation => 'Association'; + + @override + String get phonebookAssociationDetail => 'Association details:'; + + @override + String get phonebookAssociationGroupement => 'Association groupement'; + + @override + String get phonebookAssociationKind => 'Type of association:'; + + @override + String get phonebookAssociationName => 'Association name'; + + @override + String get phonebookAssociations => 'Associations'; + + @override + String get phonebookCancel => 'Cancel'; + + @override + String phonebookChangeTermYear(int year) { + return 'Switch to $year term'; + } + + @override + String get phonebookChangeTermConfirm => + 'Are you sure you want to change the entire term?\nThis action is irreversible!'; + + @override + String get phonebookClose => 'Close'; + + @override + String get phonebookConfirm => 'Confirm'; + + @override + String get phonebookCopied => 'Copied to clipboard'; + + @override + String get phonebookDeactivateAssociation => 'Deactivate association'; + + @override + String get phonebookDeactivatedAssociation => 'Association deactivated'; + + @override + String get phonebookDeactivatedAssociationWarning => + 'Warning, this association is deactivated, you cannot modify it'; + + @override + String phonebookDeactivateSelectedAssociation(String association) { + return 'Désactiver l\'association $association ?'; + } + + @override + String get phonebookDeactivatingError => 'Error during deactivation'; + + @override + String get phonebookDetail => 'Details:'; + + @override + String get phonebookDelete => 'Delete'; + + @override + String get phonebookDeleteAssociation => 'Delete association'; + + @override + String phonebookDeleteSelectedAssociation(String association) { + return 'Delete the association $association?'; + } + + @override + String get phonebookDeleteAssociationDescription => + 'This will erase all association history'; + + @override + String get phonebookDeletedAssociation => 'Association deleted'; + + @override + String get phonebookDeletedMember => 'Member deleted'; + + @override + String get phonebookDeleteRole => 'Delete role'; + + @override + String phonebookDeleteUserRole(String name) { + return 'Delete the role of $name?'; + } + + @override + String get phonebookDeactivating => 'Deactivate the association?'; + + @override + String get phonebookDeleting => 'Deleting'; + + @override + String get phonebookDeletingError => 'Error deleting'; + + @override + String get phonebookDescription => 'Description'; + + @override + String get phonebookEdit => 'Edit'; + + @override + String get phonebookEditAssociationGroupement => + 'Edit association groupement'; + + @override + String get phonebookEditAssociationGroups => 'Manage groups'; + + @override + String get phonebookEditAssociationInfo => 'Edit'; + + @override + String get phonebookEditAssociationMembers => 'Manage members'; + + @override + String get phonebookEditRole => 'Edit role'; + + @override + String get phonebookEditMembership => 'Edit role'; + + @override + String get phonebookEmail => 'Email:'; + + @override + String get phonebookEmailCopied => 'Email copied to clipboard'; + + @override + String get phonebookEmptyApparentName => 'Please enter a role name'; + + @override + String get phonebookEmptyFieldError => 'A field is not filled'; + + @override + String get phonebookEmptyKindError => 'Please choose an association type'; + + @override + String get phonebookEmptyMember => 'No member selected'; + + @override + String get phonebookErrorAssociationLoading => 'Error loading association'; + + @override + String get phonebookErrorAssociationNameEmpty => + 'Please enter an association name'; + + @override + String get phonebookErrorAssociationPicture => + 'Error editing association picture'; + + @override + String get phonebookErrorKindsLoading => 'Error loading association types'; + + @override + String get phonebookErrorLoadAssociationList => + 'Error loading association list'; + + @override + String get phonebookErrorLoadAssociationMember => + 'Error loading association members'; + + @override + String get phonebookErrorLoadAssociationPicture => + 'Error loading association picture'; + + @override + String get phonebookErrorLoadProfilePicture => 'Error'; + + @override + String get phonebookErrorRoleTagsLoading => 'Error loading role tags'; + + @override + String get phonebookExistingMembership => + 'This member is already in the current term'; + + @override + String get phonebookFilter => 'Filter'; + + @override + String get phonebookFilterDescription => + 'Filter the associations by their type'; + + @override + String get phonebookFirstname => 'First name:'; + + @override + String get phonebookGroupementDeleted => 'Association groupement deleted'; + + @override + String get phonebookGroupementDeleteError => + 'Error deleting association groupement'; + + @override + String get phonebookGroupementName => 'Groupement name'; + + @override + String phonebookGroups(String association) { + return 'Manage $association groups'; + } + + @override + String phonebookTerm(int year) { + return '$year term'; + } + + @override + String get phonebookTermChangingError => 'Error changing term'; + + @override + String get phonebookMember => 'Member'; + + @override + String get phonebookMemberReordered => 'Member reordered'; + + @override + String phonebookMembers(String association) { + return 'Manage $association members'; + } + + @override + String get phonebookMembershipAssociationError => + 'Please choose an association'; + + @override + String get phonebookMembershipRole => 'Role:'; + + @override + String get phonebookMembershipRoleError => 'Please choose a role'; + + @override + String phonebookModifyMembership(String name) { + return 'Modify $name\'s role'; + } + + @override + String get phonebookName => 'Last name:'; + + @override + String get phonebookNameCopied => 'Name and first name copied to clipboard'; + + @override + String get phonebookNamePure => 'Last name'; + + @override + String get phonebookNewTerm => 'New term'; + + @override + String get phonebookNewTermConfirmed => 'Term changed'; + + @override + String get phonebookNickname => 'Nickname:'; + + @override + String get phonebookNicknameCopied => 'Nickname copied to clipboard'; + + @override + String get phonebookNoAssociationFound => 'No association found'; + + @override + String get phonebookNoMember => 'No member'; + + @override + String get phonebookNoMemberRole => 'No role found'; + + @override + String get phonebookNoRoleTags => 'No role tags found'; + + @override + String get phonebookPhone => 'Phone:'; + + @override + String get phonebookPhonebook => 'Phonebook'; + + @override + String get phonebookPhonebookSearch => 'Search'; + + @override + String get phonebookPhonebookSearchAssociation => 'Association'; + + @override + String get phonebookPhonebookSearchField => 'Search:'; + + @override + String get phonebookPhonebookSearchName => 'Last name/First name/Nickname'; + + @override + String get phonebookPhonebookSearchRole => 'Position'; + + @override + String get phonebookPresidentRoleTag => 'Prez\''; + + @override + String get phonebookPromoNotGiven => 'Promotion not provided'; + + @override + String phonebookPromotion(int year) { + return 'Promotion $year'; + } + + @override + String get phonebookReorderingError => 'Error during reordering'; + + @override + String get phonebookResearch => 'Search'; + + @override + String get phonebookRolePure => 'Role'; + + @override + String get phonebookSearchUser => 'Search a user'; + + @override + String get phonebookTooHeavyAssociationPicture => + 'Image is too large (max 4MB)'; + + @override + String get phonebookUpdateGroups => 'Update groups'; + + @override + String get phonebookUpdatedAssociation => 'Association updated'; + + @override + String get phonebookUpdatedAssociationPicture => + 'Association picture has been changed'; + + @override + String get phonebookUpdatedGroups => 'Groups updated'; + + @override + String get phonebookUpdatedMember => 'Member updated'; + + @override + String get phonebookUpdatingError => 'Error during update'; + + @override + String get phonebookValidation => 'Validate'; + + @override + String get purchasesPurchases => 'Purchases'; + + @override + String get purchasesResearch => 'Search'; + + @override + String get purchasesNoPurchasesFound => 'No purchases found'; + + @override + String get purchasesNoTickets => 'No tickets'; + + @override + String get purchasesTicketsError => 'Error loading tickets'; + + @override + String get purchasesPurchasesError => 'Error loading purchases'; + + @override + String get purchasesNoPurchases => 'No purchase'; + + @override + String get purchasesTimes => 'times'; + + @override + String get purchasesAlreadyUsed => 'Already used'; + + @override + String get purchasesNotPaid => 'Not validated'; + + @override + String get purchasesPleaseSelectProduct => 'Please select a product'; + + @override + String get purchasesProducts => 'Products'; + + @override + String get purchasesCancel => 'Cancel'; + + @override + String get purchasesValidate => 'Validate'; + + @override + String get purchasesLeftScan => 'Scans remaining'; + + @override + String get purchasesTag => 'Tag'; + + @override + String get purchasesHistory => 'History'; + + @override + String get purchasesPleaseSelectSeller => 'Please select a seller'; + + @override + String get purchasesNoTagGiven => 'Warning, no tag entered'; + + @override + String get purchasesTickets => 'Tickets'; + + @override + String get purchasesNoScannableProducts => 'No scannable products'; + + @override + String get purchasesLoading => 'Waiting for scan'; + + @override + String get purchasesScan => 'Scan'; + + @override + String get raffleRaffle => 'Raffle'; + + @override + String get rafflePrize => 'Prize'; + + @override + String get rafflePrizes => 'Prizes'; + + @override + String get raffleActualRaffles => 'Current raffles'; + + @override + String get rafflePastRaffles => 'Past raffles'; + + @override + String get raffleYourTickets => 'All your tickets'; + + @override + String get raffleCreateMenu => 'Creation menu'; + + @override + String get raffleNextRaffles => 'Upcoming raffles'; + + @override + String get raffleNoTicket => 'You have no ticket'; + + @override + String get raffleSeeRaffleDetail => 'View prizes/tickets'; + + @override + String get raffleActualPrize => 'Current prizes'; + + @override + String get raffleMajorPrize => 'Major prizes'; + + @override + String get raffleTakeTickets => 'Take your tickets'; + + @override + String get raffleNoTicketBuyable => 'You cannot buy tickets right now'; + + @override + String get raffleNoCurrentPrize => 'There are no prizes currently'; + + @override + String get raffleModifTombola => + 'You can modify your raffles or create new ones, all decisions must then be approved by admins'; + + @override + String get raffleCreateYourRaffle => 'Your raffle creation menu'; + + @override + String get rafflePossiblePrice => 'Possible prize'; + + @override + String get raffleInformation => 'Information and statistics'; + + @override + String get raffleAccounts => 'Accounts'; + + @override + String get raffleAdd => 'Add'; + + @override + String get raffleUpdatedAmount => 'Amount updated'; + + @override + String get raffleUpdatingError => 'Error during update'; + + @override + String get raffleDeletedPrize => 'Prize deleted'; + + @override + String get raffleDeletingError => 'Error during deletion'; + + @override + String get raffleQuantity => 'Quantity'; + + @override + String get raffleClose => 'Close'; + + @override + String get raffleOpen => 'Open'; + + @override + String get raffleAddTypeTicketSimple => 'Add'; + + @override + String get raffleAddingError => 'Error during addition'; + + @override + String get raffleEditTypeTicketSimple => 'Edit'; + + @override + String get raffleFillField => 'Field cannot be empty'; + + @override + String get raffleWaiting => 'Loading'; + + @override + String get raffleEditingError => 'Error during editing'; + + @override + String get raffleAddedTicket => 'Ticket added'; + + @override + String get raffleEditedTicket => 'Ticket edited'; + + @override + String get raffleAlreadyExistTicket => 'Ticket already exists'; + + @override + String get raffleNumberExpected => 'An integer is expected'; + + @override + String get raffleDeletedTicket => 'Ticket deleted'; + + @override + String get raffleAddPrize => 'Add'; + + @override + String get raffleEditPrize => 'Edit'; + + @override + String get raffleOpenRaffle => 'Open raffle'; + + @override + String get raffleCloseRaffle => 'Close raffle'; + + @override + String get raffleOpenRaffleDescription => + 'You are going to open the raffle, users will be able to buy tickets. You will no longer be able to modify the raffle. Are you sure you want to continue?'; + + @override + String get raffleCloseRaffleDescription => + 'You are going to close the raffle, users will no longer be able to buy tickets. Are you sure you want to continue?'; + + @override + String get raffleNoCurrentRaffle => 'There is no ongoing raffle'; + + @override + String get raffleBoughtTicket => 'Ticket purchased'; + + @override + String get raffleDrawingError => 'Error during drawing'; + + @override + String get raffleInvalidPrice => 'Price must be greater than 0'; + + @override + String get raffleMustBePositive => 'Number must be strictly positive'; + + @override + String get raffleDraw => 'Draw'; + + @override + String get raffleDrawn => 'Drawn'; + + @override + String get raffleError => 'Error'; + + @override + String get raffleGathered => 'Collected'; + + @override + String get raffleTickets => 'Tickets'; + + @override + String get raffleTicket => 'ticket'; + + @override + String get raffleWinner => 'Winner'; + + @override + String get raffleNoPrize => 'No prize'; + + @override + String get raffleDeletePrize => 'Delete prize'; + + @override + String get raffleDeletePrizeDescription => + 'You are going to delete the prize, are you sure you want to continue?'; + + @override + String get raffleDrawing => 'Drawing'; + + @override + String get raffleDrawingDescription => 'Draw the prize winner?'; + + @override + String get raffleDeleteTicket => 'Delete ticket'; + + @override + String get raffleDeleteTicketDescription => + 'You are going to delete the ticket, are you sure you want to continue?'; + + @override + String get raffleWinningTickets => 'Winning tickets'; + + @override + String get raffleNoWinningTicketYet => + 'Winning tickets will be displayed here'; + + @override + String get raffleName => 'Name'; + + @override + String get raffleDescription => 'Description'; + + @override + String get raffleBuyThisTicket => 'Buy this ticket'; + + @override + String get raffleLockedRaffle => 'Locked raffle'; + + @override + String get raffleUnavailableRaffle => 'Unavailable raffle'; + + @override + String get raffleNotEnoughMoney => 'You don\'t have enough money'; + + @override + String get raffleWinnable => 'winnable'; + + @override + String get raffleNoDescription => 'No description'; + + @override + String get raffleAmount => 'Balance'; + + @override + String get raffleLoading => 'Loading'; + + @override + String get raffleTicketNumber => 'Number of tickets'; + + @override + String get rafflePrice => 'Price'; + + @override + String get raffleEditRaffle => 'Edit raffle'; + + @override + String get raffleEdit => 'Edit'; + + @override + String get raffleAddPackTicket => 'Add ticket pack'; + + @override + String get recommendationRecommendation => 'Recommendation'; + + @override + String get recommendationTitle => 'Title'; + + @override + String get recommendationLogo => 'Logo'; + + @override + String get recommendationCode => 'Code'; + + @override + String get recommendationSummary => 'Short summary'; + + @override + String get recommendationDescription => 'Description'; + + @override + String get recommendationAdd => 'Add'; + + @override + String get recommendationEdit => 'Edit'; + + @override + String get recommendationDelete => 'Delete'; + + @override + String get recommendationAddImage => 'Please add an image'; + + @override + String get recommendationAddedRecommendation => 'Deal added'; + + @override + String get recommendationEditedRecommendation => 'Deal updated'; + + @override + String get recommendationDeleteRecommendationConfirmation => + 'Are you sure you want to delete this deal?'; + + @override + String get recommendationDeleteRecommendation => 'Delete'; + + @override + String get recommendationDeletingRecommendationError => + 'Error during deletion'; + + @override + String get recommendationDeletedRecommendation => 'Deal deleted'; + + @override + String get recommendationIncorrectOrMissingFields => + 'Incorrect or missing fields'; + + @override + String get recommendationEditingError => 'Edit failed'; + + @override + String get recommendationAddingError => 'Add failed'; + + @override + String get recommendationCopiedCode => 'Discount code copied'; + + @override + String get seedLibraryAdd => 'Add'; + + @override + String get seedLibraryAddedPlant => 'Plant added'; + + @override + String get seedLibraryAddedSpecies => 'Species added'; + + @override + String get seedLibraryAddingError => 'Error during addition'; + + @override + String get seedLibraryAddPlant => 'Deposit a plant'; + + @override + String get seedLibraryAddSpecies => 'Add a species'; + + @override + String get seedLibraryAll => 'All'; + + @override + String get seedLibraryAncestor => 'Ancestor'; + + @override + String get seedLibraryAround => 'around'; + + @override + String get seedLibraryAutumn => 'Autumn'; + + @override + String get seedLibraryBorrowedPlant => 'Borrowed plant'; + + @override + String get seedLibraryBorrowingDate => 'Borrowing date:'; + + @override + String get seedLibraryBorrowPlant => 'Borrow plant'; + + @override + String get seedLibraryCard => 'Card'; + + @override + String get seedLibraryChoosingAncestor => 'Please choose an ancestor'; + + @override + String get seedLibraryChoosingSpecies => 'Please choose a species'; + + @override + String get seedLibraryChoosingSpeciesOrAncestor => + 'Please choose a species or an ancestor'; + + @override + String get seedLibraryContact => 'Contact:'; + + @override + String get seedLibraryDays => 'days'; + + @override + String get seedLibraryDeadMsg => 'Do you want to declare the plant dead?'; + + @override + String get seedLibraryDeadPlant => 'Dead plant'; + + @override + String get seedLibraryDeathDate => 'Date of death'; + + @override + String get seedLibraryDeletedSpecies => 'Species deleted'; + + @override + String get seedLibraryDeleteSpecies => 'Delete species?'; + + @override + String get seedLibraryDeleting => 'Deleting'; + + @override + String get seedLibraryDeletingError => 'Error during deletion'; + + @override + String get seedLibraryDepositNotAvailable => + 'Plant deposit is not possible without borrowing a plant first'; + + @override + String get seedLibraryDescription => 'Description'; + + @override + String get seedLibraryDifficulty => 'Difficulty:'; + + @override + String get seedLibraryEdit => 'Edit'; + + @override + String get seedLibraryEditedPlant => 'Plant updated'; + + @override + String get seedLibraryEditInformation => 'Edit information'; + + @override + String get seedLibraryEditingError => 'Error during editing'; + + @override + String get seedLibraryEditSpecies => 'Edit species'; + + @override + String get seedLibraryEmptyDifficultyError => 'Please choose a difficulty'; + + @override + String get seedLibraryEmptyFieldError => 'Please fill all fields'; + + @override + String get seedLibraryEmptyTypeError => 'Please choose a plant type'; + + @override + String get seedLibraryEndMonth => 'End month:'; + + @override + String get seedLibraryFacebookUrl => 'Facebook link'; + + @override + String get seedLibraryFilters => 'Filters'; + + @override + String get seedLibraryForum => 'Oskour mom I killed my plant - Help forum'; + + @override + String get seedLibraryForumUrl => 'Forum link'; + + @override + String get seedLibraryHelpSheets => 'Plant sheets'; + + @override + String get seedLibraryInformation => 'Information:'; + + @override + String get seedLibraryMaturationTime => 'Maturation time'; + + @override + String get seedLibraryMonthJan => 'January'; + + @override + String get seedLibraryMonthFeb => 'February'; + + @override + String get seedLibraryMonthMar => 'March'; + + @override + String get seedLibraryMonthApr => 'April'; + + @override + String get seedLibraryMonthMay => 'May'; + + @override + String get seedLibraryMonthJun => 'June'; + + @override + String get seedLibraryMonthJul => 'July'; + + @override + String get seedLibraryMonthAug => 'August'; + + @override + String get seedLibraryMonthSep => 'September'; + + @override + String get seedLibraryMonthOct => 'October'; + + @override + String get seedLibraryMonthNov => 'November'; + + @override + String get seedLibraryMonthDec => 'December'; + + @override + String get seedLibraryMyPlants => 'My plants'; + + @override + String get seedLibraryName => 'Name'; + + @override + String get seedLibraryNbSeedsRecommended => 'Number of seeds recommended'; + + @override + String get seedLibraryNbSeedsRecommendedError => + 'Please enter a recommended seed number greater than 0'; + + @override + String get seedLibraryNoDateError => 'Please enter a date'; + + @override + String get seedLibraryNoFilteredPlants => + 'No plants match your search. Try other filters.'; + + @override + String get seedLibraryNoMorePlant => 'No plants available'; + + @override + String get seedLibraryNoPersonalPlants => + 'You don\'t have any plants yet in your seed library. You can add some in the stocks.'; + + @override + String get seedLibraryNoSpecies => 'No species found'; + + @override + String get seedLibraryNoStockPlants => 'No plants available in stock'; + + @override + String get seedLibraryNotes => 'Notes'; + + @override + String get seedLibraryOk => 'OK'; + + @override + String get seedLibraryPlantationPeriod => 'Planting period:'; + + @override + String get seedLibraryPlantationType => 'Plantation type:'; + + @override + String get seedLibraryPlantDetail => 'Plant details'; + + @override + String get seedLibraryPlantingDate => 'Planting date'; + + @override + String get seedLibraryPlantingNow => 'I\'m planting it now'; + + @override + String get seedLibraryPrefix => 'Prefix'; + + @override + String get seedLibraryPrefixError => 'Prefix already used'; + + @override + String get seedLibraryPrefixLengthError => 'The prefix must be 3 characters'; + + @override + String get seedLibraryPropagationMethod => 'Propagation method:'; + + @override + String get seedLibraryReference => 'Reference:'; + + @override + String get seedLibraryRemovedPlant => 'Plant removed'; + + @override + String get seedLibraryRemovingError => 'Error removing plant'; + + @override + String get seedLibraryResearch => 'Search'; + + @override + String get seedLibrarySaveChanges => 'Save changes'; + + @override + String get seedLibrarySeason => 'Season:'; + + @override + String get seedLibrarySeed => 'Seed'; + + @override + String get seedLibrarySeeds => 'seeds'; + + @override + String get seedLibrarySeedDeposit => 'Plant deposit'; + + @override + String get seedLibrarySeedLibrary => 'Seed library'; + + @override + String get seedLibrarySeedQuantitySimple => 'Seed quantity'; + + @override + String get seedLibrarySeedQuantity => 'Seed quantity:'; + + @override + String get seedLibraryShowDeadPlants => 'Show dead plants'; + + @override + String get seedLibrarySpecies => 'Species:'; + + @override + String get seedLibrarySpeciesHelp => 'Help on species'; + + @override + String get seedLibrarySpeciesPlural => 'Species'; + + @override + String get seedLibrarySpeciesSimple => 'Species'; + + @override + String get seedLibrarySpeciesType => 'Species type:'; + + @override + String get seedLibrarySpring => 'Spring'; + + @override + String get seedLibraryStartMonth => 'Start month:'; + + @override + String get seedLibraryStock => 'Available stock'; + + @override + String get seedLibrarySummer => 'Summer'; + + @override + String get seedLibraryStocks => 'Stocks'; + + @override + String get seedLibraryTimeUntilMaturation => 'Time until maturation:'; + + @override + String get seedLibraryType => 'Type:'; + + @override + String get seedLibraryUnableToOpen => 'Unable to open link'; + + @override + String get seedLibraryUpdate => 'Edit'; + + @override + String get seedLibraryUpdatedInformation => 'Information updated'; + + @override + String get seedLibraryUpdatedSpecies => 'Species updated'; + + @override + String get seedLibraryUpdatedPlant => 'Plant updated'; + + @override + String get seedLibraryUpdatingError => 'Error updating'; + + @override + String get seedLibraryWinter => 'Winter'; + + @override + String get seedLibraryWriteReference => + 'Please write the following reference: '; + + @override + String get settingsAccount => 'Account'; + + @override + String get settingsAddProfilePicture => 'Add a photo'; + + @override + String get settingsAdmin => 'Administrator'; + + @override + String get settingsAskHelp => 'Ask for help'; + + @override + String get settingsAssociation => 'Association'; + + @override + String get settingsBirthday => 'Birthday'; + + @override + String get settingsBugs => 'Bugs'; + + @override + String get settingsChangePassword => 'Change password'; + + @override + String get settingsChangingPassword => + 'Do you really want to change your password?'; + + @override + String get settingsConfirmPassword => 'Confirm password'; + + @override + String get settingsCopied => 'Copied!'; + + @override + String get settingsDarkMode => 'Dark mode'; + + @override + String get settingsDarkModeOff => 'Off'; + + @override + String get settingsDeleteLogs => 'Delete logs?'; + + @override + String get settingsDeleteNotificationLogs => 'Delete notification logs?'; + + @override + String get settingsDetelePersonalData => 'Delete my personal data'; + + @override + String get settingsDetelePersonalDataDesc => + 'This action notifies the administrator that you want to delete your personal data.'; + + @override + String get settingsDeleting => 'Deleting'; + + @override + String get settingsEdit => 'Edit'; + + @override + String get settingsEditAccount => 'Edit account'; + + @override + String get settingsEmail => 'Email'; + + @override + String get settingsEmptyField => 'This field cannot be empty'; + + @override + String get settingsErrorProfilePicture => 'Error editing profile picture'; + + @override + String get settingsErrorSendingDemand => 'Error sending request'; + + @override + String get settingsEventsIcal => 'Ical link for events'; + + @override + String get settingsExpectingDate => 'Expected birth date'; + + @override + String get settingsFirstname => 'First name'; + + @override + String get settingsFloor => 'Floor'; + + @override + String get settingsHelp => 'Help'; + + @override + String get settingsIcalCopied => 'Ical link copied!'; + + @override + String get settingsLanguage => 'Language'; + + @override + String get settingsLanguageVar => 'English 🇬🇧'; + + @override + String get settingsLogs => 'Logs'; + + @override + String get settingsModules => 'Modules'; + + @override + String get settingsMyIcs => 'My Ical link'; + + @override + String get settingsName => 'Last name'; + + @override + String get settingsNewPassword => 'New password'; + + @override + String get settingsNickname => 'Nickname'; + + @override + String get settingsNotifications => 'Notifications'; + + @override + String get settingsOldPassword => 'Old password'; + + @override + String get settingsPasswordChanged => 'Password changed'; + + @override + String get settingsPasswordsNotMatch => 'Passwords do not match'; + + @override + String get settingsPersonalData => 'Personal data'; + + @override + String get settingsPersonalisation => 'Personalization'; + + @override + String get settingsPhone => 'Phone'; + + @override + String get settingsProfilePicture => 'Profile picture'; + + @override + String get settingsPromo => 'Promotion'; + + @override + String get settingsRepportBug => 'Report a bug'; + + @override + String get settingsSave => 'Save'; + + @override + String get settingsSecurity => 'Security'; + + @override + String get settingsSendedDemand => 'Request sent'; + + @override + String get settingsSettings => 'Settings'; + + @override + String get settingsTooHeavyProfilePicture => 'Image is too large (max 4MB)'; + + @override + String get settingsUpdatedProfile => 'Profile updated'; + + @override + String get settingsUpdatedProfilePicture => 'Profile picture updated'; + + @override + String get settingsUpdateNotification => 'Update notifications'; + + @override + String get settingsUpdatingError => 'Error updating profile'; + + @override + String get settingsVersion => 'Version'; + + @override + String get settingsPasswordStrength => 'Password strength'; + + @override + String get settingsPasswordStrengthVeryWeak => 'Very weak'; + + @override + String get settingsPasswordStrengthWeak => 'Weak'; + + @override + String get settingsPasswordStrengthMedium => 'Medium'; + + @override + String get settingsPasswordStrengthStrong => 'Strong'; + + @override + String get settingsPasswordStrengthVeryStrong => 'Very strong'; + + @override + String get settingsPhoneNumber => 'Phone number'; + + @override + String get settingsValidate => 'Confirm'; + + @override + String get settingsEditedAccount => 'Account edited'; + + @override + String get settingsFailedToEditAccount => 'Failed to edit account'; + + @override + String get settingsChooseLanguage => 'Choose a language'; + + @override + String settingsNotificationCounter(int active, int total) { + String _temp0 = intl.Intl.pluralLogic( + active, + locale: localeName, + other: 'notifications', + one: 'notification', + zero: 'notification', + ); + return '$active/$total active $_temp0'; + } + + @override + String get settingsEvent => 'Event'; + + @override + String get settingsIcal => 'Ical link'; + + @override + String get settingsSynncWithCalendar => 'Sync with calendar'; + + @override + String get settingsIcalLinkCopied => 'Ical link copied'; + + @override + String get settingsProfile => 'Profile'; + + @override + String get settingsConnexion => 'Connection'; + + @override + String get settingsLogOut => 'Log out'; + + @override + String get settingsLogOutDescription => 'Do you really want to log out?'; + + @override + String get settingsLogOutSuccess => 'Logged out successfully'; + + @override + String get settingsDeleteMyAccount => 'Delete my account'; + + @override + String get settingsDeleteMyAccountDescription => + 'This action will send a request to the administrator to delete your account.'; + + @override + String get settingsDeletionAsked => + 'Your account deletion request has been sent to the administrator.'; + + @override + String get settingsDeleteMyAccountError => + 'Error sending account deletion request'; + + @override + String get voteAdd => 'Add'; + + @override + String get voteAddMember => 'Add a member'; + + @override + String get voteAddedPretendance => 'List added'; + + @override + String get voteAddedSection => 'Section added'; + + @override + String get voteAddingError => 'Error adding'; + + @override + String get voteAddPretendance => 'Add a list'; + + @override + String get voteAddSection => 'Add a section'; + + @override + String get voteAll => 'All'; + + @override + String get voteAlreadyAddedMember => 'Member already added'; + + @override + String get voteAlreadyVoted => 'Vote recorded'; + + @override + String get voteChooseList => 'Choose a list'; + + @override + String get voteClear => 'Reset'; + + @override + String get voteClearVotes => 'Reset votes'; + + @override + String get voteClosedVote => 'Votes closed'; + + @override + String get voteCloseVote => 'Close votes'; + + @override + String get voteConfirmVote => 'Confirm vote'; + + @override + String get voteCountVote => 'Count votes'; + + @override + String get voteDelete => 'Delete'; + + @override + String get voteDeletedAll => 'All deleted'; + + @override + String get voteDeletedPipo => 'Fake lists deleted'; + + @override + String get voteDeletedSection => 'Section deleted'; + + @override + String get voteDeleteAll => 'Delete all'; + + @override + String get voteDeleteAllDescription => + 'Do you really want to delete everything?'; + + @override + String get voteDeletePipo => 'Delete fake lists'; + + @override + String get voteDeletePipoDescription => + 'Do you really want to delete the fake lists?'; + + @override + String get voteDeletePretendance => 'Delete the list'; + + @override + String get voteDeletePretendanceDesc => + 'Do you really want to delete this list?'; + + @override + String get voteDeleteSection => 'Delete the section'; + + @override + String get voteDeleteSectionDescription => + 'Do you really want to delete this section?'; + + @override + String get voteDeletingError => 'Error deleting'; + + @override + String get voteDescription => 'Description'; + + @override + String get voteEdit => 'Edit'; + + @override + String get voteEditedPretendance => 'List edited'; + + @override + String get voteEditedSection => 'Section edited'; + + @override + String get voteEditingError => 'Error editing'; + + @override + String get voteErrorClosingVotes => 'Error closing votes'; + + @override + String get voteErrorCountingVotes => 'Error counting votes'; + + @override + String get voteErrorResetingVotes => 'Error resetting votes'; + + @override + String get voteErrorOpeningVotes => 'Error opening votes'; + + @override + String get voteIncorrectOrMissingFields => 'Incorrect or missing fields'; + + @override + String get voteMembers => 'Members'; + + @override + String get voteName => 'Name'; + + @override + String get voteNoPretendanceList => 'No list of candidates'; + + @override + String get voteNoSection => 'No section'; + + @override + String get voteCanNotVote => 'You cannot vote'; + + @override + String get voteNoSectionList => 'No section'; + + @override + String get voteNotOpenedVote => 'Vote not opened'; + + @override + String get voteOnGoingCount => 'Counting in progress'; + + @override + String get voteOpenVote => 'Open votes'; + + @override + String get votePipo => 'Fake'; + + @override + String get votePretendance => 'Lists'; + + @override + String get votePretendanceDeleted => 'Candidate list deleted'; + + @override + String get votePretendanceNotDeleted => 'Error deleting'; + + @override + String get voteProgram => 'Program'; + + @override + String get votePublish => 'Publish'; + + @override + String get votePublishVoteDescription => + 'Do you really want to publish the votes?'; + + @override + String get voteResetedVotes => 'Votes reset'; + + @override + String get voteResetVote => 'Reset votes'; + + @override + String get voteResetVoteDescription => 'What do you want to do?'; + + @override + String get voteRole => 'Role'; + + @override + String get voteSectionDescription => 'Section description'; + + @override + String get voteSection => 'Section'; + + @override + String get voteSectionName => 'Section name'; + + @override + String get voteSeeMore => 'See more'; + + @override + String get voteSelected => 'Selected'; + + @override + String get voteShowVotes => 'Show votes'; + + @override + String get voteVote => 'Vote'; + + @override + String get voteVoteError => 'Error recording vote'; + + @override + String get voteVoteFor => 'Vote for '; + + @override + String get voteVoteNotStarted => 'Vote not opened'; + + @override + String get voteVoters => 'Voting groups'; + + @override + String get voteVoteSuccess => 'Vote recorded'; + + @override + String get voteVotes => 'Votes'; + + @override + String get voteVotesClosed => 'Votes closed'; + + @override + String get voteVotesCounted => 'Votes counted'; + + @override + String get voteVotesOpened => 'Votes opened'; + + @override + String get voteWarning => 'Warning'; + + @override + String get voteWarningMessage => + 'Selection will not be saved.\nDo you want to continue?'; + + @override + String get moduleAdvert => 'Feed'; + + @override + String get moduleAdvertDescription => 'View the latest feed'; + + @override + String get moduleAmap => 'AMAP'; + + @override + String get moduleAmapDescription => 'Order your AMAP basket'; + + @override + String get moduleBooking => 'Booking'; + + @override + String get moduleBookingDescription => 'Book a room'; + + @override + String get moduleCalendar => 'Calendar'; + + @override + String get moduleCalendarDescription => 'View the calendar of events'; + + @override + String get moduleCentralisation => 'Centralisation'; + + @override + String get moduleCentralisationDescription => 'Viw all links'; + + @override + String get moduleCinema => 'Cinema'; + + @override + String get moduleCinemaDescription => 'View the cinema schedule'; + + @override + String get moduleEvent => 'Event'; + + @override + String get moduleEventDescription => 'View events'; + + @override + String get moduleFlappyBird => 'Flappy Bird'; + + @override + String get moduleFlappyBirdDescription => 'Play Flappy Bird'; + + @override + String get moduleLoan => 'Loan'; + + @override + String get moduleLoanDescription => 'See your loans'; + + @override + String get modulePhonebook => 'Phonebook'; + + @override + String get modulePhonebookDescription => 'View the phonebook'; + + @override + String get modulePurchases => 'Purchases'; + + @override + String get modulePurchasesDescription => 'View your purchases'; + + @override + String get moduleRaffle => 'Raffle'; + + @override + String get moduleRaffleDescription => 'View the raffle'; + + @override + String get moduleRecommendation => 'Recommendation'; + + @override + String get moduleRecommendationDescription => 'View the recommendations'; + + @override + String get moduleSeedLibrary => 'Seed Library'; + + @override + String get moduleSeedLibraryDescription => 'View the seed library'; + + @override + String get moduleVote => 'Vote'; + + @override + String get moduleVoteDescription => 'Vote for the campaigns'; + + @override + String get modulePh => 'PH'; + + @override + String get modulePhDescription => 'View the PH'; + + @override + String get moduleSettings => 'Settings'; + + @override + String get moduleSettingsDescription => 'Manage your settings'; + + @override + String get moduleFeed => 'Events'; + + @override + String get moduleFeedDescription => 'View the latest events'; + + @override + String get moduleStyleGuide => 'StyleGuide'; + + @override + String get moduleStyleGuideDescription => 'Style guide for developers'; + + @override + String get moduleAdmin => 'Admin'; + + @override + String get moduleAdminDescription => + 'Administration module for administrators'; + + @override + String get moduleOthers => 'Others'; + + @override + String get moduleOthersDescription => 'Other modules'; + + @override + String get modulePayment => 'Payment'; + + @override + String get modulePaymentDescription => 'Pay and see your transactions'; + + @override + String get toolInvalidNumber => 'Invalid number'; + + @override + String get toolDateRequired => 'Date required'; + + @override + String get toolSuccess => 'Success'; +} diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart new file mode 100644 index 0000000000..54b359d2b0 --- /dev/null +++ b/lib/l10n/app_localizations_fr.dart @@ -0,0 +1,4591 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for French (`fr`). +class AppLocalizationsFr extends AppLocalizations { + AppLocalizationsFr([String locale = 'fr']) : super(locale); + + @override + String get dateToday => 'Aujourd\'hui'; + + @override + String get dateYesterday => 'Hier'; + + @override + String get dateTomorrow => 'Demain'; + + @override + String get dateAt => 'à'; + + @override + String get dateFrom => 'de'; + + @override + String get dateTo => 'à'; + + @override + String get dateBetweenDays => 'au'; + + @override + String get dateStarting => 'Commence'; + + @override + String get dateLast => ''; + + @override + String get dateUntil => 'Jusqu\'au'; + + @override + String get feedFilterAll => 'Tous'; + + @override + String get feedFilterPending => 'En attente'; + + @override + String get feedFilterApproved => 'Approuvés'; + + @override + String get feedFilterRejected => 'Rejetés'; + + @override + String get feedEmptyAll => 'Aucun événement disponible'; + + @override + String get feedEmptyPending => 'Aucun événement en attente de validation'; + + @override + String get feedEmptyApproved => 'Aucun événement approuvé'; + + @override + String get feedEmptyRejected => 'Aucun événement rejeté'; + + @override + String get feedEventManagement => 'Gestion des événements'; + + @override + String get feedTitle => 'Titre'; + + @override + String get feedLocation => 'Lieu'; + + @override + String get feedSGDate => 'Date du SG'; + + @override + String get feedSGExternalLink => 'Lien externe du SG'; + + @override + String get feedCreateEvent => 'Créer l\'événement'; + + @override + String get feedNotification => 'Envoyer une notification'; + + @override + String get feedPleaseSelectAnAssociation => + 'Veuillez sélectionner une association'; + + @override + String get feedReject => 'Rejeter'; + + @override + String get feedApprove => 'Approuver'; + + @override + String get feedEnded => 'Terminé'; + + @override + String get feedOngoing => 'En cours'; + + @override + String get feedFilter => 'Filtrer'; + + @override + String get feedAssociation => 'Association'; + + @override + String feedAssociationEvent(String name) { + return 'Event de $name'; + } + + @override + String get feedEditEvent => 'Modifier l\'événement'; + + @override + String get feedManageAssociationEvents => + 'Gérer les événements de l\'association'; + + @override + String get feedNews => 'Calendrier'; + + @override + String get feedNewsType => 'Type d\'actualité'; + + @override + String get feedNoAssociationEvents => 'Aucun événement d\'association'; + + @override + String get feedApply => 'Appliquer'; + + @override + String get feedAdmin => 'Administration'; + + @override + String get feedCreateAnEvent => 'Créer un événement'; + + @override + String get feedManageRequests => 'Demandes de publication'; + + @override + String get feedNoNewsAvailable => 'Aucune actualité disponible'; + + @override + String get feedRefresh => 'Actualiser'; + + @override + String get feedPleaseProvideASGExternalLink => + 'Veuillez entrer un lien externe pour le SG'; + + @override + String get feedPleaseProvideASGDate => 'Veuillez entrer une date de SG'; + + @override + String feedShotgunIn(String time) { + return 'Shotgun $time'; + } + + @override + String feedVoteIn(String time) { + return 'Vote $time'; + } + + @override + String get feedCantOpenLink => 'Impossible d\'ouvrir le lien'; + + @override + String get feedGetReady => 'Prépare-toi !'; + + @override + String get eventActionCampaign => 'Tu peux voter'; + + @override + String get eventActionEvent => 'Tu es invité'; + + @override + String get eventActionCampaignSubtitle => 'Votez maintenant'; + + @override + String get eventActionEventSubtitle => 'Répondez à l\'invitation'; + + @override + String get eventActionCampaignButton => 'Voter'; + + @override + String get eventActionEventButton => 'Réserver'; + + @override + String get eventActionCampaignValidated => 'J\'ai voté !'; + + @override + String get eventActionEventValidated => 'Je viens !'; + + @override + String get adminAccountTypes => 'Types de compte'; + + @override + String get adminAdd => 'Ajouter'; + + @override + String get adminAddGroup => 'Ajouter un groupe'; + + @override + String get adminAddMember => 'Ajouter un membre'; + + @override + String get adminAddedGroup => 'Groupe créé'; + + @override + String get adminAddedLoaner => 'Préteur ajouté'; + + @override + String get adminAddedMember => 'Membre ajouté'; + + @override + String get adminAddingError => 'Erreur lors de l\'ajout'; + + @override + String get adminAddingMember => 'Ajout d\'un membre'; + + @override + String get adminAddLoaningGroup => 'Ajouter un groupe de prêt'; + + @override + String get adminAddSchool => 'Ajouter une école'; + + @override + String get adminAddStructure => 'Ajouter une structure'; + + @override + String get adminAddedSchool => 'École créée'; + + @override + String get adminAddedStructure => 'Structure ajoutée'; + + @override + String get adminEditedStructure => 'Structure modifiée'; + + @override + String get adminAdministration => 'Administration'; + + @override + String get adminAssociationMembership => 'Adhésion'; + + @override + String get adminAssociationMembershipName => 'Nom de l\'adhésion'; + + @override + String get adminAssociationsMemberships => 'Adhésions'; + + @override + String adminBankAccountHolder(String bankAccountHolder) { + return 'Titulaire du compte bancaire : $bankAccountHolder'; + } + + @override + String get adminBankAccountHolderModified => + 'Titulaire du compte bancaire modifié'; + + @override + String get adminBankDetails => 'Coordonnées bancaires'; + + @override + String get adminBic => 'BIC'; + + @override + String get adminBicError => 'Le BIC doit faire 11 caractères'; + + @override + String get adminCity => 'Ville'; + + @override + String get adminClearFilters => 'Effacer les filtres'; + + @override + String get adminCountry => 'Pays'; + + @override + String get adminCreateAssociationMembership => 'Créer une adhésion'; + + @override + String get adminCreatedAssociationMembership => 'Adhésion créée'; + + @override + String get adminCreationError => 'Erreur lors de la création'; + + @override + String get adminDateError => + 'La date de début doit être avant la date de fin'; + + @override + String get adminDefineAsBankAccountHolder => + 'Définir comme titulaire du compte bancaire'; + + @override + String get adminDelete => 'Supprimer'; + + @override + String get adminDeleteAssociationMember => 'Supprimer le membre ?'; + + @override + String get adminDeleteAssociationMemberConfirmation => + 'Êtes-vous sûr de vouloir supprimer ce membre ?'; + + @override + String get adminDeleteAssociationMembership => 'Supprimer l\'adhésion ?'; + + @override + String get adminDeletedAssociationMembership => 'Adhésion supprimée'; + + @override + String get adminDeleteGroup => 'Supprimer le groupe'; + + @override + String get adminDeletedGroup => 'Groupe supprimé'; + + @override + String get adminDeleteSchool => 'Supprimer l\'école ?'; + + @override + String get adminDeletedSchool => 'École supprimée'; + + @override + String get adminDeleting => 'Suppression'; + + @override + String get adminDeletingError => 'Erreur lors de la suppression'; + + @override + String get adminDescription => 'Description'; + + @override + String get adminEdit => 'Modifier'; + + @override + String get adminEditStructure => 'Modifier la structure'; + + @override + String get adminEditMembership => 'Modifier l\'adhésion'; + + @override + String get adminEmptyDate => 'Date vide'; + + @override + String get adminEmptyFieldError => 'Le nom ne peut pas être vide'; + + @override + String get adminEmailFailed => + 'Impossible d\'envoyer un mail aux adresses suivantes'; + + @override + String get adminEmailRegex => 'Email Regex'; + + @override + String get adminEmptyUser => 'Utilisateur vide'; + + @override + String get adminEndDate => 'Date de fin'; + + @override + String get adminEndDateMaximal => 'Date de fin maximale'; + + @override + String get adminEndDateMinimal => 'Date de fin minimale'; + + @override + String get adminError => 'Erreur'; + + @override + String get adminFilters => 'Filtres'; + + @override + String get adminGroup => 'Groupe'; + + @override + String get adminGroups => 'Groupes'; + + @override + String get adminIban => 'IBAN'; + + @override + String get adminIbanError => 'L\'IBAN doit faire 27 caractères'; + + @override + String get adminLoaningGroup => 'Groupe de prêt'; + + @override + String get adminLooking => 'Recherche'; + + @override + String get adminManager => 'Administrateur de la structure'; + + @override + String get adminMaximum => 'Maximum'; + + @override + String get adminMembers => 'Membres'; + + @override + String get adminMembershipAddingError => + 'Erreur lors de l\'ajout (surement dû à une superposition de dates)'; + + @override + String get adminMemberships => 'Adhésions'; + + @override + String get adminMembershipUpdatingError => + 'Erreur lors de la modification (surement dû à une superposition de dates)'; + + @override + String get adminMinimum => 'Minimum'; + + @override + String get adminModifyModuleVisibility => 'Visibilité des modules'; + + @override + String get adminName => 'Nom'; + + @override + String get adminNoGroup => 'Aucun groupe'; + + @override + String get adminNoManager => 'Aucun manager n\'est sélectionné'; + + @override + String get adminNoMember => 'Aucun membre'; + + @override + String get adminNoMoreLoaner => 'Aucun prêteur n\'est disponible'; + + @override + String get adminNoSchool => 'Sans école'; + + @override + String get adminRemoveGroupMember => 'Supprimer le membre du groupe ?'; + + @override + String get adminResearch => 'Recherche'; + + @override + String get adminSchools => 'Écoles'; + + @override + String get adminShortId => 'Short ID (3 lettres)'; + + @override + String get adminShortIdError => 'Le short ID doit faire 3 caractères'; + + @override + String get adminSiegeAddress => 'Adresse du siège'; + + @override + String get adminSiret => 'SIRET'; + + @override + String get adminSiretError => 'SIRET must be 14 digits'; + + @override + String get adminStreet => 'Numéro et rue'; + + @override + String get adminStructures => 'Structures'; + + @override + String get adminStartDate => 'Date de début'; + + @override + String get adminStartDateMaximal => 'Date de début maximale'; + + @override + String get adminStartDateMinimal => 'Date de début minimale'; + + @override + String get adminUndefinedBankAccountHolder => + 'Titulaire du compte bancaire non défini'; + + @override + String get adminUpdatedAssociationMembership => 'Adhésion modifiée'; + + @override + String get adminUpdatedGroup => 'Groupe modifié'; + + @override + String get adminUpdatedMembership => 'Adhésion modifiée'; + + @override + String get adminUpdatingError => 'Erreur lors de la modification'; + + @override + String get adminUser => 'Utilisateur'; + + @override + String get adminValidateFilters => 'Valider les filtres'; + + @override + String get adminVisibilities => 'Visibilités'; + + @override + String get adminZipcode => 'Code postal'; + + @override + String get adminGroupNotification => 'Notification de groupe'; + + @override + String adminNotifyGroup(String groupName) { + return 'Notifier le groupe $groupName'; + } + + @override + String get adminTitle => 'Titre'; + + @override + String get adminContent => 'Contenu'; + + @override + String get adminSend => 'Envoyer'; + + @override + String get adminNotificationSent => 'Notification envoyée'; + + @override + String get adminFailedToSendNotification => + 'Échec de l\'envoi de la notification'; + + @override + String get adminGroupsManagement => 'Gestion des groupes'; + + @override + String get adminEditGroup => 'Modifier le groupe'; + + @override + String get adminManageMembers => 'Gérer les membres'; + + @override + String get adminDeleteGroupConfirmation => + 'Êtes-vous sûr de vouloir supprimer ce groupe ?'; + + @override + String get adminFailedToDeleteGroup => 'Échec de la suppression du groupe'; + + @override + String get adminUsersAndGroups => 'Utilisateurs et groupes'; + + @override + String get adminUsersManagement => 'Gestion des utilisateurs'; + + @override + String get adminUsersManagementDescription => + 'Gérer les utilisateurs de l\'application'; + + @override + String get adminManageUserGroups => 'Gérer les groupes d\'utilisateurs'; + + @override + String get adminSendNotificationToGroup => + 'Envoyer une notification à un groupe'; + + @override + String get adminPaiementModule => 'Module de paiement'; + + @override + String get adminPaiement => 'Paiement'; + + @override + String get adminManagePaiementStructures => + 'Gérer les structures du module de paiement'; + + @override + String get adminManageUsersAssociationMemberships => + 'Gérer les adhésions des utilisateurs'; + + @override + String get adminAssociationMembershipsManagement => 'Gestion des adhésions'; + + @override + String get adminChooseGroupManager => 'Groupe gestionnaire de l\'adhésion'; + + @override + String get adminSelectManager => 'Sélectionner un gestionnaire'; + + @override + String get adminImportList => 'Importer une liste'; + + @override + String get adminImportUsersDescription => + 'Importer des utilisateurs depuis un fichier CSV. Le fichier CSV doit contenir une adresse email par ligne.'; + + @override + String get adminFailedToInviteUsers => + 'Échec de l\'invitation des utilisateurs'; + + @override + String get adminDeleteUsers => 'Supprimer des utilisateurs'; + + @override + String get adminAdmin => 'Admin'; + + @override + String get adminAssociations => 'Associations'; + + @override + String get adminManageAssociations => 'Gérer les associations'; + + @override + String get adminAddAssociation => 'Ajouter une association'; + + @override + String get adminAssociationName => 'Nom de l\'association'; + + @override + String get adminSelectGroupAssociationManager => + 'Séléctionner roupe gestionnaire de l\'association'; + + @override + String adminEditAssociation(String associationName) { + return 'Modifier l\'association : $associationName'; + } + + @override + String adminManagerGroup(String groupName) { + return 'Groupe gestionnaire : $groupName'; + } + + @override + String get adminAssociationCreated => 'Association créée'; + + @override + String get adminAssociationUpdated => 'Association mise à jour'; + + @override + String get adminAssociationCreationError => + 'Échec de la création de l\'association'; + + @override + String get adminAssociationUpdateError => + 'Échec de la mise à jour de l\'association'; + + @override + String get adminInvite => 'Inviter'; + + @override + String get adminInvitedUsers => 'Utilisateurs invités'; + + @override + String get adminInviteUsers => 'Inviter des utilisateurs'; + + @override + String adminInviteUsersCounter(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count utilisateurs', + one: '$count utilisateur', + zero: 'Aucun utilisateur', + ); + return '$_temp0 dans le fichier CSV'; + } + + @override + String get adminUpdatedAssociationLogo => 'Logo de l\'association mis à jour'; + + @override + String get adminTooHeavyLogo => + 'Le logo de l\'association est trop lourd, il doit faire moins de 4 Mo'; + + @override + String get adminFailedToUpdateAssociationLogo => + 'Échec de la mise à jour du logo de l\'association'; + + @override + String get adminChooseGroup => 'Choisir un groupe'; + + @override + String get adminChooseAssociationManagerGroup => + 'Choisir un groupe gestionnaire pour l\'association'; + + @override + String get advertAdd => 'Ajouter'; + + @override + String get advertAddedAdvert => 'Annonce publiée'; + + @override + String get advertAddedAnnouncer => 'Annonceur ajouté'; + + @override + String get advertAddingError => 'Erreur lors de l\'ajout'; + + @override + String get advertAdmin => 'Admin'; + + @override + String get advertAdvert => 'Annonce'; + + @override + String get advertChoosingAnnouncer => 'Veuillez choisir un annonceur'; + + @override + String get advertChoosingPoster => 'Veuillez choisir une image'; + + @override + String get advertContent => 'Contenu'; + + @override + String get advertDeleteAdvert => 'Supprimer l\'annonce'; + + @override + String get advertDeleteAnnouncer => 'Supprimer l\'annonceur ?'; + + @override + String get advertDeleting => 'Suppression'; + + @override + String get advertEdit => 'Modifier'; + + @override + String get advertEditedAdvert => 'Annonce modifiée'; + + @override + String get advertEditingError => 'Erreur lors de la modification'; + + @override + String get advertGroupAdvert => 'Groupe'; + + @override + String get advertIncorrectOrMissingFields => 'Champs incorrects ou manquants'; + + @override + String get advertInvalidNumber => 'Veuillez entrer un nombre'; + + @override + String get advertManagement => 'Gestion'; + + @override + String get advertModifyAnnouncingGroup => 'Modifier un groupe d\'annonce'; + + @override + String get advertNoMoreAnnouncer => 'Aucun annonceur n\'est disponible'; + + @override + String get advertNoValue => 'Veuillez entrer une valeur'; + + @override + String get advertPositiveNumber => 'Veuillez entrer un nombre positif'; + + @override + String get advertPublishToFeed => 'Publier dans le feed'; + + @override + String get advertNotification => 'Envoyer une notification'; + + @override + String get advertRemovedAnnouncer => 'Annonceur supprimé'; + + @override + String get advertRemovingError => 'Erreur lors de la suppression'; + + @override + String get advertTags => 'Tags'; + + @override + String get advertTitle => 'Titre'; + + @override + String get advertMonthJan => 'Janv'; + + @override + String get advertMonthFeb => 'Févr.'; + + @override + String get advertMonthMar => 'Mars'; + + @override + String get advertMonthApr => 'Avr.'; + + @override + String get advertMonthMay => 'Mai'; + + @override + String get advertMonthJun => 'Juin'; + + @override + String get advertMonthJul => 'Juill.'; + + @override + String get advertMonthAug => 'Août'; + + @override + String get advertMonthSep => 'Sept.'; + + @override + String get advertMonthOct => 'Oct.'; + + @override + String get advertMonthNov => 'Nov.'; + + @override + String get advertMonthDec => 'Déc.'; + + @override + String get amapAccounts => 'Comptes'; + + @override + String get amapAdd => 'Ajouter'; + + @override + String get amapAddDelivery => 'Ajouter une livraison'; + + @override + String get amapAddedCommand => 'Commande ajoutée'; + + @override + String get amapAddedOrder => 'Commande ajoutée'; + + @override + String get amapAddedProduct => 'Produit ajouté'; + + @override + String get amapAddedUser => 'Utilisateur ajouté'; + + @override + String get amapAddProduct => 'Ajouter un produit'; + + @override + String get amapAddUser => 'Ajouter un utilisateur'; + + @override + String get amapAddingACommand => 'Ajouter une commande'; + + @override + String get amapAddingCommand => 'Ajouter la commande'; + + @override + String get amapAddingError => 'Erreur lors de l\'ajout'; + + @override + String get amapAddingProduct => 'Ajouter un produit'; + + @override + String get amapAddOrder => 'Ajouter une commande'; + + @override + String get amapAdmin => 'Admin'; + + @override + String get amapAlreadyExistCommand => + 'Il existe déjà une commande à cette date'; + + @override + String get amapAmap => 'Amap'; + + @override + String get amapAmount => 'Solde'; + + @override + String get amapArchive => 'Archiver'; + + @override + String get amapArchiveDelivery => 'Archiver'; + + @override + String get amapArchivingDelivery => 'Archivage de la livraison'; + + @override + String get amapCategory => 'Catégorie'; + + @override + String get amapCloseDelivery => 'Verrouiller'; + + @override + String get amapCommandDate => 'Date de la commande'; + + @override + String get amapCommandProducts => 'Produits de la commande'; + + @override + String get amapConfirm => 'Confirmer'; + + @override + String get amapContact => 'Contacts associatifs '; + + @override + String get amapCreateCategory => 'Créer une catégorie'; + + @override + String get amapDelete => 'Supprimer'; + + @override + String get amapDeleteDelivery => 'Supprimer la livraison ?'; + + @override + String get amapDeleteDeliveryDescription => + 'Voulez-vous vraiment supprimer cette livraison ?'; + + @override + String get amapDeletedDelivery => 'Livraison supprimée'; + + @override + String get amapDeletedOrder => 'Commande supprimée'; + + @override + String get amapDeletedProduct => 'Produit supprimé'; + + @override + String get amapDeleteProduct => 'Supprimer le produit ?'; + + @override + String get amapDeleteProductDescription => + 'Voulez-vous vraiment supprimer ce produit ?'; + + @override + String get amapDeleting => 'Suppression'; + + @override + String get amapDeletingDelivery => 'Supprimer la livraison ?'; + + @override + String get amapDeletingError => 'Erreur lors de la suppression'; + + @override + String get amapDeletingOrder => 'Supprimer la commande ?'; + + @override + String get amapDeletingProduct => 'Supprimer le produit ?'; + + @override + String get amapDeliver => 'Livraison teminée ?'; + + @override + String get amapDeliveries => 'Livraisons'; + + @override + String get amapDeliveringDelivery => 'Toutes les commandes sont livrées ?'; + + @override + String get amapDelivery => 'Livraison'; + + @override + String get amapDeliveryArchived => 'Livraison archivée'; + + @override + String get amapDeliveryDate => 'Date de livraison'; + + @override + String get amapDeliveryDelivered => 'Livraison effectuée'; + + @override + String get amapDeliveryHistory => 'Historique des livraisons'; + + @override + String get amapDeliveryList => 'Liste des livraisons'; + + @override + String get amapDeliveryLocked => 'Livraison verrouillée'; + + @override + String get amapDeliveryOn => 'Livraison le'; + + @override + String get amapDeliveryOpened => 'Livraison ouverte'; + + @override + String get amapDeliveryNotArchived => 'Livraison non archivée'; + + @override + String get amapDeliveryNotLocked => 'Livraison non verrouillée'; + + @override + String get amapDeliveryNotDelivered => 'Livraison non effectuée'; + + @override + String get amapDeliveryNotOpened => 'Livraison non ouverte'; + + @override + String get amapEditDelivery => 'Modifier la livraison'; + + @override + String get amapEditedCommand => 'Commande modifiée'; + + @override + String get amapEditingError => 'Erreur lors de la modification'; + + @override + String get amapEditProduct => 'Modifier le produit'; + + @override + String get amapEndingDelivery => 'Fin de la livraison'; + + @override + String get amapError => 'Erreur'; + + @override + String get amapErrorLink => 'Erreur lors de l\'ouverture du lien'; + + @override + String get amapErrorLoadingUser => + 'Erreur lors du chargement des utilisateurs'; + + @override + String get amapEvening => 'Soir'; + + @override + String get amapExpectingNumber => 'Veuillez entrer un nombre'; + + @override + String get amapFillField => 'Veuillez remplir ce champ'; + + @override + String get amapHandlingAccount => 'Gérer les comptes'; + + @override + String get amapLoading => 'Chargement...'; + + @override + String get amapLoadingError => 'Erreur lors du chargement'; + + @override + String get amapLock => 'Verrouiller'; + + @override + String get amapLocked => 'Verrouillée'; + + @override + String get amapLockedDelivery => 'Livraison verrouillée'; + + @override + String get amapLockedOrder => 'Commande verrouillée'; + + @override + String get amapLooking => 'Rechercher'; + + @override + String get amapLockingDelivery => 'Verrouiller la livraison ?'; + + @override + String get amapMidDay => 'Midi'; + + @override + String get amapMyOrders => 'Mes commandes'; + + @override + String get amapName => 'Nom'; + + @override + String get amapNextStep => 'Étape suivante'; + + @override + String get amapNoProduct => 'Pas de produit'; + + @override + String get amapNoCurrentOrder => 'Pas de commande en cours'; + + @override + String get amapNoMoney => 'Pas assez d\'argent'; + + @override + String get amapNoOpennedDelivery => 'Pas de livraison ouverte'; + + @override + String get amapNoOrder => 'Pas de commande'; + + @override + String get amapNoSelectedDelivery => 'Pas de livraison sélectionnée'; + + @override + String get amapNotEnoughMoney => 'Pas assez d\'argent'; + + @override + String get amapNotPlannedDelivery => 'Pas de livraison planifiée'; + + @override + String get amapOneOrder => 'commande'; + + @override + String get amapOpenDelivery => 'Ouvrir'; + + @override + String get amapOpened => 'Ouverte'; + + @override + String get amapOpenningDelivery => 'Ouvrir la livraison ?'; + + @override + String get amapOrder => 'Commander'; + + @override + String get amapOrders => 'Commandes'; + + @override + String get amapPickChooseCategory => + 'Veuillez entrer une valeur ou choisir une catégorie existante'; + + @override + String get amapPickDeliveryMoment => 'Choisissez un moment de livraison'; + + @override + String get amapPresentation => 'Présentation'; + + @override + String get amapPresentation1 => + 'L\'AMAP (association pour le maintien d\'une agriculture paysanne) est un service proposé par l\'association Planet&Co de l\'ECL. Vous pouvez ainsi recevoir des produits (paniers de fruits et légumes, jus, confitures...) directement sur le campus !\n\nLes commandes doivent être passées avant le vendredi 21h et sont livrées sur le campus le mardi de 13h à 13h45 (ou de 18h15 à 18h30 si vous ne pouvez pas passer le midi) dans le hall du M16.\n\nVous ne pouvez commander que si votre solde le permet. Vous pouvez recharger votre solde via la collecte Lydia ou bien avec un chèque que vous pouvez nous transmettre lors des permanences.\n\nLien vers la collecte Lydia pour le rechargement : '; + + @override + String get amapPresentation2 => + '\n\nN\'hésitez pas à nous contacter en cas de problème !'; + + @override + String get amapPrice => 'Prix'; + + @override + String get amapProduct => 'produit'; + + @override + String get amapProducts => 'Produits'; + + @override + String get amapProductInDelivery => 'Produit dans une livraison non terminée'; + + @override + String get amapQuantity => 'Quantité'; + + @override + String get amapRequiredDate => 'La date est requise'; + + @override + String get amapSeeMore => 'Voir plus'; + + @override + String get amapThe => 'Le'; + + @override + String get amapUnlock => 'Dévérouiller'; + + @override + String get amapUnlockedDelivery => 'Livraison dévérouillée'; + + @override + String get amapUnlockingDelivery => 'Dévérouiller la livraison ?'; + + @override + String get amapUpdate => 'Modifier'; + + @override + String get amapUpdatedAmount => 'Solde modifié'; + + @override + String get amapUpdatedOrder => 'Commande modifiée'; + + @override + String get amapUpdatedProduct => 'Produit modifié'; + + @override + String get amapUpdatingError => 'Echec de la modification'; + + @override + String get amapUsersNotFound => 'Aucun utilisateur trouvé'; + + @override + String get amapWaiting => 'En attente'; + + @override + String get bookingAdd => 'Ajouter'; + + @override + String get bookingAddBookingPage => 'Demande'; + + @override + String get bookingAddRoom => 'Ajouter une salle'; + + @override + String get bookingAddBooking => 'Ajouter une réservation'; + + @override + String get bookingAddedBooking => 'Demande ajoutée'; + + @override + String get bookingAddedRoom => 'Salle ajoutée'; + + @override + String get bookingAddedManager => 'Gestionnaire ajouté'; + + @override + String get bookingAddingError => 'Erreur lors de l\'ajout'; + + @override + String get bookingAddManager => 'Ajouter un gestionnaire'; + + @override + String get bookingAdminPage => 'Administrateur'; + + @override + String get bookingAllDay => 'Toute la journée'; + + @override + String get bookingBookedFor => 'Réservé pour'; + + @override + String get bookingBooking => 'Réservation'; + + @override + String get bookingBookingCreated => 'Réservation créée'; + + @override + String get bookingBookingDemand => 'Demande de réservation'; + + @override + String get bookingBookingNote => 'Note de la réservation'; + + @override + String get bookingBookingPage => 'Réservation'; + + @override + String get bookingBookingReason => 'Motif de la réservation'; + + @override + String get bookingBy => 'par'; + + @override + String get bookingConfirm => 'Confirmer'; + + @override + String get bookingConfirmation => 'Confirmation'; + + @override + String get bookingConfirmBooking => 'Confirmer la réservation ?'; + + @override + String get bookingConfirmed => 'Validée'; + + @override + String get bookingDates => 'Dates'; + + @override + String get bookingDecline => 'Refuser'; + + @override + String get bookingDeclineBooking => 'Refuser la réservation ?'; + + @override + String get bookingDeclined => 'Refusée'; + + @override + String get bookingDelete => 'Supprimer'; + + @override + String get bookingDeleting => 'Suppression'; + + @override + String get bookingDeleteBooking => 'Suppression'; + + @override + String get bookingDeleteBookingConfirmation => + 'Êtes-vous sûr de vouloir supprimer cette réservation ?'; + + @override + String get bookingDeletedBooking => 'Réservation supprimée'; + + @override + String get bookingDeletedRoom => 'Salle supprimée'; + + @override + String get bookingDeletedManager => 'Gestionnaire supprimé'; + + @override + String get bookingDeleteRoomConfirmation => + 'Êtes-vous sûr de vouloir supprimer cette salle ?\n\nLa salle ne doit avoir aucune réservation en cours ou à venir pour être supprimée'; + + @override + String get bookingDeleteManagerConfirmation => + 'Êtes-vous sûr de vouloir supprimer ce gestionnaire ?\n\nLe gestionnaire ne doit être associé à aucune salle pour pouvoir être supprimé'; + + @override + String get bookingDeletingBooking => 'Supprimer la réservation ?'; + + @override + String get bookingDeletingError => 'Erreur lors de la suppression'; + + @override + String get bookingDeletingRoom => 'Supprimer la salle ?'; + + @override + String get bookingEdit => 'Modifier'; + + @override + String get bookingEditBooking => 'Modifier une réservation'; + + @override + String get bookingEditionError => 'Erreur lors de la modification'; + + @override + String get bookingEditedBooking => 'Réservation modifiée'; + + @override + String get bookingEditedRoom => 'Salle modifiée'; + + @override + String get bookingEditedManager => 'Gestionnaire modifié'; + + @override + String get bookingEditManager => 'Modifier ou supprimer un gestionnaire'; + + @override + String get bookingEditRoom => 'Modifier ou supprimer une salle'; + + @override + String get bookingEndDate => 'Date de fin'; + + @override + String get bookingEndHour => 'Heure de fin'; + + @override + String get bookingEntity => 'Pour qui ?'; + + @override + String get bookingError => 'Erreur'; + + @override + String get bookingEventEvery => 'Tous les'; + + @override + String get bookingHistoryPage => 'Historique'; + + @override + String get bookingIncorrectOrMissingFields => + 'Champs incorrects ou manquants'; + + @override + String get bookingInterval => 'Intervalle'; + + @override + String get bookingInvalidIntervalError => 'Intervalle invalide'; + + @override + String get bookingInvalidDates => 'Dates invalides'; + + @override + String get bookingInvalidRoom => 'Salle invalide'; + + @override + String get bookingKeysRequested => 'Clés demandées'; + + @override + String get bookingManagement => 'Gestion'; + + @override + String get bookingManager => 'Gestionnaire'; + + @override + String get bookingManagerName => 'Nom du gestionnaire'; + + @override + String get bookingMultipleDay => 'Plusieurs jours'; + + @override + String get bookingMyBookings => 'Mes réservations'; + + @override + String get bookingNecessaryKey => 'Clé nécessaire'; + + @override + String get bookingNext => 'Suivant'; + + @override + String get bookingNo => 'Non'; + + @override + String get bookingNoCurrentBooking => 'Pas de réservation en cours'; + + @override + String get bookingNoDateError => 'Veuillez choisir une date'; + + @override + String get bookingNoAppointmentInReccurence => + 'Aucun créneau existe avec ces paramètres de récurrence'; + + @override + String get bookingNoDaySelected => 'Aucun jour sélectionné'; + + @override + String get bookingNoDescriptionError => 'Veuillez entrer une description'; + + @override + String get bookingNoKeys => 'Aucune clé'; + + @override + String get bookingNoNoteError => 'Veuillez entrer une note'; + + @override + String get bookingNoPhoneRegistered => 'Numéro non renseigné'; + + @override + String get bookingNoReasonError => 'Veuillez entrer un motif'; + + @override + String get bookingNoRoomFoundError => 'Aucune salle enregistrée'; + + @override + String get bookingNoRoomFound => 'Aucune salle trouvée'; + + @override + String get bookingNote => 'Note'; + + @override + String get bookingOther => 'Autre'; + + @override + String get bookingPending => 'En attente'; + + @override + String get bookingPrevious => 'Précédent'; + + @override + String get bookingReason => 'Motif'; + + @override + String get bookingRecurrence => 'Récurrence'; + + @override + String get bookingRecurrenceDays => 'Jours de récurrence'; + + @override + String get bookingRecurrenceEndDate => 'Date de fin de récurrence'; + + @override + String get bookingRecurrent => 'Récurrent'; + + @override + String get bookingRegisteredRooms => 'Salles enregistrées'; + + @override + String get bookingRoom => 'Salle'; + + @override + String get bookingRoomName => 'Nom de la salle'; + + @override + String get bookingStartDate => 'Date de début'; + + @override + String get bookingStartHour => 'Heure de début'; + + @override + String get bookingWeeks => 'Semaines'; + + @override + String get bookingYes => 'Oui'; + + @override + String get bookingWeekDayMon => 'Lundi'; + + @override + String get bookingWeekDayTue => 'Mardi'; + + @override + String get bookingWeekDayWed => 'Mercredi'; + + @override + String get bookingWeekDayThu => 'Jeudi'; + + @override + String get bookingWeekDayFri => 'Vendredi'; + + @override + String get bookingWeekDaySat => 'Samedi'; + + @override + String get bookingWeekDaySun => 'Dimanche'; + + @override + String get cinemaAdd => 'Ajouter'; + + @override + String get cinemaAddedSession => 'Séance ajoutée'; + + @override + String get cinemaAddingError => 'Erreur lors de l\'ajout'; + + @override + String get cinemaAddSession => 'Ajouter une séance'; + + @override + String get cinemaCinema => 'Cinéma'; + + @override + String get cinemaDeleteSession => 'Supprimer la séance ?'; + + @override + String get cinemaDeleting => 'Suppression'; + + @override + String get cinemaDuration => 'Durée'; + + @override + String get cinemaEdit => 'Modifier'; + + @override + String get cinemaEditedSession => 'Séance modifiée'; + + @override + String get cinemaEditingError => 'Erreur lors de la modification'; + + @override + String get cinemaEditSession => 'Modifier la séance'; + + @override + String get cinemaEmptyUrl => 'Veuillez entrer une URL'; + + @override + String get cinemaImportFromTMDB => 'Importer depuis TMDB'; + + @override + String get cinemaIncomingSession => 'A l\'affiche'; + + @override + String get cinemaIncorrectOrMissingFields => 'Champs incorrects ou manquants'; + + @override + String get cinemaInvalidUrl => 'URL invalide'; + + @override + String get cinemaGenre => 'Genre'; + + @override + String get cinemaName => 'Nom'; + + @override + String get cinemaNoDateError => 'Veuillez entrer une date'; + + @override + String get cinemaNoDuration => 'Veuillez entrer une durée'; + + @override + String get cinemaNoOverview => 'Aucun synopsis'; + + @override + String get cinemaNoPoster => 'Aucune affiche'; + + @override + String get cinemaNoSession => 'Aucune séance'; + + @override + String get cinemaOverview => 'Synopsis'; + + @override + String get cinemaPosterUrl => 'URL de l\'affiche'; + + @override + String get cinemaSessionDate => 'Jour de la séance'; + + @override + String get cinemaStartHour => 'Heure de début'; + + @override + String get cinemaTagline => 'Slogan'; + + @override + String get cinemaThe => 'Le'; + + @override + String get drawerAdmin => 'Administration'; + + @override + String get drawerAndroidAppLink => + 'https://play.google.com/store/apps/details?id=fr.myecl.titan'; + + @override + String get drawerCopied => 'Copié !'; + + @override + String get drawerDownloadAppOnMobileDevice => + 'Ce site est la version Web de l\'application MyECL. Nous vous invitons à télécharger l\'application. N\'utilisez ce site qu\'en cas de problème avec l\'application.\n'; + + @override + String get drawerIosAppLink => + 'https://apps.apple.com/fr/app/myecl/id6444443430'; + + @override + String get drawerLoginOut => 'Voulez-vous vous déconnecter ?'; + + @override + String get drawerLogOut => 'Déconnexion'; + + @override + String get drawerOr => ' ou '; + + @override + String get drawerSettings => 'Paramètres'; + + @override + String get eventAdd => 'Ajouter'; + + @override + String get eventAddEvent => 'Ajouter un événement'; + + @override + String get eventAddedEvent => 'Événement ajouté'; + + @override + String get eventAddingError => 'Erreur lors de l\'ajout'; + + @override + String get eventAllDay => 'Toute la journée'; + + @override + String get eventConfirm => 'Confirmer'; + + @override + String get eventConfirmEvent => 'Confirmer l\'événement ?'; + + @override + String get eventConfirmation => 'Confirmation'; + + @override + String get eventConfirmed => 'Confirmé'; + + @override + String get eventDates => 'Dates'; + + @override + String get eventDecline => 'Refuser'; + + @override + String get eventDeclineEvent => 'Refuser l\'événement ?'; + + @override + String get eventDeclined => 'Refusé'; + + @override + String get eventDelete => 'Supprimer'; + + @override + String eventDeleteConfirm(String name) { + return 'Supprimer l\'event $name ?'; + } + + @override + String get eventDeletedEvent => 'Événement supprimé'; + + @override + String get eventDeleting => 'Suppression'; + + @override + String get eventDeletingError => 'Erreur lors de la suppression'; + + @override + String get eventDeletingEvent => 'Supprimer l\'événement ?'; + + @override + String get eventDescription => 'Description'; + + @override + String get eventEdit => 'Modifier'; + + @override + String get eventEditEvent => 'Modifier un événement'; + + @override + String get eventEditedEvent => 'Événement modifié'; + + @override + String get eventEditingError => 'Erreur lors de la modification'; + + @override + String get eventEndDate => 'Date de fin'; + + @override + String get eventEndHour => 'Heure de fin'; + + @override + String get eventError => 'Erreur'; + + @override + String get eventEventList => 'Liste des événements'; + + @override + String get eventEventType => 'Type d\'événement'; + + @override + String get eventEvery => 'Tous les'; + + @override + String get eventHistory => 'Historique'; + + @override + String get eventIncorrectOrMissingFields => + 'Certains champs sont incorrects ou manquants'; + + @override + String get eventInterval => 'Intervalle'; + + @override + String get eventInvalidDates => + 'La date de fin doit être après la date de début'; + + @override + String get eventInvalidIntervalError => + 'Veuillez entrer un intervalle valide'; + + @override + String get eventLocation => 'Lieu'; + + @override + String get eventModifiedEvent => 'Événement modifié'; + + @override + String get eventModifyingError => 'Erreur lors de la modification'; + + @override + String get eventMyEvents => 'Mes événements'; + + @override + String get eventName => 'Nom'; + + @override + String get eventNext => 'Suivant'; + + @override + String get eventNo => 'Non'; + + @override + String get eventNoCurrentEvent => 'Aucun événement en cours'; + + @override + String get eventNoDateError => 'Veuillez entrer une date'; + + @override + String get eventNoDaySelected => 'Aucun jour sélectionné'; + + @override + String get eventNoDescriptionError => 'Veuillez entrer une description'; + + @override + String get eventNoEvent => 'Aucun événement'; + + @override + String get eventNoNameError => 'Veuillez entrer un nom'; + + @override + String get eventNoOrganizerError => 'Veuillez entrer un organisateur'; + + @override + String get eventNoPlaceError => 'Veuillez entrer un lieu'; + + @override + String get eventNoPhoneRegistered => 'Numéro non renseigné'; + + @override + String get eventNoRuleError => 'Veuillez entrer une règle de récurrence'; + + @override + String get eventOrganizer => 'Organisateur'; + + @override + String get eventOther => 'Autre'; + + @override + String get eventPending => 'En attente'; + + @override + String get eventPrevious => 'Précédent'; + + @override + String get eventRecurrence => 'Récurrence'; + + @override + String get eventRecurrenceDays => 'Jours de récurrence'; + + @override + String get eventRecurrenceEndDate => 'Date de fin de la récurrence'; + + @override + String get eventRecurrenceRule => 'Règle de récurrence'; + + @override + String get eventRoom => 'Salle'; + + @override + String get eventStartDate => 'Date de début'; + + @override + String get eventStartHour => 'Heure de début'; + + @override + String get eventTitle => 'Événements'; + + @override + String get eventYes => 'Oui'; + + @override + String get eventEventEvery => 'Toutes les'; + + @override + String get eventWeeks => 'semaines'; + + @override + String get eventDayMon => 'Lundi'; + + @override + String get eventDayTue => 'Mardi'; + + @override + String get eventDayWed => 'Mercredi'; + + @override + String get eventDayThu => 'Jeudi'; + + @override + String get eventDayFri => 'Vendredi'; + + @override + String get eventDaySat => 'Samedi'; + + @override + String get eventDaySun => 'Dimanche'; + + @override + String get globalConfirm => 'Confirmer'; + + @override + String get globalCancel => 'Annuler'; + + @override + String get globalIrreversibleAction => 'Cette action est irréversible'; + + @override + String globalOptionnal(String text) { + return '$text (Optionnel)'; + } + + @override + String get homeCalendar => 'Calendrier'; + + @override + String get homeEventOf => 'Évènements du'; + + @override + String get homeIncomingEvents => 'Évènements à venir'; + + @override + String get homeLastInfos => 'Dernières annonces'; + + @override + String get homeNoEvents => 'Aucun évènement'; + + @override + String get homeTranslateDayShortMon => 'Lun'; + + @override + String get homeTranslateDayShortTue => 'Mar'; + + @override + String get homeTranslateDayShortWed => 'Mer'; + + @override + String get homeTranslateDayShortThu => 'Jeu'; + + @override + String get homeTranslateDayShortFri => 'Ven'; + + @override + String get homeTranslateDayShortSat => 'Sam'; + + @override + String get homeTranslateDayShortSun => 'Dim'; + + @override + String get loanAdd => 'Ajouter'; + + @override + String get loanAddLoan => 'Ajouter un prêt'; + + @override + String get loanAddObject => 'Ajouter un objet'; + + @override + String get loanAddedLoan => 'Prêt ajouté'; + + @override + String get loanAddedObject => 'Objet ajouté'; + + @override + String get loanAddedRoom => 'Salle ajoutée'; + + @override + String get loanAddingError => 'Erreur lors de l\'ajout'; + + @override + String get loanAdmin => 'Administrateur'; + + @override + String get loanAvailable => 'Disponible'; + + @override + String get loanAvailableMultiple => 'Disponibles'; + + @override + String get loanBorrowed => 'Emprunté'; + + @override + String get loanBorrowedMultiple => 'Empruntés'; + + @override + String get loanAnd => 'et'; + + @override + String get loanAssociation => 'Association'; + + @override + String get loanAvailableItems => 'Objets disponibles'; + + @override + String get loanBeginDate => 'Date du début du prêt'; + + @override + String get loanBorrower => 'Emprunteur'; + + @override + String get loanCaution => 'Caution'; + + @override + String get loanCancel => 'Annuler'; + + @override + String get loanConfirm => 'Confirmer'; + + @override + String get loanConfirmation => 'Confirmation'; + + @override + String get loanDates => 'Dates'; + + @override + String get loanDays => 'Jours'; + + @override + String get loanDelay => 'Délai de la prolongation'; + + @override + String get loanDelete => 'Supprimer'; + + @override + String get loanDeletingLoan => 'Supprimer le prêt ?'; + + @override + String get loanDeletedItem => 'Objet supprimé'; + + @override + String get loanDeletedLoan => 'Prêt supprimé'; + + @override + String get loanDeleting => 'Suppression'; + + @override + String get loanDeletingError => 'Erreur lors de la suppression'; + + @override + String get loanDeletingItem => 'Supprimer l\'objet ?'; + + @override + String get loanDuration => 'Durée'; + + @override + String get loanEdit => 'Modifier'; + + @override + String get loanEditItem => 'Modifier l\'objet'; + + @override + String get loanEditLoan => 'Modifier le prêt'; + + @override + String get loanEditedRoom => 'Salle modifiée'; + + @override + String get loanEndDate => 'Date de fin du prêt'; + + @override + String get loanEnded => 'Terminé'; + + @override + String get loanEnterDate => 'Veuillez entrer une date'; + + @override + String get loanExtendedLoan => 'Prêt prolongé'; + + @override + String get loanExtendingError => 'Erreur lors de la prolongation'; + + @override + String get loanHistory => 'Historique'; + + @override + String get loanIncorrectOrMissingFields => + 'Des champs sont manquants ou incorrects'; + + @override + String get loanInvalidNumber => 'Veuillez entrer un nombre'; + + @override + String get loanInvalidDates => 'Les dates ne sont pas valides'; + + @override + String get loanItem => 'Objet'; + + @override + String get loanItems => 'Objets'; + + @override + String get loanItemHandling => 'Gestion des objets'; + + @override + String get loanItemSelected => 'objet sélectionné'; + + @override + String get loanItemsSelected => 'objets sélectionnés'; + + @override + String get loanLendingDuration => 'Durée possible du prêt'; + + @override + String get loanLoan => 'Prêt'; + + @override + String get loanLoanHandling => 'Gestion des prêts'; + + @override + String get loanLooking => 'Rechercher'; + + @override + String get loanName => 'Nom'; + + @override + String get loanNext => 'Suivant'; + + @override + String get loanNo => 'Non'; + + @override + String get loanNoAssociationsFounded => 'Aucune association trouvée'; + + @override + String get loanNoAvailableItems => 'Aucun objet disponible'; + + @override + String get loanNoBorrower => 'Aucun emprunteur'; + + @override + String get loanNoItems => 'Aucun objet'; + + @override + String get loanNoItemSelected => 'Aucun objet sélectionné'; + + @override + String get loanNoLoan => 'Aucun prêt'; + + @override + String get loanNoReturnedDate => 'Pas de date de retour'; + + @override + String get loanQuantity => 'Quantité'; + + @override + String get loanNone => 'Aucun'; + + @override + String get loanNote => 'Note'; + + @override + String get loanNoValue => 'Veuillez entrer une valeur'; + + @override + String get loanOnGoing => 'En cours'; + + @override + String get loanOnGoingLoan => 'Prêt en cours'; + + @override + String get loanOthers => 'autres'; + + @override + String get loanPaidCaution => 'Caution payée'; + + @override + String get loanPositiveNumber => 'Veuillez entrer un nombre positif'; + + @override + String get loanPrevious => 'Précédent'; + + @override + String get loanReturned => 'Rendu'; + + @override + String get loanReturnedLoan => 'Prêt rendu'; + + @override + String get loanReturningError => 'Erreur lors du retour'; + + @override + String get loanReturningLoan => 'Retour'; + + @override + String get loanReturnLoan => 'Rendre le prêt ?'; + + @override + String get loanReturnLoanDescription => 'Voulez-vous rendre ce prêt ?'; + + @override + String get loanToReturn => 'A rendre'; + + @override + String get loanUnavailable => 'Indisponible'; + + @override + String get loanUpdate => 'Modifier'; + + @override + String get loanUpdatedItem => 'Objet modifié'; + + @override + String get loanUpdatedLoan => 'Prêt modifié'; + + @override + String get loanUpdatingError => 'Erreur lors de la modification'; + + @override + String get loanYes => 'Oui'; + + @override + String get loginAppName => 'MyECL'; + + @override + String get loginCreateAccount => 'Créer un compte'; + + @override + String get loginForgotPassword => 'Mot de passe oublié ?'; + + @override + String get loginFruitVegetableOrders => 'Commandes de fruits et légumes'; + + @override + String get loginInterfaceCustomization => 'Personnalisation de l\'interface'; + + @override + String get loginLoginFailed => 'Échec de la connexion'; + + @override + String get loginMadeBy => 'Développé par ProximApp'; + + @override + String get loginMaterialLoans => 'Gestion des prêts de matériel'; + + @override + String get loginNewTermsElections => 'L\'élection des nouveaux mandats'; + + @override + String get loginRaffles => 'Tombolas'; + + @override + String get loginSignIn => 'Se connecter'; + + @override + String get loginRegister => 'S\'inscrire'; + + @override + String get loginShortDescription => 'L\'application de l\'associatif'; + + @override + String get loginUpcomingEvents => 'Les évènements à venir'; + + @override + String get loginUpcomingScreenings => 'Les prochaines séances'; + + @override + String get othersCheckInternetConnection => + 'Veuillez vérifier votre connexion internet'; + + @override + String get othersRetry => 'Réessayer'; + + @override + String get othersTooOldVersion => + 'Votre version de l\'application est trop ancienne.\n\nVeuillez mettre à jour l\'application.'; + + @override + String get othersUnableToConnectToServer => + 'Impossible de se connecter au serveur'; + + @override + String get othersVersion => 'Version'; + + @override + String get othersNoModule => + 'Aucun module disponible, veuillez réessayer ultérieurement 😢😢'; + + @override + String get othersAdmin => 'Admin'; + + @override + String get othersError => 'Une erreur est survenue'; + + @override + String get othersNoValue => 'Veuillez entrer une valeur'; + + @override + String get othersInvalidNumber => 'Veuillez entrer un nombre'; + + @override + String get othersNoDateError => 'Veuillez entrer une date'; + + @override + String get othersImageSizeTooBig => + 'La taille de l\'image ne doit pas dépasser 4 Mio'; + + @override + String get othersImageError => 'Erreur lors de l\'ajout de l\'image'; + + @override + String get paiementAccept => 'Accepter'; + + @override + String get paiementAccessPage => 'Accéder à la page'; + + @override + String get paiementAdd => 'Ajouter'; + + @override + String get paiementAddedSeller => 'Vendeur ajouté'; + + @override + String get paiementAddingSellerError => 'Erreur lors de l\'ajout du vendeur'; + + @override + String get paiementAddingStoreError => 'Erreur lors de l\'ajout du magasin'; + + @override + String get paiementAddSeller => 'Ajouter un vendeur'; + + @override + String get paiementAddStore => 'Ajouter un magasin'; + + @override + String get paiementAddThisDevice => 'Ajouter cet appareil'; + + @override + String get paiementAdmin => 'Administrateur'; + + @override + String get paiementAmount => 'Montant'; + + @override + String get paiementAskDeviceActivation => + 'Demande d\'activation de l\'appareil'; + + @override + String get paiementAStore => 'un magasin'; + + @override + String get paiementAt => 'à'; + + @override + String get paiementAuthenticationRequired => + 'Authentification requise pour payer'; + + @override + String get paiementAuthentificationFailed => 'Échec de l\'authentification'; + + @override + String get paiementBalanceAfterTopUp => 'Solde après recharge :'; + + @override + String get paiementBalanceAfterTransaction => 'Solde après paiement : '; + + @override + String get paiementBank => 'Encaisser'; + + @override + String get paiementBillingSpace => 'Espace facturation'; + + @override + String get paiementCameraPermissionRequired => + 'Permission d\'accès à la caméra requise'; + + @override + String get paiementCameraPerssionRequiredDescription => + 'Pour scanner un QR Code, vous devez autoriser l\'accès à la caméra.'; + + @override + String get paiementCanBank => 'Peut encaisser'; + + @override + String get paiementCanCancelTransaction => 'Peut annuler des transactions'; + + @override + String get paiementCancel => 'Annuler'; + + @override + String get paiementCancelled => 'Annulé'; + + @override + String get paiementCancelledTransaction => 'Paiement annulé'; + + @override + String get paiementCancelTransaction => 'Annuler la transaction'; + + @override + String get paiementCancelTransactions => 'Annuler les transactions'; + + @override + String get paiementCanManageSellers => 'Peut gérer les vendeurs'; + + @override + String get paiementCanSeeHistory => 'Peut voir l\'historique'; + + @override + String get paiementCantLaunchURL => 'Impossible d\'ouvrir le lien'; + + @override + String get paiementClose => 'Fermer'; + + @override + String get paiementCreate => 'Créer'; + + @override + String get paiementCreateInvoice => 'Créer une facture'; + + @override + String get paiementDecline => 'Refuser'; + + @override + String get paiementDeletedSeller => 'Vendeur supprimé'; + + @override + String get paiementDeleteInvoice => 'Supprimer la facture'; + + @override + String get paiementDeleteSeller => 'Supprimer le vendeur'; + + @override + String get paiementDeleteSellerDescription => + 'Voulez-vous vraiment supprimer ce vendeur ?'; + + @override + String get paiementDeleteSuccessfully => 'Supprimé avec succès'; + + @override + String get paiementDeleteStore => 'Supprimer le magasin'; + + @override + String get paiementDeleteStoreDescription => + 'Voulez-vous vraiment supprimer ce magasin ?'; + + @override + String get paiementDeleteStoreError => 'Impossible de supprimer le magasin'; + + @override + String get paiementDeletingSellerError => + 'Erreur lors de la suppression du vendeur'; + + @override + String get paiementDeviceActivationReceived => + 'La demande d\'activation est prise en compte, veuilliez consulter votre boite mail pour finaliser la démarche'; + + @override + String get paiementDeviceNotActivated => 'Appareil non activé'; + + @override + String get paiementDeviceNotActivatedDescription => + 'Votre appareil n\'est pas encore activé. \nPour l\'activer, veuillez vous rendre sur la page des appareils.'; + + @override + String get paiementDeviceNotRegistered => 'Appareil non enregistré'; + + @override + String get paiementDeviceNotRegisteredDescription => + 'Votre appareil n\'est pas encore enregistré. \nPour l\'enregistrer, veuillez vous rendre sur la page des appareils.'; + + @override + String get paiementDeviceRecoveryError => + 'Erreur lors de la récupération de l\'appareil'; + + @override + String get paiementDeviceRevoked => 'Appareil révoqué'; + + @override + String get paiementDeviceRevokingError => + 'Erreur lors de la révocation de l\'appareil'; + + @override + String get paiementDevices => 'Appareils'; + + @override + String get paiementDoneTransaction => 'Transaction effectuée'; + + @override + String get paiementDownload => 'Télécharger'; + + @override + String paiementEditStore(String store) { + return 'Modifier le magasin $store'; + } + + @override + String get paiementErrorDeleting => 'Erreur lors de la suppression'; + + @override + String get paiementErrorUpdatingStatus => + 'Erreur lors de la mise à jour du statut'; + + @override + String paiementFromTo(DateTime from, DateTime to) { + final intl.DateFormat fromDateFormat = intl.DateFormat.yMd(localeName); + final String fromString = fromDateFormat.format(from); + final intl.DateFormat toDateFormat = intl.DateFormat.yMd(localeName); + final String toString = toDateFormat.format(to); + + return 'Du $fromString au $toString'; + } + + @override + String get paiementGetBalanceError => + 'Erreur lors de la récupération du solde : '; + + @override + String get paiementGetTransactionsError => + 'Erreur lors de la récupération des transactions : '; + + @override + String get paiementHandOver => 'Passation'; + + @override + String get paiementHistory => 'Historique'; + + @override + String get paiementInvoiceCreatedSuccessfully => 'Facture créée avec succès'; + + @override + String get paiementInvoices => 'Factures'; + + @override + String paiementInvoicesPerPage(int quantity) { + return '$quantity factures/page'; + } + + @override + String get paiementLastTransactions => 'Dernières transactions'; + + @override + String get paiementLimitedTo => 'Limité à'; + + @override + String get paiementManagement => 'Gestion'; + + @override + String get paiementManageSellers => 'Gérer les vendeurs'; + + @override + String get paiementMarkPaid => 'Marquer comme payé'; + + @override + String get paiementMarkReceived => 'Marquer comme reçu'; + + @override + String get paiementMarkUnpaid => 'Marquer comme non payé'; + + @override + String get paiementMaxAmount => + 'Le montant maximum de votre portefeuille est de'; + + @override + String get paiementMean => 'Moyenne : '; + + @override + String get paiementModify => 'Modifier'; + + @override + String get paiementModifyingStoreError => + 'Erreur lors de la modification du magasin'; + + @override + String get paiementModifySuccessfully => 'Modifié avec succès'; + + @override + String get paiementNewCGU => 'Nouvelles Conditions Générales d\'Utilisation'; + + @override + String get paiementNext => 'Suivant'; + + @override + String get paiementNextAccountable => 'Prochain responsable'; + + @override + String get paiementNoInvoiceToCreate => 'Aucune facture à créer'; + + @override + String get paiementNoMembership => 'Aucune adhésion'; + + @override + String get paiementNoMembershipDescription => + 'Ce produit n\'est pas disponnible pour les non-adhérents. Confirmer l\'encaissement ?'; + + @override + String get paiementNoThanks => 'Non merci'; + + @override + String get paiementNoTransaction => 'Aucune transaction'; + + @override + String get paiementNoTransactionForThisMonth => + 'Aucune transaction pour ce mois'; + + @override + String get paiementOf => 'de'; + + @override + String get paiementPaid => 'Payé'; + + @override + String get paiementPay => 'Payer'; + + @override + String get paiementPayment => 'Paiement'; + + @override + String get paiementPayWithHA => 'Payer avec HelloAsso'; + + @override + String get paiementPending => 'En attente'; + + @override + String get paiementPersonalBalance => 'Solde personnel'; + + @override + String get paiementAddFunds => 'Ajouter des fonds'; + + @override + String get paiementInsufficientFunds => 'Fonds insuffisants'; + + @override + String get paiementTimeRemaining => 'Temps restant'; + + @override + String get paiementHurryUp => 'Dépêchez-vous !'; + + @override + String get paiementCompletePayment => 'Finaliser le paiement'; + + @override + String get paiementConfirmPayment => 'Confirmer le paiement'; + + @override + String get paiementPleaseAcceptPopup => 'Veuillez autoriser les popups'; + + @override + String get paiementPleaseAcceptTOS => + 'Veuillez accepter les Conditions Générales d\'Utilisation.'; + + @override + String get paiementPleaseAddDevice => + 'Veuillez ajouter cet appareil pour payer'; + + @override + String get paiementPleaseAuthenticate => 'Veuillez vous authentifier'; + + @override + String get paiementPleaseEnterMinAmount => + 'Veuillez entrer un montant supérieur à 1'; + + @override + String get paiementPleaseEnterValidAmount => + 'Veuillez entrer un montant valide'; + + @override + String get paiementProceedSuccessfully => 'Paiement effectué avec succès'; + + @override + String get paiementQRCodeAlreadyUsed => 'QR Code déjà utilisé'; + + @override + String get paiementReactivateRevokedDeviceDescription => + 'Votre appareil a été révoqué. \nPour le réactiver, veuillez vous rendre sur la page des appareils.'; + + @override + String get paiementReceived => 'Reçu'; + + @override + String get paiementRefund => 'Remboursement'; + + @override + String get paiementRefundAction => 'Rembourser'; + + @override + String get paiementRefundedThe => 'Remboursé le'; + + @override + String get paiementRevokeDevice => 'Révoquer l\'appareil ?'; + + @override + String get paiementRevokeDeviceDescription => + 'Vous ne pourrez plus utiliser cet appareil pour les paiements'; + + @override + String get paiementRightsOf => 'Droits de'; + + @override + String get paiementRightsUpdated => 'Droits mis à jour'; + + @override + String get paiementRightsUpdateError => + 'Erreur lors de la mise à jour des droits'; + + @override + String get paiementScan => 'Scanner'; + + @override + String get paiementScanAlreadyUsedQRCode => 'QR Code déjà utilisé'; + + @override + String get paiementScanCode => 'Scanner un code'; + + @override + String get paiementScanNoMembership => 'Pas d\'adhésion'; + + @override + String get paiementScanNoMembershipConfirmation => + 'Ce produit n\'est pas disponnible pour les non-adhérents. Confirmer l\'encaissement ?'; + + @override + String get paiementSeeHistory => 'Voir l\'historique'; + + @override + String get paiementSelectStructure => 'Choisir une structure'; + + @override + String get paiementSellerError => 'Vous n\'êtes pas vendeur de ce magasin'; + + @override + String get paiementSellerRigths => 'Droits du vendeur'; + + @override + String get paiementSellersOf => 'Les vendeurs de'; + + @override + String get paiementSettings => 'Paramètres'; + + @override + String get paiementSpent => 'Déboursé'; + + @override + String get paiementStats => 'Stats'; + + @override + String get paiementStoreBalance => 'Solde du magasin'; + + @override + String get paiementStoreDeleted => 'Magasin supprimée'; + + @override + String paiementStructureManagement(String structure) { + return 'Gestion de $structure'; + } + + @override + String get paiementStoreName => 'Nom du magasin'; + + @override + String get paiementStores => 'Magasins'; + + @override + String get paiementStructureAdmin => 'Administrateur de la structure'; + + @override + String get paiementSuccededTransaction => 'Paiement réussi'; + + @override + String get paiementConfirmYourPurchase => 'Confirmer votre achat'; + + @override + String get paiementYourBalance => 'Votre solde'; + + @override + String get paiementPaymentSuccessful => 'Paiement réussi !'; + + @override + String get paiementPaymentCanceled => 'Paiement annulé'; + + @override + String get paiementPaymentRequest => 'Demande de paiement'; + + @override + String get paiementPaymentRequestAccepted => 'Demande de paiement acceptée'; + + @override + String get paiementPaymentRequestRefused => 'Demande de paiement refusée'; + + @override + String get paiementPaymentRequestError => + 'Erreur lors du traitement de la demande'; + + @override + String get paiementRefuse => 'Refuser'; + + @override + String get paiementSuccessfullyAddedStore => 'Magasin ajoutée avec succès'; + + @override + String get paiementSuccessfullyModifiedStore => + 'Magasin modifiée avec succès'; + + @override + String get paiementThe => 'Le'; + + @override + String get paiementThisDevice => '(cet appareil)'; + + @override + String get paiementTopUp => 'Recharge'; + + @override + String get paiementTopUpAction => 'Recharger'; + + @override + String get paiementTotalDuringPeriod => 'Total sur la période'; + + @override + String get paiementTransaction => 'ransaction'; + + @override + String get paiementTransactionCancelled => 'Transaction annulée'; + + @override + String get paiementTransactionCancelledDescription => + 'Voulez-vous vraiment annuler la transaction de'; + + @override + String get paiementTransactionCancelledError => + 'Erreur lors de l\'annulation de la transaction'; + + @override + String get paiementTransferStructure => 'Transfert de structure'; + + @override + String get paiementTransferStructureDescription => + 'Le nouveau responsable aura accès à toutes les fonctionnalités de gestion de la structure. Vous allez recevoir un email pour valider ce transfert. Le lien ne sera actif que pendant 20 minutes. Cette action est irréversible. Êtes-vous sûr de vouloir continuer ?'; + + @override + String get paiementTransferStructureError => + 'Erreur lors du transfert de la structure'; + + @override + String get paiementTransferStructureSuccess => + 'Transfert de structure demandé avec succès'; + + @override + String get paiementUnknownDevice => 'Appareil inconnu'; + + @override + String get paiementValidUntil => 'Valide jusqu\'à'; + + @override + String get paiementYouAreTransferingStructureTo => + 'Vous êtes sur le point de transférer la structure à '; + + @override + String get phAddNewJournal => 'Ajouter un nouveau journal'; + + @override + String get phNameField => 'Nom : '; + + @override + String get phDateField => 'Date : '; + + @override + String get phDelete => 'Voulez-vous vraiment supprimer ce journal ?'; + + @override + String get phIrreversibleAction => 'Cette action est irréversible'; + + @override + String get phToHeavyFile => 'Fichier trop volumineux'; + + @override + String get phAddPdfFile => 'Ajouter un fichier PDF'; + + @override + String get phEditPdfFile => 'Modifier le fichier PDF'; + + @override + String get phPhName => 'Nom du PH'; + + @override + String get phDate => 'Date'; + + @override + String get phAdded => 'Ajouté'; + + @override + String get phEdited => 'Modifié'; + + @override + String get phAddingFileError => 'Erreur d\'ajout'; + + @override + String get phMissingInformatonsOrPdf => + 'Informations manquantes ou fichier PDF manquant'; + + @override + String get phAdd => 'Ajouter'; + + @override + String get phEdit => 'Modifier'; + + @override + String get phSeePreviousJournal => 'Voir les anciens journaux'; + + @override + String get phNoJournalInDatabase => 'Pas encore de PH dans la base de donnée'; + + @override + String get phSuccesDowloading => 'Téléchargé avec succès'; + + @override + String get phonebookAdd => 'Ajouter'; + + @override + String get phonebookAddAssociation => 'Ajouter une association'; + + @override + String get phonebookAddAssociationGroupement => + 'Ajouter un groupement d\'association'; + + @override + String get phonebookAddedAssociation => 'Association ajoutée'; + + @override + String get phonebookAddedMember => 'Membre ajouté'; + + @override + String get phonebookAddingError => 'Erreur lors de l\'ajout'; + + @override + String get phonebookAddMember => 'Ajouter un membre'; + + @override + String get phonebookAddRole => 'Ajouter un rôle'; + + @override + String get phonebookAdmin => 'Admin'; + + @override + String get phonebookAll => 'Toutes'; + + @override + String get phonebookApparentName => 'Nom public du rôle :'; + + @override + String get phonebookAssociation => 'Association'; + + @override + String get phonebookAssociationDetail => 'Détail de l\'association :'; + + @override + String get phonebookAssociationGroupement => 'Groupement d\'association'; + + @override + String get phonebookAssociationKind => 'Type d\'association :'; + + @override + String get phonebookAssociationName => 'Nom de l\'association'; + + @override + String get phonebookAssociations => 'Associations'; + + @override + String get phonebookCancel => 'Annuler'; + + @override + String phonebookChangeTermYear(int year) { + return 'Passer au mandat $year'; + } + + @override + String get phonebookChangeTermConfirm => + 'Êtes-vous sûr de vouloir changer tout le mandat ?\nCette action est irréversible !'; + + @override + String get phonebookClose => 'Fermer'; + + @override + String get phonebookConfirm => 'Confirmer'; + + @override + String get phonebookCopied => 'Copié dans le presse-papier'; + + @override + String get phonebookDeactivateAssociation => 'Désactiver l\'association'; + + @override + String get phonebookDeactivatedAssociation => 'Association désactivée'; + + @override + String get phonebookDeactivatedAssociationWarning => + 'Attention, cette association est désactivée, vous ne pouvez pas la modifier'; + + @override + String phonebookDeactivateSelectedAssociation(String association) { + return 'Désactiver l\'association $association ?'; + } + + @override + String get phonebookDeactivatingError => 'Erreur lors de la désactivation'; + + @override + String get phonebookDetail => 'Détail :'; + + @override + String get phonebookDelete => 'Supprimer'; + + @override + String get phonebookDeleteAssociation => 'Supprimer l\'association'; + + @override + String phonebookDeleteSelectedAssociation(String association) { + return 'Supprimer l\'association $association ?'; + } + + @override + String get phonebookDeleteAssociationDescription => + 'Ceci va supprimer l\'historique de l\'association'; + + @override + String get phonebookDeletedAssociation => 'Association supprimée'; + + @override + String get phonebookDeletedMember => 'Membre supprimé'; + + @override + String get phonebookDeleteRole => 'Supprimer le rôle'; + + @override + String phonebookDeleteUserRole(String name) { + return 'Supprimer le rôle de l\'utilisateur $name ?'; + } + + @override + String get phonebookDeactivating => 'Désactiver l\'association ?'; + + @override + String get phonebookDeleting => 'Suppression'; + + @override + String get phonebookDeletingError => 'Erreur lors de la suppression'; + + @override + String get phonebookDescription => 'Description'; + + @override + String get phonebookEdit => 'Modifier'; + + @override + String get phonebookEditAssociationGroupement => + 'Modifier le groupement d\'association'; + + @override + String get phonebookEditAssociationGroups => 'Gérer les groupes'; + + @override + String get phonebookEditAssociationInfo => 'Modifier'; + + @override + String get phonebookEditAssociationMembers => 'Gérer les membres'; + + @override + String get phonebookEditRole => 'Modifier le rôle'; + + @override + String get phonebookEditMembership => 'Modifier le rôle'; + + @override + String get phonebookEmail => 'Email :'; + + @override + String get phonebookEmailCopied => 'Email copié dans le presse-papier'; + + @override + String get phonebookEmptyApparentName => 'Veuillez entrer un nom de role'; + + @override + String get phonebookEmptyFieldError => 'Un champ n\'est pas rempli'; + + @override + String get phonebookEmptyKindError => + 'Veuillez choisir un type d\'association'; + + @override + String get phonebookEmptyMember => 'Aucun membre sélectionné'; + + @override + String get phonebookErrorAssociationLoading => + 'Erreur lors du chargement de l\'association'; + + @override + String get phonebookErrorAssociationNameEmpty => + 'Veuillez entrer un nom d\'association'; + + @override + String get phonebookErrorAssociationPicture => + 'Erreur lors de la modification de la photo d\'association'; + + @override + String get phonebookErrorKindsLoading => + 'Erreur lors du chargement des types d\'association'; + + @override + String get phonebookErrorLoadAssociationList => + 'Erreur lors du chargement de la liste des associations'; + + @override + String get phonebookErrorLoadAssociationMember => + 'Erreur lors du chargement des membres de l\'association'; + + @override + String get phonebookErrorLoadAssociationPicture => + 'Erreur lors du chargement de la photo d\'association'; + + @override + String get phonebookErrorLoadProfilePicture => 'Erreur'; + + @override + String get phonebookErrorRoleTagsLoading => + 'Erreur lors du chargement des tags de rôle'; + + @override + String get phonebookExistingMembership => + 'Ce membre est déjà dans le mandat actuel'; + + @override + String get phonebookFilter => 'Filtrer'; + + @override + String get phonebookFilterDescription => 'Filtrer les associations par type'; + + @override + String get phonebookFirstname => 'Prénom :'; + + @override + String get phonebookGroupementDeleted => 'Groupement d\'association supprimé'; + + @override + String get phonebookGroupementDeleteError => + 'Erreur lors de la suppression du groupement d\'association'; + + @override + String get phonebookGroupementName => 'Nom du groupement'; + + @override + String phonebookGroups(String association) { + return 'Gérer les groupes de $association'; + } + + @override + String phonebookTerm(int year) { + return 'Mandat $year'; + } + + @override + String get phonebookTermChangingError => + 'Erreur lors du changement de mandat'; + + @override + String get phonebookMember => 'Membre'; + + @override + String get phonebookMemberReordered => 'Membre réordonné'; + + @override + String phonebookMembers(String association) { + return 'Gérer les membres de $association'; + } + + @override + String get phonebookMembershipAssociationError => + 'Veuillez choisir une association'; + + @override + String get phonebookMembershipRole => 'Rôle :'; + + @override + String get phonebookMembershipRoleError => 'Veuillez choisir un rôle'; + + @override + String phonebookModifyMembership(String name) { + return 'Modifier le rôle de $name'; + } + + @override + String get phonebookName => 'Nom :'; + + @override + String get phonebookNameCopied => 'Nom et prénom copié dans le presse-papier'; + + @override + String get phonebookNamePure => 'Nom'; + + @override + String get phonebookNewTerm => 'Nouveau mandat'; + + @override + String get phonebookNewTermConfirmed => 'Mandat changé'; + + @override + String get phonebookNickname => 'Surnom :'; + + @override + String get phonebookNicknameCopied => 'Surnom copié dans le presse-papier'; + + @override + String get phonebookNoAssociationFound => 'Aucune association trouvée'; + + @override + String get phonebookNoMember => 'Aucun membre'; + + @override + String get phonebookNoMemberRole => 'Aucun role trouvé'; + + @override + String get phonebookNoRoleTags => 'Aucun tag de rôle trouvé'; + + @override + String get phonebookPhone => 'Téléphone :'; + + @override + String get phonebookPhonebook => 'Annuaire'; + + @override + String get phonebookPhonebookSearch => 'Rechercher'; + + @override + String get phonebookPhonebookSearchAssociation => 'Association'; + + @override + String get phonebookPhonebookSearchField => 'Rechercher :'; + + @override + String get phonebookPhonebookSearchName => 'Nom/Prénom/Surnom'; + + @override + String get phonebookPhonebookSearchRole => 'Poste'; + + @override + String get phonebookPresidentRoleTag => 'Prez\''; + + @override + String get phonebookPromoNotGiven => 'Promo non renseignée'; + + @override + String phonebookPromotion(int year) { + return 'Promotion $year'; + } + + @override + String get phonebookReorderingError => 'Erreur lors du réordonnement'; + + @override + String get phonebookResearch => 'Rechercher'; + + @override + String get phonebookRolePure => 'Rôle'; + + @override + String get phonebookSearchUser => 'Rechercher un utilisateur'; + + @override + String get phonebookTooHeavyAssociationPicture => + 'L\'image est trop lourde (max 4Mo)'; + + @override + String get phonebookUpdateGroups => 'Mettre à jour les groupes'; + + @override + String get phonebookUpdatedAssociation => 'Association modifiée'; + + @override + String get phonebookUpdatedAssociationPicture => + 'La photo d\'association a été changée'; + + @override + String get phonebookUpdatedGroups => 'Groupes mis à jour'; + + @override + String get phonebookUpdatedMember => 'Membre modifié'; + + @override + String get phonebookUpdatingError => 'Erreur lors de la modification'; + + @override + String get phonebookValidation => 'Valider'; + + @override + String get purchasesPurchases => 'Achats'; + + @override + String get purchasesResearch => 'Rechercher'; + + @override + String get purchasesNoPurchasesFound => 'Aucun achat trouvé'; + + @override + String get purchasesNoTickets => 'Aucun ticket'; + + @override + String get purchasesTicketsError => 'Erreur lors du chargement des tickets'; + + @override + String get purchasesPurchasesError => 'Erreur lors du chargement des achats'; + + @override + String get purchasesNoPurchases => 'Aucun achat'; + + @override + String get purchasesTimes => 'fois'; + + @override + String get purchasesAlreadyUsed => 'Déjà utilisé'; + + @override + String get purchasesNotPaid => 'Non validé'; + + @override + String get purchasesPleaseSelectProduct => 'Veuillez sélectionner un produit'; + + @override + String get purchasesProducts => 'Produits'; + + @override + String get purchasesCancel => 'Annuler'; + + @override + String get purchasesValidate => 'Valider'; + + @override + String get purchasesLeftScan => 'Scans restants'; + + @override + String get purchasesTag => 'Tag'; + + @override + String get purchasesHistory => 'Historique'; + + @override + String get purchasesPleaseSelectSeller => 'Veuillez sélectionner un vendeur'; + + @override + String get purchasesNoTagGiven => 'Attention, aucun tag n\'a été entré'; + + @override + String get purchasesTickets => 'Tickets'; + + @override + String get purchasesNoScannableProducts => 'Aucun produit scannable'; + + @override + String get purchasesLoading => 'En attente de scan'; + + @override + String get purchasesScan => 'Scanner'; + + @override + String get raffleRaffle => 'Tombola'; + + @override + String get rafflePrize => 'Lot'; + + @override + String get rafflePrizes => 'Lots'; + + @override + String get raffleActualRaffles => 'Tombola en cours'; + + @override + String get rafflePastRaffles => 'Tombola passés'; + + @override + String get raffleYourTickets => 'Tous vos tickets'; + + @override + String get raffleCreateMenu => 'Menu de Création'; + + @override + String get raffleNextRaffles => 'Prochaines tombolas'; + + @override + String get raffleNoTicket => 'Vous n\'avez pas de ticket'; + + @override + String get raffleSeeRaffleDetail => 'Voir lots/tickets'; + + @override + String get raffleActualPrize => 'Lots actuels'; + + @override + String get raffleMajorPrize => 'Lot Majeurs'; + + @override + String get raffleTakeTickets => 'Prendre vos tickets'; + + @override + String get raffleNoTicketBuyable => + 'Vous ne pouvez pas achetez de billets pour l\'instant'; + + @override + String get raffleNoCurrentPrize => 'Il n\'y a aucun lots actuellement'; + + @override + String get raffleModifTombola => + 'Vous pouvez modifiez vos tombolas ou en créer de nouvelles, toute décision doit ensuite être prise par les admins'; + + @override + String get raffleCreateYourRaffle => 'Votre menu de création de tombolas'; + + @override + String get rafflePossiblePrice => 'Prix possible'; + + @override + String get raffleInformation => 'Information et Statistiques'; + + @override + String get raffleAccounts => 'Comptes'; + + @override + String get raffleAdd => 'Ajouter'; + + @override + String get raffleUpdatedAmount => 'Montant mis à jour'; + + @override + String get raffleUpdatingError => 'Erreur lors de la mise à jour'; + + @override + String get raffleDeletedPrize => 'Lot supprimé'; + + @override + String get raffleDeletingError => 'Erreur lors de la suppression'; + + @override + String get raffleQuantity => 'Quantité'; + + @override + String get raffleClose => 'Fermer'; + + @override + String get raffleOpen => 'Ouvrir'; + + @override + String get raffleAddTypeTicketSimple => 'Ajouter'; + + @override + String get raffleAddingError => 'Erreur lors de l\'ajout'; + + @override + String get raffleEditTypeTicketSimple => 'Modifier'; + + @override + String get raffleFillField => 'Le champ ne peut pas être vide'; + + @override + String get raffleWaiting => 'Chargement'; + + @override + String get raffleEditingError => 'Erreur lors de la modification'; + + @override + String get raffleAddedTicket => 'Ticket ajouté'; + + @override + String get raffleEditedTicket => 'Ticket modifié'; + + @override + String get raffleAlreadyExistTicket => 'Le ticket existe déjà'; + + @override + String get raffleNumberExpected => 'Un entier est attendu'; + + @override + String get raffleDeletedTicket => 'Ticket supprimé'; + + @override + String get raffleAddPrize => 'Ajouter'; + + @override + String get raffleEditPrize => 'Modifier'; + + @override + String get raffleOpenRaffle => 'Ouvrir la tombola'; + + @override + String get raffleCloseRaffle => 'Fermer la tombola'; + + @override + String get raffleOpenRaffleDescription => + 'Vous allez ouvrir la tombola, les utilisateurs pourront acheter des tickets. Vous ne pourrez plus modifier la tombola. Êtes-vous sûr de vouloir continuer ?'; + + @override + String get raffleCloseRaffleDescription => + 'Vous allez fermer la tombola, les utilisateurs ne pourront plus acheter de tickets. Êtes-vous sûr de vouloir continuer ?'; + + @override + String get raffleNoCurrentRaffle => 'Il n\'y a aucune tombola en cours'; + + @override + String get raffleBoughtTicket => 'Ticket acheté'; + + @override + String get raffleDrawingError => 'Erreur lors du tirage'; + + @override + String get raffleInvalidPrice => 'Le prix doit être supérieur à 0'; + + @override + String get raffleMustBePositive => 'Le nombre doit être strictement positif'; + + @override + String get raffleDraw => 'Tirer'; + + @override + String get raffleDrawn => 'Tiré'; + + @override + String get raffleError => 'Erreur'; + + @override + String get raffleGathered => 'Récolté'; + + @override + String get raffleTickets => 'Tickets'; + + @override + String get raffleTicket => 'ticket'; + + @override + String get raffleWinner => 'Gagnant'; + + @override + String get raffleNoPrize => 'Aucun lot'; + + @override + String get raffleDeletePrize => 'Supprimer le lot'; + + @override + String get raffleDeletePrizeDescription => + 'Vous allez supprimer le lot, êtes-vous sûr de vouloir continuer ?'; + + @override + String get raffleDrawing => 'Tirage'; + + @override + String get raffleDrawingDescription => 'Tirer le gagnant du lot ?'; + + @override + String get raffleDeleteTicket => 'Supprimer le ticket'; + + @override + String get raffleDeleteTicketDescription => + 'Vous allez supprimer le ticket, êtes-vous sûr de vouloir continuer ?'; + + @override + String get raffleWinningTickets => 'Tickets gagnants'; + + @override + String get raffleNoWinningTicketYet => + 'Les tickets gagnants seront affichés ici'; + + @override + String get raffleName => 'Nom'; + + @override + String get raffleDescription => 'Description'; + + @override + String get raffleBuyThisTicket => 'Acheter ce ticket'; + + @override + String get raffleLockedRaffle => 'Tombola verrouillée'; + + @override + String get raffleUnavailableRaffle => 'Tombola indisponible'; + + @override + String get raffleNotEnoughMoney => 'Vous n\'avez pas assez d\'argent'; + + @override + String get raffleWinnable => 'gagnable'; + + @override + String get raffleNoDescription => 'Aucune description'; + + @override + String get raffleAmount => 'Solde'; + + @override + String get raffleLoading => 'Chargement'; + + @override + String get raffleTicketNumber => 'Nombre de ticket'; + + @override + String get rafflePrice => 'Prix'; + + @override + String get raffleEditRaffle => 'Modifier la tombola'; + + @override + String get raffleEdit => 'Modifier'; + + @override + String get raffleAddPackTicket => 'Ajouter un pack de ticket'; + + @override + String get recommendationRecommendation => 'Bons plans'; + + @override + String get recommendationTitle => 'Titre'; + + @override + String get recommendationLogo => 'Logo'; + + @override + String get recommendationCode => 'Code'; + + @override + String get recommendationSummary => 'Court résumé'; + + @override + String get recommendationDescription => 'Description'; + + @override + String get recommendationAdd => 'Ajouter'; + + @override + String get recommendationEdit => 'Modifier'; + + @override + String get recommendationDelete => 'Supprimer'; + + @override + String get recommendationAddImage => 'Veuillez ajouter une image'; + + @override + String get recommendationAddedRecommendation => 'Bon plan ajouté'; + + @override + String get recommendationEditedRecommendation => 'Bon plan modifié'; + + @override + String get recommendationDeleteRecommendationConfirmation => + 'Êtes-vous sûr de vouloir supprimer ce bon plan ?'; + + @override + String get recommendationDeleteRecommendation => 'Suppresion'; + + @override + String get recommendationDeletingRecommendationError => + 'Erreur lors de la suppression'; + + @override + String get recommendationDeletedRecommendation => 'Bon plan supprimé'; + + @override + String get recommendationIncorrectOrMissingFields => + 'Champs incorrects ou manquants'; + + @override + String get recommendationEditingError => 'Échec de la modification'; + + @override + String get recommendationAddingError => 'Échec de l\'ajout'; + + @override + String get recommendationCopiedCode => 'Code de réduction copié'; + + @override + String get seedLibraryAdd => 'Ajouter'; + + @override + String get seedLibraryAddedPlant => 'Plante ajoutée'; + + @override + String get seedLibraryAddedSpecies => 'Espèce ajoutée'; + + @override + String get seedLibraryAddingError => 'Erreur lors de l\'ajout'; + + @override + String get seedLibraryAddPlant => 'Déposer une plante'; + + @override + String get seedLibraryAddSpecies => 'Ajouter une espèce'; + + @override + String get seedLibraryAll => 'Toutes'; + + @override + String get seedLibraryAncestor => 'Ancêtre'; + + @override + String get seedLibraryAround => 'environ'; + + @override + String get seedLibraryAutumn => 'Automne'; + + @override + String get seedLibraryBorrowedPlant => 'Plante empruntée'; + + @override + String get seedLibraryBorrowingDate => 'Date d\'emprunt :'; + + @override + String get seedLibraryBorrowPlant => 'Emprunter la plante'; + + @override + String get seedLibraryCard => 'Carte'; + + @override + String get seedLibraryChoosingAncestor => 'Veuillez choisir un ancêtre'; + + @override + String get seedLibraryChoosingSpecies => 'Veuillez choisir une espèce'; + + @override + String get seedLibraryChoosingSpeciesOrAncestor => + 'Veuillez choisir une espèce ou un ancêtre'; + + @override + String get seedLibraryContact => 'Contact :'; + + @override + String get seedLibraryDays => 'jours'; + + @override + String get seedLibraryDeadMsg => 'Voulez-vous déclarer la plante morte ?'; + + @override + String get seedLibraryDeadPlant => 'Plante morte'; + + @override + String get seedLibraryDeathDate => 'Date de mort'; + + @override + String get seedLibraryDeletedSpecies => 'Espèce supprimée'; + + @override + String get seedLibraryDeleteSpecies => 'Supprimer l\'espèce ?'; + + @override + String get seedLibraryDeleting => 'Suppression'; + + @override + String get seedLibraryDeletingError => 'Erreur lors de la suppression'; + + @override + String get seedLibraryDepositNotAvailable => + 'Le dépôt de plantes n\'est pas possible sans emprunter une plante au préalable'; + + @override + String get seedLibraryDescription => 'Description'; + + @override + String get seedLibraryDifficulty => 'Difficulté :'; + + @override + String get seedLibraryEdit => 'Modifier'; + + @override + String get seedLibraryEditedPlant => 'Plante modifiée'; + + @override + String get seedLibraryEditInformation => 'Modifier les informations'; + + @override + String get seedLibraryEditingError => 'Erreur lors de la modification'; + + @override + String get seedLibraryEditSpecies => 'Modifier l\'espèce'; + + @override + String get seedLibraryEmptyDifficultyError => + 'Veuillez choisir une difficulté'; + + @override + String get seedLibraryEmptyFieldError => 'Veuillez remplir tous les champs'; + + @override + String get seedLibraryEmptyTypeError => 'Veuillez choisir un type de plante'; + + @override + String get seedLibraryEndMonth => 'Mois de fin :'; + + @override + String get seedLibraryFacebookUrl => 'Lien Facebook'; + + @override + String get seedLibraryFilters => 'Filtres'; + + @override + String get seedLibraryForum => + 'Oskour maman j\'ai tué ma plante - Forum d\'aide'; + + @override + String get seedLibraryForumUrl => 'Lien Forum'; + + @override + String get seedLibraryHelpSheets => 'Fiches sur les plantes'; + + @override + String get seedLibraryInformation => 'Informations :'; + + @override + String get seedLibraryMaturationTime => 'Temps de maturation'; + + @override + String get seedLibraryMonthJan => 'Janvier'; + + @override + String get seedLibraryMonthFeb => 'Février'; + + @override + String get seedLibraryMonthMar => 'Mars'; + + @override + String get seedLibraryMonthApr => 'Avril'; + + @override + String get seedLibraryMonthMay => 'Mai'; + + @override + String get seedLibraryMonthJun => 'Juin'; + + @override + String get seedLibraryMonthJul => 'Juillet'; + + @override + String get seedLibraryMonthAug => 'Août'; + + @override + String get seedLibraryMonthSep => 'Septembre'; + + @override + String get seedLibraryMonthOct => 'Octobre'; + + @override + String get seedLibraryMonthNov => 'Novembre'; + + @override + String get seedLibraryMonthDec => 'Décembre'; + + @override + String get seedLibraryMyPlants => 'Mes plantes'; + + @override + String get seedLibraryName => 'Nom'; + + @override + String get seedLibraryNbSeedsRecommended => 'Nombre de graines recommandées'; + + @override + String get seedLibraryNbSeedsRecommendedError => + 'Veuillez entrer un nombre de graines recommandé supérieur à 0'; + + @override + String get seedLibraryNoDateError => 'Veuillez entrer une date'; + + @override + String get seedLibraryNoFilteredPlants => + 'Aucune plante ne correspond à votre recherche. Essayez d\'autres filtres.'; + + @override + String get seedLibraryNoMorePlant => 'Aucune plante n\'est disponible'; + + @override + String get seedLibraryNoPersonalPlants => + 'Vous n\'avez pas encore de plantes dans votre grainothèque. Vous pouvez en ajouter en allant dans les stocks.'; + + @override + String get seedLibraryNoSpecies => 'Aucune espèce trouvée'; + + @override + String get seedLibraryNoStockPlants => + 'Aucune plante disponible dans le stock'; + + @override + String get seedLibraryNotes => 'Notes'; + + @override + String get seedLibraryOk => 'OK'; + + @override + String get seedLibraryPlantationPeriod => 'Période de plantation :'; + + @override + String get seedLibraryPlantationType => 'Type de plantation :'; + + @override + String get seedLibraryPlantDetail => 'Détail de la plante'; + + @override + String get seedLibraryPlantingDate => 'Date de plantation'; + + @override + String get seedLibraryPlantingNow => 'Je la plante maintenant'; + + @override + String get seedLibraryPrefix => 'Préfixe'; + + @override + String get seedLibraryPrefixError => 'Prefixe déjà utilisé'; + + @override + String get seedLibraryPrefixLengthError => + 'Le préfixe doit faire 3 caractères'; + + @override + String get seedLibraryPropagationMethod => 'Méthode de propagation :'; + + @override + String get seedLibraryReference => 'Référence :'; + + @override + String get seedLibraryRemovedPlant => 'Plante supprimée'; + + @override + String get seedLibraryRemovingError => 'Erreur lors de la suppression'; + + @override + String get seedLibraryResearch => 'Recherche'; + + @override + String get seedLibrarySaveChanges => 'Sauvegarder les modifications'; + + @override + String get seedLibrarySeason => 'Saison :'; + + @override + String get seedLibrarySeed => 'Graine'; + + @override + String get seedLibrarySeeds => 'graines'; + + @override + String get seedLibrarySeedDeposit => 'Dépôt de plantes'; + + @override + String get seedLibrarySeedLibrary => 'Grainothèque'; + + @override + String get seedLibrarySeedQuantitySimple => 'Quantité de graines'; + + @override + String get seedLibrarySeedQuantity => 'Quantité de graines :'; + + @override + String get seedLibraryShowDeadPlants => 'Afficher les plantes mortes'; + + @override + String get seedLibrarySpecies => 'Espèce :'; + + @override + String get seedLibrarySpeciesHelp => 'Aide sur l\'espèce'; + + @override + String get seedLibrarySpeciesPlural => 'Espèces'; + + @override + String get seedLibrarySpeciesSimple => 'Espèce'; + + @override + String get seedLibrarySpeciesType => 'Type d\'espèce :'; + + @override + String get seedLibrarySpring => 'Printemps'; + + @override + String get seedLibraryStartMonth => 'Mois de début :'; + + @override + String get seedLibraryStock => 'Stock disponible'; + + @override + String get seedLibrarySummer => 'Été'; + + @override + String get seedLibraryStocks => 'Stocks'; + + @override + String get seedLibraryTimeUntilMaturation => 'Temps avant maturation :'; + + @override + String get seedLibraryType => 'Type :'; + + @override + String get seedLibraryUnableToOpen => 'Impossible d\'ouvrir le lien'; + + @override + String get seedLibraryUpdate => 'Modifier'; + + @override + String get seedLibraryUpdatedInformation => 'Informations modifiées'; + + @override + String get seedLibraryUpdatedSpecies => 'Espèce modifiée'; + + @override + String get seedLibraryUpdatedPlant => 'Plante modifiée'; + + @override + String get seedLibraryUpdatingError => 'Erreur lors de la modification'; + + @override + String get seedLibraryWinter => 'Hiver'; + + @override + String get seedLibraryWriteReference => + 'Veuillez écrire la référence suivante : '; + + @override + String get settingsAccount => 'Compte'; + + @override + String get settingsAddProfilePicture => 'Ajouter une photo'; + + @override + String get settingsAdmin => 'Administrateur'; + + @override + String get settingsAskHelp => 'Demander de l\'aide'; + + @override + String get settingsAssociation => 'Association'; + + @override + String get settingsBirthday => 'Date de naissance'; + + @override + String get settingsBugs => 'Bugs'; + + @override + String get settingsChangePassword => 'Changer de mot de passe'; + + @override + String get settingsChangingPassword => + 'Voulez-vous vraiment changer votre mot de passe ?'; + + @override + String get settingsConfirmPassword => 'Confirmer le mot de passe'; + + @override + String get settingsCopied => 'Copié !'; + + @override + String get settingsDarkMode => 'Mode sombre'; + + @override + String get settingsDarkModeOff => 'Désactivé'; + + @override + String get settingsDeleteLogs => 'Supprimer les logs ?'; + + @override + String get settingsDeleteNotificationLogs => + 'Supprimer les logs des notifications ?'; + + @override + String get settingsDetelePersonalData => 'Supprimer mes données personnelles'; + + @override + String get settingsDetelePersonalDataDesc => + 'Cette action notifie l\'administrateur que vous souhaitez supprimer vos données personnelles.'; + + @override + String get settingsDeleting => 'Suppresion'; + + @override + String get settingsEdit => 'Modifier'; + + @override + String get settingsEditAccount => 'Modifier mon profil'; + + @override + String get settingsEmail => 'Email'; + + @override + String get settingsEmptyField => 'Ce champ ne peut pas être vide'; + + @override + String get settingsErrorProfilePicture => + 'Erreur lors de la modification de la photo de profil'; + + @override + String get settingsErrorSendingDemand => + 'Erreur lors de l\'envoi de la demande'; + + @override + String get settingsEventsIcal => 'Lien Ical des événements'; + + @override + String get settingsExpectingDate => 'Date de naissance attendue'; + + @override + String get settingsFirstname => 'Prénom'; + + @override + String get settingsFloor => 'Étage'; + + @override + String get settingsHelp => 'Aide'; + + @override + String get settingsIcalCopied => 'Lien Ical copié !'; + + @override + String get settingsLanguage => 'Langue'; + + @override + String get settingsLanguageVar => 'Français 🇫🇷'; + + @override + String get settingsLogs => 'Logs'; + + @override + String get settingsModules => 'Modules'; + + @override + String get settingsMyIcs => 'Mon lien Ical'; + + @override + String get settingsName => 'Nom'; + + @override + String get settingsNewPassword => 'Nouveau mot de passe'; + + @override + String get settingsNickname => 'Surnom'; + + @override + String get settingsNotifications => 'Notifications'; + + @override + String get settingsOldPassword => 'Ancien mot de passe'; + + @override + String get settingsPasswordChanged => 'Mot de passe changé'; + + @override + String get settingsPasswordsNotMatch => + 'Les mots de passe ne correspondent pas'; + + @override + String get settingsPersonalData => 'Données personnelles'; + + @override + String get settingsPersonalisation => 'Personnalisation'; + + @override + String get settingsPhone => 'Téléphone'; + + @override + String get settingsProfilePicture => 'Photo de profil'; + + @override + String get settingsPromo => 'Promotion'; + + @override + String get settingsRepportBug => 'Signaler un bug'; + + @override + String get settingsSave => 'Enregistrer'; + + @override + String get settingsSecurity => 'Sécurité'; + + @override + String get settingsSendedDemand => 'Demande envoyée'; + + @override + String get settingsSettings => 'Paramètres'; + + @override + String get settingsTooHeavyProfilePicture => + 'L\'image est trop lourde (max 4Mo)'; + + @override + String get settingsUpdatedProfile => 'Profil modifié'; + + @override + String get settingsUpdatedProfilePicture => 'Photo de profil modifiée'; + + @override + String get settingsUpdateNotification => 'Mettre à jour les notifications'; + + @override + String get settingsUpdatingError => + 'Erreur lors de la modification du profil'; + + @override + String get settingsVersion => 'Version'; + + @override + String get settingsPasswordStrength => 'Force du mot de passe'; + + @override + String get settingsPasswordStrengthVeryWeak => 'Très faible'; + + @override + String get settingsPasswordStrengthWeak => 'Faible'; + + @override + String get settingsPasswordStrengthMedium => 'Moyen'; + + @override + String get settingsPasswordStrengthStrong => 'Fort'; + + @override + String get settingsPasswordStrengthVeryStrong => 'Très fort'; + + @override + String get settingsPhoneNumber => 'Numéro de téléphone'; + + @override + String get settingsValidate => 'Valider'; + + @override + String get settingsEditedAccount => 'Compte modifié avec succès'; + + @override + String get settingsFailedToEditAccount => + 'Échec de la modification du compte'; + + @override + String get settingsChooseLanguage => 'Choix de la langue'; + + @override + String settingsNotificationCounter(int active, int total) { + String _temp0 = intl.Intl.pluralLogic( + active, + locale: localeName, + other: 'activées', + one: 'activée', + zero: 'activée', + ); + return '$active/$total $_temp0'; + } + + @override + String get settingsEvent => 'Événement'; + + @override + String get settingsIcal => 'Lien Ical'; + + @override + String get settingsSynncWithCalendar => 'Synchroniser avec votre calendrier'; + + @override + String get settingsIcalLinkCopied => 'Lien Ical copié dans le presse-papier'; + + @override + String get settingsProfile => 'Profil'; + + @override + String get settingsConnexion => 'Connexion'; + + @override + String get settingsLogOut => 'Se déconnecter'; + + @override + String get settingsLogOutDescription => + 'Êtes-vous sûr de vouloir vous déconnecter ?'; + + @override + String get settingsLogOutSuccess => 'Déconnexion réussie'; + + @override + String get settingsDeleteMyAccount => 'Supprimer mon compte'; + + @override + String get settingsDeleteMyAccountDescription => + 'Cette action notifie l\'administrateur que vous souhaitez supprimer votre compte.'; + + @override + String get settingsDeletionAsked => + 'Demande de suppression de compte envoyée'; + + @override + String get settingsDeleteMyAccountError => + 'Erreur lors de la demande de suppression de compte'; + + @override + String get voteAdd => 'Ajouter'; + + @override + String get voteAddMember => 'Ajouter un membre'; + + @override + String get voteAddedPretendance => 'Liste ajoutée'; + + @override + String get voteAddedSection => 'Section ajoutée'; + + @override + String get voteAddingError => 'Erreur lors de l\'ajout'; + + @override + String get voteAddPretendance => 'Ajouter une liste'; + + @override + String get voteAddSection => 'Ajouter une section'; + + @override + String get voteAll => 'Tous'; + + @override + String get voteAlreadyAddedMember => 'Membre déjà ajouté'; + + @override + String get voteAlreadyVoted => 'Vote enregistré'; + + @override + String get voteChooseList => 'Choisir une liste'; + + @override + String get voteClear => 'Réinitialiser'; + + @override + String get voteClearVotes => 'Réinitialiser les votes'; + + @override + String get voteClosedVote => 'Votes clos'; + + @override + String get voteCloseVote => 'Fermer les votes'; + + @override + String get voteConfirmVote => 'Confirmer le vote'; + + @override + String get voteCountVote => 'Dépouiller les votes'; + + @override + String get voteDelete => 'Supprimer'; + + @override + String get voteDeletedAll => 'Tout supprimé'; + + @override + String get voteDeletedPipo => 'Listes pipos supprimées'; + + @override + String get voteDeletedSection => 'Section supprimée'; + + @override + String get voteDeleteAll => 'Supprimer tout'; + + @override + String get voteDeleteAllDescription => + 'Voulez-vous vraiment supprimer tout ?'; + + @override + String get voteDeletePipo => 'Supprimer les listes pipos'; + + @override + String get voteDeletePipoDescription => + 'Voulez-vous vraiment supprimer les listes pipos ?'; + + @override + String get voteDeletePretendance => 'Supprimer la liste'; + + @override + String get voteDeletePretendanceDesc => + 'Voulez-vous vraiment supprimer cette liste ?'; + + @override + String get voteDeleteSection => 'Supprimer la section'; + + @override + String get voteDeleteSectionDescription => + 'Voulez-vous vraiment supprimer cette section ?'; + + @override + String get voteDeletingError => 'Erreur lors de la suppression'; + + @override + String get voteDescription => 'Description'; + + @override + String get voteEdit => 'Modifier'; + + @override + String get voteEditedPretendance => 'Liste modifiée'; + + @override + String get voteEditedSection => 'Section modifiée'; + + @override + String get voteEditingError => 'Erreur lors de la modification'; + + @override + String get voteErrorClosingVotes => 'Erreur lors de la fermeture des votes'; + + @override + String get voteErrorCountingVotes => 'Erreur lors du dépouillement des votes'; + + @override + String get voteErrorResetingVotes => + 'Erreur lors de la réinitialisation des votes'; + + @override + String get voteErrorOpeningVotes => 'Erreur lors de l\'ouverture des votes'; + + @override + String get voteIncorrectOrMissingFields => 'Champs incorrects ou manquants'; + + @override + String get voteMembers => 'Membres'; + + @override + String get voteName => 'Nom'; + + @override + String get voteNoPretendanceList => 'Aucune liste de prétendance'; + + @override + String get voteNoSection => 'Aucune section'; + + @override + String get voteCanNotVote => 'Vous ne pouvez pas voter'; + + @override + String get voteNoSectionList => 'Aucune section'; + + @override + String get voteNotOpenedVote => 'Vote non ouvert'; + + @override + String get voteOnGoingCount => 'Dépouillement en cours'; + + @override + String get voteOpenVote => 'Ouvrir les votes'; + + @override + String get votePipo => 'Pipo'; + + @override + String get votePretendance => 'Listes'; + + @override + String get votePretendanceDeleted => 'Prétendance supprimée'; + + @override + String get votePretendanceNotDeleted => 'Erreur lors de la suppression'; + + @override + String get voteProgram => 'Programme'; + + @override + String get votePublish => 'Publier'; + + @override + String get votePublishVoteDescription => + 'Voulez-vous vraiment publier les votes ?'; + + @override + String get voteResetedVotes => 'Votes réinitialisés'; + + @override + String get voteResetVote => 'Réinitialiser les votes'; + + @override + String get voteResetVoteDescription => 'Que voulez-vous faire ?'; + + @override + String get voteRole => 'Rôle'; + + @override + String get voteSectionDescription => 'Description de la section'; + + @override + String get voteSection => 'Section'; + + @override + String get voteSectionName => 'Nom de la section'; + + @override + String get voteSeeMore => 'Voir plus'; + + @override + String get voteSelected => 'Sélectionné'; + + @override + String get voteShowVotes => 'Voir les votes'; + + @override + String get voteVote => 'Vote'; + + @override + String get voteVoteError => 'Erreur lors de l\'enregistrement du vote'; + + @override + String get voteVoteFor => 'Voter pour '; + + @override + String get voteVoteNotStarted => 'Vote non ouvert'; + + @override + String get voteVoters => 'Groupes votants'; + + @override + String get voteVoteSuccess => 'Vote enregistré'; + + @override + String get voteVotes => 'Voix'; + + @override + String get voteVotesClosed => 'Votes clos'; + + @override + String get voteVotesCounted => 'Votes dépouillés'; + + @override + String get voteVotesOpened => 'Votes ouverts'; + + @override + String get voteWarning => 'Attention'; + + @override + String get voteWarningMessage => + 'La sélection ne sera pas sauvegardée.\nVoulez-vous continuer ?'; + + @override + String get moduleAdvert => 'Feed'; + + @override + String get moduleAdvertDescription => 'Gérer les feeds'; + + @override + String get moduleAmap => 'AMAP'; + + @override + String get moduleAmapDescription => 'Gérer les livraisons et les produits'; + + @override + String get moduleBooking => 'Réservation'; + + @override + String get moduleBookingDescription => + 'Gérer les réservations, les salles et les managers'; + + @override + String get moduleCalendar => 'Calendrier'; + + @override + String get moduleCalendarDescription => + 'Consulter les événements et les activités'; + + @override + String get moduleCentralisation => 'Centralisation'; + + @override + String get moduleCentralisationDescription => + 'Gérer la centralisation des données'; + + @override + String get moduleCinema => 'Cinéma'; + + @override + String get moduleCinemaDescription => 'Gérer les séances de cinéma'; + + @override + String get moduleEvent => 'Événement'; + + @override + String get moduleEventDescription => + 'Gérer les événements et les participants'; + + @override + String get moduleFlappyBird => 'Flappy Bird'; + + @override + String get moduleFlappyBirdDescription => + 'Jouer à Flappy Bird et consulter le classement'; + + @override + String get moduleLoan => 'Prêt'; + + @override + String get moduleLoanDescription => 'Gérer les prêts et les articles'; + + @override + String get modulePhonebook => 'Annuaire'; + + @override + String get modulePhonebookDescription => + 'Gérer les associations, les membres et les administrateurs'; + + @override + String get modulePurchases => 'Achats'; + + @override + String get modulePurchasesDescription => + 'Gérer les achats, les tickets et l\'historique'; + + @override + String get moduleRaffle => 'Tombola'; + + @override + String get moduleRaffleDescription => + 'Gérer les tombolas, les prix et les tickets'; + + @override + String get moduleRecommendation => 'Bons plans'; + + @override + String get moduleRecommendationDescription => + 'Gérer les recommandations, les informations et les administrateurs'; + + @override + String get moduleSeedLibrary => 'Grainothèque'; + + @override + String get moduleSeedLibraryDescription => + 'Gérer les graines, les espèces et les stocks'; + + @override + String get moduleVote => 'Vote'; + + @override + String get moduleVoteDescription => + 'Gérer les votes, les sections et les candidats'; + + @override + String get modulePh => 'PH'; + + @override + String get modulePhDescription => + 'Gérer les PH, les formulaires et les administrateurs'; + + @override + String get moduleSettings => 'Paramètres'; + + @override + String get moduleSettingsDescription => + 'Gérer les paramètres de l\'application'; + + @override + String get moduleFeed => 'Events'; + + @override + String get moduleFeedDescription => 'Consulter les événements'; + + @override + String get moduleStyleGuide => 'StyleGuide'; + + @override + String get moduleStyleGuideDescription => + 'Explore the UI components and styles used in Titan'; + + @override + String get moduleAdmin => 'Admin'; + + @override + String get moduleAdminDescription => + 'Gérer les utilisateurs, groupes et structures'; + + @override + String get moduleOthers => 'Autres'; + + @override + String get moduleOthersDescription => 'Afficher les autres modules'; + + @override + String get modulePayment => 'Paiement'; + + @override + String get modulePaymentDescription => + 'Gérer les paiements, les statistiques et les appareils'; + + @override + String get toolInvalidNumber => 'Chiffre invalide'; + + @override + String get toolDateRequired => 'Date requise'; + + @override + String get toolSuccess => 'Succès'; +} diff --git a/lib/loan/providers/end_provider.dart b/lib/loan/providers/end_provider.dart index d418232b26..c63d378746 100644 --- a/lib/loan/providers/end_provider.dart +++ b/lib/loan/providers/end_provider.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:titan/loan/class/item.dart'; import 'package:titan/tools/functions.dart'; @@ -11,9 +12,9 @@ class EndNotifier extends StateNotifier { state = end; } - void setEndFromSelected(String start, List selected) { - state = processDate( - DateTime.parse(processDateBack(start)).add( + void setEndFromSelected(String start, List selected, String locale) { + state = DateFormat.yMd(locale).format( + DateTime.parse(processDateBack(start, locale)).add( Duration( days: selected .map((item) => item.suggestedLendingDuration) diff --git a/lib/loan/router.dart b/lib/loan/router.dart index 3b1ab07940..f09fd99378 100644 --- a/lib/loan/router.dart +++ b/lib/loan/router.dart @@ -1,7 +1,7 @@ -import 'package:either_dart/either.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:titan/drawer/class/module.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/class/module.dart'; import 'package:titan/loan/providers/is_loan_admin_provider.dart'; import 'package:titan/loan/ui/pages/admin_page/admin_page.dart' deferred as admin_page; @@ -26,10 +26,10 @@ class LoanRouter { static const String addEditItem = '/add_edit_item'; static const String detail = '/detail'; static final Module module = Module( - name: "Prêt", - icon: const Left(HeroIcons.buildingLibrary), + getName: (context) => AppLocalizations.of(context)!.moduleLoan, + getDescription: (context) => + AppLocalizations.of(context)!.moduleLoanDescription, root: LoanRouter.root, - selected: false, ); LoanRouter(this.ref); @@ -41,6 +41,10 @@ class LoanRouter { AuthenticatedMiddleware(ref), DeferredLoadingMiddleware(main_page.loadLibrary), ], + pageType: QCustomPage( + transitionsBuilder: (_, animation, _, child) => + FadeTransition(opacity: animation, child: child), + ), children: [ QRoute( path: admin, diff --git a/lib/loan/tools/constants.dart b/lib/loan/tools/constants.dart index e962e9a589..b4e6a9c3b9 100644 --- a/lib/loan/tools/constants.dart +++ b/lib/loan/tools/constants.dart @@ -6,94 +6,3 @@ class LoanColorConstants { static const Color redGradient2 = Color.fromARGB(255, 172, 32, 10); static const Color urgentRed = Color.fromARGB(255, 99, 13, 0); } - -class LoanTextConstants { - static const String add = "Ajouter"; - static const String addLoan = "Ajouter un prêt"; - static const String addObject = "Ajouter un objet"; - static const String addedLoan = "Prêt ajouté"; - static const String addedObject = "Objet ajouté"; - static const String addedRoom = "Salle ajoutée"; - static const String addingError = "Erreur lors de l'ajout"; - static const String admin = "Administrateur"; - static const String available = "Disponible"; - static const String availableMultiple = "Disponibles"; - static const String borrowed = "Emprunté"; - static const String borrowedMultiple = "Empruntés"; - static const String and = "et"; - static const String association = "Association"; - static const String availableItems = "Objets disponibles"; - static const String beginDate = "Date du début du prêt"; - static const String borrower = "Emprunteur"; - static const String caution = "Caution"; - static const String cancel = "Annuler"; - static const String confirm = "Confirmer"; - static const String confirmation = "Confirmation"; - static const String dates = "Dates"; - static const String days = "Jours"; - static const String delay = "Délai de la prolongation"; - static const String delete = "Supprimer"; - static const String deletingLoan = "Supprimer le prêt ?"; - static const String deletedItem = "Objet supprimé"; - static const String deletedLoan = "Prêt supprimé"; - static const String deleting = "Suppression"; - static const String deletingError = "Erreur lors de la suppression"; - static const String deletingItem = "Supprimer l'objet ?"; - static const String duration = "Durée"; - static const String edit = "Modifier"; - static const String editItem = "Modifier l'objet"; - static const String editLoan = "Modifier le prêt"; - static const String editedRoom = "Salle modifiée"; - static const String endDate = "Date de fin du prêt"; - static const String ended = "Terminé"; - static const String enterDate = "Veuillez entrer une date"; - static const String extendedLoan = "Prêt prolongé"; - static const String extendingError = "Erreur lors de la prolongation"; - static const String history = "Historique"; - static const String incorrectOrMissingFields = - "Des champs sont manquants ou incorrects"; - static const String invalidNumber = "Veuillez entrer un nombre"; - static const String invalidDates = "Les dates ne sont pas valides"; - static const String item = "Objet"; - static const String items = "Objets"; - static const String itemHandling = "Gestion des objets"; - static const String itemSelected = "objet sélectionné"; - static const String itemsSelected = "objets sélectionnés"; - static const String lendingDuration = "Durée possible du prêt"; - static const String loan = "Prêt"; - static const String loanHandling = "Gestion des prêts"; - static const String looking = "Rechercher"; - static const String name = "Nom"; - static const String next = "Suivant"; - static const String no = "Non"; - static const String noAssociationsFounded = "Aucune association trouvée"; - static const String noAvailableItems = "Aucun objet disponible"; - static const String noBorrower = "Aucun emprunteur"; - static const String noItems = "Aucun objet"; - static const String noItemSelected = "Aucun objet sélectionné"; - static const String noLoan = "Aucun prêt"; - static const String noReturnedDate = "Pas de date de retour"; - static const String quantity = "Quantité"; - static const String none = "Aucun"; - static const String note = "Note"; - static const String noValue = "Veuillez entrer une valeur"; - static const String onGoing = "En cours"; - static const String onGoingLoan = "Prêt en cours"; - static const String others = "autres"; - static const String paidCaution = "Caution payée"; - static const String positiveNumber = "Veuillez entrer un nombre positif"; - static const String previous = "Précédent"; - static const String returned = "Rendu"; - static const String returnedLoan = "Prêt rendu"; - static const String returningError = "Erreur lors du retour"; - static const String returningLoan = "Retour"; - static const String returnLoan = "Rendre le prêt ?"; - static const String returnLoanDescription = "Voulez-vous rendre ce prêt ?"; - static const String toReturn = "A rendre"; - static const String unavailable = "Indisponible"; - static const String update = "Modifier"; - static const String updatedItem = "Objet modifié"; - static const String updatedLoan = "Prêt modifié"; - static const String updatingError = "Erreur lors de la modification"; - static const String yes = "Oui"; -} diff --git a/lib/loan/tools/functions.dart b/lib/loan/tools/functions.dart index 6c51b476d5..e814f4b116 100644 --- a/lib/loan/tools/functions.dart +++ b/lib/loan/tools/functions.dart @@ -1,13 +1,14 @@ +import 'package:flutter/material.dart'; import 'package:titan/loan/class/item_quantity.dart'; -import 'package:titan/loan/tools/constants.dart'; +import 'package:titan/l10n/app_localizations.dart'; -String formatItems(List itemsQty) { +String formatItems(List itemsQty, BuildContext context) { if (itemsQty.length == 2) { - return "${itemsQty[0].quantity} ${itemsQty[0].itemSimple.name} ${LoanTextConstants.and} ${itemsQty[1].quantity} ${itemsQty[1].itemSimple.name}"; + return "${itemsQty[0].quantity} ${itemsQty[0].itemSimple.name} ${AppLocalizations.of(context)!.loanAnd} ${itemsQty[1].quantity} ${itemsQty[1].itemSimple.name}"; } else if (itemsQty.length == 3) { - return "${itemsQty[0].quantity} ${itemsQty[0].itemSimple.name}, ${itemsQty[1].quantity} ${itemsQty[1].itemSimple.name} ${LoanTextConstants.and} ${itemsQty[2].quantity} ${itemsQty[2].itemSimple.name}"; + return "${itemsQty[0].quantity} ${itemsQty[0].itemSimple.name}, ${itemsQty[1].quantity} ${itemsQty[1].itemSimple.name} ${AppLocalizations.of(context)!.loanAnd} ${itemsQty[2].quantity} ${itemsQty[2].itemSimple.name}"; } else if (itemsQty.length > 3) { - return "${itemsQty[0].quantity} ${itemsQty[0].itemSimple.name}, ${itemsQty[1].quantity} ${itemsQty[1].itemSimple.name} ${LoanTextConstants.and} ${itemsQty.length - 2} ${LoanTextConstants.others}"; + return "${itemsQty[0].quantity} ${itemsQty[0].itemSimple.name}, ${itemsQty[1].quantity} ${itemsQty[1].itemSimple.name} ${AppLocalizations.of(context)!.loanAnd} ${itemsQty.length - 2} ${AppLocalizations.of(context)!.loanOthers}"; } else if (itemsQty.length == 1) { return "${itemsQty[0].quantity} ${itemsQty[0].itemSimple.name}"; } else { @@ -15,12 +16,12 @@ String formatItems(List itemsQty) { } } -String formatNumberItems(int n) { +String formatNumberItems(int n, BuildContext context) { if (n >= 2) { - return "$n ${LoanTextConstants.itemsSelected}"; + return "$n ${AppLocalizations.of(context)!.loanItemsSelected}"; } else if (n == 1) { - return "$n ${LoanTextConstants.itemSelected} "; + return "$n ${AppLocalizations.of(context)!.loanItemSelected} "; } else { - return LoanTextConstants.noItemSelected; + return AppLocalizations.of(context)!.loanNoItemSelected; } } diff --git a/lib/loan/ui/loan.dart b/lib/loan/ui/loan.dart index ba6684d3bf..03392d07f8 100644 --- a/lib/loan/ui/loan.dart +++ b/lib/loan/ui/loan.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:qlevar_router/qlevar_router.dart'; import 'package:titan/loan/providers/item_list_provider.dart'; import 'package:titan/loan/providers/loaner_provider.dart'; import 'package:titan/loan/providers/loaners_items_provider.dart'; import 'package:titan/loan/router.dart'; -import 'package:titan/loan/tools/constants.dart'; +import 'package:titan/tools/constants.dart'; import 'package:titan/tools/ui/widgets/top_bar.dart'; -import 'package:qlevar_router/qlevar_router.dart'; class LoanTemplate extends HookConsumerWidget { final Widget child; @@ -14,26 +14,30 @@ class LoanTemplate extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return SafeArea( - child: Column( - children: [ - TopBar( - title: LoanTextConstants.loan, - root: LoanRouter.root, - onBack: () { - if (QR.currentPath == - LoanRouter.root + LoanRouter.admin + LoanRouter.addEditLoan) { - final loanersItemsNotifier = ref.watch( - loanersItemsProvider.notifier, - ); - final loaner = ref.watch(loanerProvider); - final itemList = ref.watch(itemListProvider); - loanersItemsNotifier.setTData(loaner, itemList); - } - }, - ), - Expanded(child: child), - ], + return Container( + color: ColorConstants.background, + child: SafeArea( + child: Column( + children: [ + TopBar( + root: LoanRouter.root, + onBack: () { + if (QR.currentPath == + LoanRouter.root + + LoanRouter.admin + + LoanRouter.addEditLoan) { + final loanersItemsNotifier = ref.watch( + loanersItemsProvider.notifier, + ); + final loaner = ref.watch(loanerProvider); + final itemList = ref.watch(itemListProvider); + loanersItemsNotifier.setTData(loaner, itemList); + } + }, + ), + Expanded(child: child), + ], + ), ), ); } diff --git a/lib/loan/ui/pages/admin_page/admin_page.dart b/lib/loan/ui/pages/admin_page/admin_page.dart index 4dd4a31a9b..d0f468a7e0 100644 --- a/lib/loan/ui/pages/admin_page/admin_page.dart +++ b/lib/loan/ui/pages/admin_page/admin_page.dart @@ -41,6 +41,7 @@ class AdminPage extends HookConsumerWidget { return LoanTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { final itemListNotifier = ref.read(itemListProvider.notifier); final loanersItemsNotifier = ref.read(loanersItemsProvider.notifier); diff --git a/lib/loan/ui/pages/admin_page/delay_dialog.dart b/lib/loan/ui/pages/admin_page/delay_dialog.dart index bcc37de33c..3d54421d6a 100644 --- a/lib/loan/ui/pages/admin_page/delay_dialog.dart +++ b/lib/loan/ui/pages/admin_page/delay_dialog.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:titan/loan/tools/constants.dart'; import 'package:numberpicker/numberpicker.dart'; +import 'package:titan/l10n/app_localizations.dart'; class DelayDialog extends StatefulWidget { final void Function(int i) onYes; @@ -43,9 +43,9 @@ class IntegerExampleState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Text( - LoanTextConstants.delay, - style: TextStyle( + Text( + AppLocalizations.of(context)!.loanDelay, + style: const TextStyle( fontSize: 25, fontWeight: FontWeight.w800, color: Colors.black, @@ -70,9 +70,9 @@ class IntegerExampleState extends State { onPressed: () { Navigator.of(context).pop(); }, - child: const Text( - LoanTextConstants.cancel, - style: TextStyle( + child: Text( + AppLocalizations.of(context)!.loanCancel, + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: Colors.black, @@ -84,9 +84,9 @@ class IntegerExampleState extends State { Navigator.of(context).pop(); widget.onYes(_currentIntValue); }, - child: const Text( - LoanTextConstants.confirm, - style: TextStyle( + child: Text( + AppLocalizations.of(context)!.loanConfirm, + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: Colors.black, diff --git a/lib/loan/ui/pages/admin_page/item_card.dart b/lib/loan/ui/pages/admin_page/item_card.dart index b2ad78f726..186672092a 100644 --- a/lib/loan/ui/pages/admin_page/item_card.dart +++ b/lib/loan/ui/pages/admin_page/item_card.dart @@ -6,6 +6,7 @@ import 'package:titan/loan/tools/constants.dart'; import 'package:titan/tools/ui/layouts/card_button.dart'; import 'package:titan/tools/ui/layouts/card_layout.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; +import 'package:titan/l10n/app_localizations.dart'; class ItemCard extends StatelessWidget { final Item item; @@ -46,8 +47,8 @@ class ItemCard extends StatelessWidget { const SizedBox(height: 5), Text( availableQuantity > 0 - ? '$availableQuantity ${availableQuantity <= 1 ? LoanTextConstants.available : LoanTextConstants.availableMultiple}' - : LoanTextConstants.unavailable, + ? '$availableQuantity ${availableQuantity <= 1 ? AppLocalizations.of(context)!.loanAvailable : AppLocalizations.of(context)!.loanAvailableMultiple}' + : AppLocalizations.of(context)!.loanUnavailable, style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, diff --git a/lib/loan/ui/pages/admin_page/loan_card.dart b/lib/loan/ui/pages/admin_page/loan_card.dart index 1046fda50a..00875760d8 100644 --- a/lib/loan/ui/pages/admin_page/loan_card.dart +++ b/lib/loan/ui/pages/admin_page/loan_card.dart @@ -1,6 +1,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; +import 'package:intl/intl.dart'; import 'package:titan/loan/class/loan.dart'; import 'package:titan/loan/tools/constants.dart'; import 'package:titan/loan/tools/functions.dart'; @@ -8,6 +9,7 @@ import 'package:titan/tools/functions.dart'; import 'package:titan/tools/ui/layouts/card_button.dart'; import 'package:titan/tools/ui/layouts/card_layout.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; +import 'package:titan/l10n/app_localizations.dart'; class LoanCard extends StatelessWidget { final Loan loan; @@ -28,6 +30,7 @@ class LoanCard extends StatelessWidget { @override Widget build(BuildContext context) { + final locale = Localizations.localeOf(context); final shouldReturn = DateTime.now().compareTo(loan.end) > 0 && !loan.returned; return GestureDetector( @@ -101,7 +104,7 @@ class LoanCard extends StatelessWidget { ), const SizedBox(height: 7), Text( - formatItems(loan.itemsQuantity), + formatItems(loan.itemsQuantity, context), style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, @@ -125,10 +128,10 @@ class LoanCard extends StatelessWidget { children: [ Text( loan.returned - ? LoanTextConstants.returned + ? AppLocalizations.of(context)!.loanReturned : shouldReturn - ? LoanTextConstants.toReturn - : LoanTextConstants.onGoing, + ? AppLocalizations.of(context)!.loanToReturn + : AppLocalizations.of(context)!.loanOnGoing, style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, @@ -140,9 +143,9 @@ class LoanCard extends StatelessWidget { Text( (loan.returned) ? loan.returnedDate != null - ? processDate(loan.returnedDate!) - : LoanTextConstants.noReturnedDate - : processDate(loan.end), + ? DateFormat.yMd(locale).format(loan.returnedDate!) + : AppLocalizations.of(context)!.loanNoReturnedDate + : DateFormat.yMd(locale).format(loan.end), style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, diff --git a/lib/loan/ui/pages/admin_page/loan_history.dart b/lib/loan/ui/pages/admin_page/loan_history.dart index 944612165d..b2896fc99c 100644 --- a/lib/loan/ui/pages/admin_page/loan_history.dart +++ b/lib/loan/ui/pages/admin_page/loan_history.dart @@ -7,13 +7,13 @@ import 'package:titan/loan/providers/loan_focus_provider.dart'; import 'package:titan/loan/providers/loan_provider.dart'; import 'package:titan/loan/providers/loaner_provider.dart'; import 'package:titan/loan/router.dart'; -import 'package:titan/loan/tools/constants.dart'; import 'package:titan/loan/ui/pages/admin_page/loan_card.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; import 'package:titan/tools/ui/widgets/loader.dart'; import 'package:titan/tools/ui/widgets/styled_search_bar.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class HistoryLoan extends HookConsumerWidget { const HistoryLoan({super.key}); @@ -49,7 +49,7 @@ class HistoryLoan extends HookConsumerWidget { return Column( children: [ StyledSearchBar( - label: LoanTextConstants.history, + label: AppLocalizations.of(context)!.loanHistory, onChanged: (value) async { if (value.isNotEmpty) { adminHistoryLoanListNotifier.setTData( diff --git a/lib/loan/ui/pages/admin_page/loaners_items.dart b/lib/loan/ui/pages/admin_page/loaners_items.dart index 54beb7dc43..62700bc9ec 100644 --- a/lib/loan/ui/pages/admin_page/loaners_items.dart +++ b/lib/loan/ui/pages/admin_page/loaners_items.dart @@ -9,7 +9,6 @@ import 'package:titan/loan/providers/item_provider.dart'; import 'package:titan/loan/providers/loaner_provider.dart'; import 'package:titan/loan/providers/loaners_items_provider.dart'; import 'package:titan/loan/router.dart'; -import 'package:titan/loan/tools/constants.dart'; import 'package:titan/loan/ui/pages/admin_page/item_card.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/layouts/card_layout.dart'; @@ -19,6 +18,7 @@ import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; import 'package:titan/tools/ui/widgets/styled_search_bar.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class LoanersItems extends HookConsumerWidget { const LoanersItems({super.key}); @@ -43,7 +43,7 @@ class LoanersItems extends HookConsumerWidget { final item = loanersItems[loaner]; if (item == null) { - return const Center(child: Text(LoanTextConstants.noItems)); + return Center(child: Text(AppLocalizations.of(context)!.loanNoItems)); } return AsyncChild( value: item, @@ -54,7 +54,7 @@ class LoanersItems extends HookConsumerWidget { return Column( children: [ StyledSearchBar( - label: LoanTextConstants.itemHandling, + label: AppLocalizations.of(context)!.loanItemHandling, onChanged: (value) async { if (value.isNotEmpty) { loanersItemsNotifier.setTData( @@ -97,8 +97,16 @@ class LoanersItems extends HookConsumerWidget { context: context, builder: (BuildContext context) { return CustomDialogBox( - descriptions: LoanTextConstants.deletingItem, + descriptions: AppLocalizations.of( + context, + )!.loanDeletingItem, onYes: () { + final deletedItemMsg = AppLocalizations.of( + context, + )!.loanDeletedItem; + final deletingErrorMsg = AppLocalizations.of( + context, + )!.loanDeletingError; tokenExpireWrapper(ref, () async { final value = await itemListNotifier.deleteItem( e, @@ -110,17 +118,17 @@ class LoanersItems extends HookConsumerWidget { }); displayToastWithContext( TypeMsg.msg, - LoanTextConstants.deletedItem, + deletedItemMsg, ); } else { displayToastWithContext( TypeMsg.error, - LoanTextConstants.deletingError, + deletingErrorMsg, ); } }); }, - title: LoanTextConstants.delete, + title: AppLocalizations.of(context)!.loanDelete, ); }, ); diff --git a/lib/loan/ui/pages/admin_page/on_going_loan.dart b/lib/loan/ui/pages/admin_page/on_going_loan.dart index 3da8eef0ad..d70cba3628 100644 --- a/lib/loan/ui/pages/admin_page/on_going_loan.dart +++ b/lib/loan/ui/pages/admin_page/on_going_loan.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:titan/loan/class/item.dart'; import 'package:titan/loan/class/loan.dart'; import 'package:titan/loan/providers/admin_loan_list_provider.dart'; @@ -14,7 +15,6 @@ import 'package:titan/loan/providers/loaner_provider.dart'; import 'package:titan/loan/providers/loaners_items_provider.dart'; import 'package:titan/loan/providers/start_provider.dart'; import 'package:titan/loan/router.dart'; -import 'package:titan/loan/tools/constants.dart'; import 'package:titan/loan/ui/pages/admin_page/loan_card.dart'; import 'package:titan/loan/ui/pages/admin_page/delay_dialog.dart'; import 'package:titan/tools/functions.dart'; @@ -26,12 +26,14 @@ import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; import 'package:titan/tools/ui/widgets/loader.dart'; import 'package:titan/tools/ui/widgets/styled_search_bar.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class OnGoingLoan extends HookConsumerWidget { const OnGoingLoan({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final locale = Localizations.localeOf(context); final loaner = ref.watch(loanerProvider); final loanListNotifier = ref.watch(loanerLoanListProvider.notifier); final loanList = ref.watch(loanerLoanListProvider); @@ -66,7 +68,7 @@ class OnGoingLoan extends HookConsumerWidget { children: [ StyledSearchBar( label: - '${data.isEmpty ? LoanTextConstants.none : data.length} ${LoanTextConstants.loan.toLowerCase()}${data.length > 1 ? 's' : ''} ${LoanTextConstants.onGoing.toLowerCase()}', + '${data.isEmpty ? AppLocalizations.of(context)!.loanNone : data.length} ${AppLocalizations.of(context)!.loanLoan.toLowerCase()}${data.length > 1 ? 's' : ''} ${AppLocalizations.of(context)!.loanOnGoing.toLowerCase()}', onChanged: (value) async { if (value.isNotEmpty) { adminLoanListNotifier.setTData( @@ -84,7 +86,9 @@ class OnGoingLoan extends HookConsumerWidget { firstChild: GestureDetector( onTap: () async { await loanNotifier.setLoan(Loan.empty()); - startNotifier.setStart(processDate(DateTime.now())); + startNotifier.setStart( + DateFormat.yMd(locale).format(DateTime.now()), + ); endNotifier.setEnd(""); QR.to( LoanRouter.root + LoanRouter.admin + LoanRouter.addEditLoan, @@ -109,14 +113,22 @@ class OnGoingLoan extends HookConsumerWidget { isAdmin: true, onEdit: () async { await loanNotifier.setLoan(e); - startNotifier.setStart(processDate(e.start)); - endNotifier.setEnd(processDate(e.end)); + startNotifier.setStart( + DateFormat.yMd(locale).format(e.start), + ); + endNotifier.setEnd(DateFormat.yMd(locale).format(e.end)); QR.to( LoanRouter.root + LoanRouter.admin + LoanRouter.addEditLoan, ); loanersItemsNotifier.setTData(loaner, itemList); }, onCalendar: () async { + final extendedLoanMsg = AppLocalizations.of( + context, + )!.loanExtendedLoan; + final extendedLoanErrorMsg = AppLocalizations.of( + context, + )!.loanExtendingError; await showDialog( context: context, builder: (BuildContext context) { @@ -138,12 +150,12 @@ class OnGoingLoan extends HookConsumerWidget { ); displayToastWithContext( TypeMsg.msg, - LoanTextConstants.extendedLoan, + extendedLoanMsg, ); } else { displayToastWithContext( TypeMsg.error, - LoanTextConstants.extendingError, + extendedLoanErrorMsg, ); } }); @@ -156,9 +168,17 @@ class OnGoingLoan extends HookConsumerWidget { await showDialog( context: context, builder: (context) => CustomDialogBox( - title: LoanTextConstants.returnLoan, - descriptions: LoanTextConstants.returnLoanDescription, + title: AppLocalizations.of(context)!.loanReturnLoan, + descriptions: AppLocalizations.of( + context, + )!.loanReturnLoanDescription, onYes: () async { + final returningLoanMsg = AppLocalizations.of( + context, + )!.loanReturnedLoan; + final returningLoanErrorMsg = AppLocalizations.of( + context, + )!.loanReturningError; await tokenExpireWrapper(ref, () async { final loanItemsId = e.itemsQuantity .map((e) => e.itemSimple.id) @@ -188,12 +208,12 @@ class OnGoingLoan extends HookConsumerWidget { ); displayToastWithContext( TypeMsg.msg, - LoanTextConstants.returnedLoan, + returningLoanMsg, ); } else { displayToastWithContext( TypeMsg.msg, - LoanTextConstants.returningError, + returningLoanErrorMsg, ); } }); diff --git a/lib/loan/ui/pages/detail_pages/item_card_in_loan.dart b/lib/loan/ui/pages/detail_pages/item_card_in_loan.dart index e303cfef6f..52991f0396 100644 --- a/lib/loan/ui/pages/detail_pages/item_card_in_loan.dart +++ b/lib/loan/ui/pages/detail_pages/item_card_in_loan.dart @@ -1,8 +1,8 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:titan/loan/class/item_quantity.dart'; -import 'package:titan/loan/tools/constants.dart'; import 'package:titan/tools/ui/layouts/card_layout.dart'; +import 'package:titan/l10n/app_localizations.dart'; class ItemCardInLoan extends StatelessWidget { final ItemQuantity itemQty; @@ -32,7 +32,7 @@ class ItemCardInLoan extends StatelessWidget { ), const SizedBox(height: 10), Text( - '${itemQty.quantity} ${itemQty.quantity <= 1 ? LoanTextConstants.borrowed : LoanTextConstants.borrowedMultiple}', + '${itemQty.quantity} ${itemQty.quantity <= 1 ? AppLocalizations.of(context)!.loanBorrowed : AppLocalizations.of(context)!.loanBorrowedMultiple}', style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, diff --git a/lib/loan/ui/pages/item_group_page/add_edit_item_page.dart b/lib/loan/ui/pages/item_group_page/add_edit_item_page.dart index 3ee3b53931..fd98f9048e 100644 --- a/lib/loan/ui/pages/item_group_page/add_edit_item_page.dart +++ b/lib/loan/ui/pages/item_group_page/add_edit_item_page.dart @@ -6,7 +6,6 @@ import 'package:titan/loan/providers/item_list_provider.dart'; import 'package:titan/loan/providers/item_provider.dart'; import 'package:titan/loan/providers/loaner_provider.dart'; import 'package:titan/loan/providers/loaners_items_provider.dart'; -import 'package:titan/loan/tools/constants.dart'; import 'package:titan/loan/ui/loan.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; @@ -15,6 +14,7 @@ import 'package:titan/tools/ui/widgets/align_left_text.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; import 'package:titan/tools/ui/widgets/text_entry.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AddEditItemPage extends HookConsumerWidget { const AddEditItemPage({super.key}); @@ -52,8 +52,8 @@ class AddEditItemPage extends HookConsumerWidget { const SizedBox(height: 30), AlignLeftText( isEdit - ? LoanTextConstants.editItem - : LoanTextConstants.addObject, + ? AppLocalizations.of(context)!.loanEditItem + : AppLocalizations.of(context)!.loanAddObject, padding: const EdgeInsets.symmetric(horizontal: 30), color: Colors.grey, ), @@ -62,11 +62,14 @@ class AddEditItemPage extends HookConsumerWidget { child: Column( children: [ const SizedBox(height: 30), - TextEntry(label: LoanTextConstants.name, controller: name), + TextEntry( + label: AppLocalizations.of(context)!.loanName, + controller: name, + ), const SizedBox(height: 30), TextEntry( keyboardType: TextInputType.number, - label: LoanTextConstants.quantity, + label: AppLocalizations.of(context)!.loanQuantity, isInt: true, controller: quantity, ), @@ -75,7 +78,7 @@ class AddEditItemPage extends HookConsumerWidget { keyboardType: TextInputType.number, controller: caution, isInt: true, - label: LoanTextConstants.caution, + label: AppLocalizations.of(context)!.loanCaution, suffix: '€', ), const SizedBox(height: 30), @@ -83,8 +86,8 @@ class AddEditItemPage extends HookConsumerWidget { keyboardType: TextInputType.number, controller: lendingDuration, isInt: true, - label: LoanTextConstants.lendingDuration, - suffix: LoanTextConstants.days, + label: AppLocalizations.of(context)!.loanLendingDuration, + suffix: AppLocalizations.of(context)!.loanDays, ), const SizedBox(height: 50), WaitingButton( @@ -93,6 +96,12 @@ class AddEditItemPage extends HookConsumerWidget { child: child, ), onTap: () async { + final updatedItemMsg = isEdit + ? AppLocalizations.of(context)!.loanUpdatedItem + : AppLocalizations.of(context)!.loanAddedObject; + final updatedItemErrorMsg = isEdit + ? AppLocalizations.of(context)!.loanUpdatingError + : AppLocalizations.of(context)!.loanAddingError; if (key.currentState == null) { return; } @@ -123,41 +132,31 @@ class AddEditItemPage extends HookConsumerWidget { loaner, await itemListNotifier.copy(), ); - if (isEdit) { - displayToastWithContext( - TypeMsg.msg, - LoanTextConstants.updatedItem, - ); - } else { - displayToastWithContext( - TypeMsg.msg, - LoanTextConstants.addedObject, - ); - } + displayToastWithContext( + TypeMsg.msg, + updatedItemMsg, + ); } else { - if (isEdit) { - displayToastWithContext( - TypeMsg.error, - LoanTextConstants.updatingError, - ); - } else { - displayToastWithContext( - TypeMsg.error, - LoanTextConstants.addingError, - ); - } + displayToastWithContext( + TypeMsg.error, + updatedItemErrorMsg, + ); } }); } else { displayToast( context, TypeMsg.error, - LoanTextConstants.incorrectOrMissingFields, + AppLocalizations.of( + context, + )!.loanIncorrectOrMissingFields, ); } }, child: Text( - isEdit ? LoanTextConstants.edit : LoanTextConstants.add, + isEdit + ? AppLocalizations.of(context)!.loanEdit + : AppLocalizations.of(context)!.loanAdd, style: const TextStyle( color: Colors.white, fontSize: 25, diff --git a/lib/loan/ui/pages/loan_group_page/add_edit_button.dart b/lib/loan/ui/pages/loan_group_page/add_edit_button.dart index 09137c3fbc..0ceee02bcd 100644 --- a/lib/loan/ui/pages/loan_group_page/add_edit_button.dart +++ b/lib/loan/ui/pages/loan_group_page/add_edit_button.dart @@ -12,12 +12,12 @@ import 'package:titan/loan/providers/loaner_loan_list_provider.dart'; import 'package:titan/loan/providers/loaner_provider.dart'; import 'package:titan/loan/providers/selected_items_provider.dart'; import 'package:titan/loan/providers/start_provider.dart'; -import 'package:titan/loan/tools/constants.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/tools/ui/layouts/add_edit_button_layout.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AddEditButton extends HookConsumerWidget { final TextEditingController note; @@ -32,6 +32,7 @@ class AddEditButton extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final locale = Localizations.localeOf(context); final adminLoanListNotifier = ref.watch(adminLoanListProvider.notifier); final items = ref.watch(itemListProvider); final selectedItems = ref.watch(editSelectedListProvider); @@ -51,14 +52,22 @@ class AddEditButton extends HookConsumerWidget { builder: (child) => AddEditButtonLayout(child: child), onTap: () async { await onAddEdit(() async { - if (processDateBack(start).compareTo(processDateBack(end)) > 0) { + if (processDateBack( + start, + locale.toString(), + ).compareTo(processDateBack(end, locale.toString())) > + 0) { displayToast( context, TypeMsg.error, - LoanTextConstants.invalidDates, + AppLocalizations.of(context)!.loanInvalidDates, ); } else if (borrower.id.isEmpty) { - displayToast(context, TypeMsg.error, LoanTextConstants.noBorrower); + displayToast( + context, + TypeMsg.error, + AppLocalizations.of(context)!.loanNoBorrower, + ); } else { await items.when( data: (itemList) async { @@ -81,12 +90,22 @@ class AddEditButton extends HookConsumerWidget { itemsQuantity: selected, borrower: borrower, caution: caution.text, - end: DateTime.parse(processDateBack(end)), + end: DateTime.parse( + processDateBack(end, locale.toString()), + ), id: isEdit ? loan.id : "", notes: note.text, - start: DateTime.parse(processDateBack(start)), + start: DateTime.parse( + processDateBack(start, locale.toString()), + ), returned: false, ); + final addedLoanMsg = isEdit + ? AppLocalizations.of(context)!.loanUpdatedLoan + : AppLocalizations.of(context)!.loanAddedLoan; + final addingErrorMsg = isEdit + ? AppLocalizations.of(context)!.loanUpdatingError + : AppLocalizations.of(context)!.loanAddingError; final value = isEdit ? await loanListNotifier.updateLoan(newLoan) : await loanListNotifier.addLoan(newLoan); @@ -96,34 +115,14 @@ class AddEditButton extends HookConsumerWidget { await loanListNotifier.copy(), ); QR.back(); - if (isEdit) { - displayToastWithContext( - TypeMsg.msg, - LoanTextConstants.updatedLoan, - ); - } else { - displayToastWithContext( - TypeMsg.msg, - LoanTextConstants.addedLoan, - ); - } + displayToastWithContext(TypeMsg.msg, addedLoanMsg); } else { - if (isEdit) { - displayToastWithContext( - TypeMsg.error, - LoanTextConstants.updatingError, - ); - } else { - displayToastWithContext( - TypeMsg.error, - LoanTextConstants.addingError, - ); - } + displayToastWithContext(TypeMsg.error, addingErrorMsg); } } else { displayToastWithContext( TypeMsg.error, - LoanTextConstants.noItemSelected, + AppLocalizations.of(context)!.loanNoItemSelected, ); } }); @@ -135,7 +134,7 @@ class AddEditButton extends HookConsumerWidget { displayToast( context, TypeMsg.error, - LoanTextConstants.addingError, + AppLocalizations.of(context)!.loanAddingError, ); }, ); @@ -143,7 +142,9 @@ class AddEditButton extends HookConsumerWidget { }); }, child: Text( - isEdit ? LoanTextConstants.edit : LoanTextConstants.add, + isEdit + ? AppLocalizations.of(context)!.loanEdit + : AppLocalizations.of(context)!.loanAdd, style: const TextStyle( color: Colors.white, fontSize: 25, diff --git a/lib/loan/ui/pages/loan_group_page/add_edit_loan_page.dart b/lib/loan/ui/pages/loan_group_page/add_edit_loan_page.dart index fd7a0a1a06..bc3d701a73 100644 --- a/lib/loan/ui/pages/loan_group_page/add_edit_loan_page.dart +++ b/lib/loan/ui/pages/loan_group_page/add_edit_loan_page.dart @@ -8,7 +8,6 @@ import 'package:titan/loan/providers/item_list_provider.dart'; import 'package:titan/loan/providers/loan_provider.dart'; import 'package:titan/loan/providers/loaner_provider.dart'; import 'package:titan/loan/providers/loaners_items_provider.dart'; -import 'package:titan/loan/tools/constants.dart'; import 'package:titan/loan/ui/loan.dart'; import 'package:titan/loan/ui/pages/loan_group_page/add_edit_button.dart'; import 'package:titan/loan/ui/pages/loan_group_page/end_date_entry.dart'; @@ -21,6 +20,7 @@ import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/tools/ui/widgets/styled_search_bar.dart'; import 'package:titan/tools/ui/widgets/text_entry.dart'; import 'package:titan/user/providers/user_list_provider.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AddEditLoanPage extends HookConsumerWidget { const AddEditLoanPage({super.key}); @@ -58,8 +58,8 @@ class AddEditLoanPage extends HookConsumerWidget { const SizedBox(height: 30), StyledSearchBar( label: isEdit - ? LoanTextConstants.editLoan - : LoanTextConstants.addLoan, + ? AppLocalizations.of(context)!.loanEditLoan + : AppLocalizations.of(context)!.loanAddLoan, onChanged: (value) async { if (value.isNotEmpty) { loanersItemsNotifier.setTData( @@ -81,7 +81,7 @@ class AddEditLoanPage extends HookConsumerWidget { const NumberSelectedText(), const SizedBox(height: 20), TextEntry( - label: LoanTextConstants.borrower, + label: AppLocalizations.of(context)!.loanBorrower, onChanged: (value) { tokenExpireWrapper(ref, () async { if (queryController.text.isNotEmpty) { @@ -104,13 +104,13 @@ class AddEditLoanPage extends HookConsumerWidget { const EndDateEntry(), const SizedBox(height: 30), TextEntry( - label: LoanTextConstants.note, + label: AppLocalizations.of(context)!.loanNote, controller: note, canBeEmpty: true, ), const SizedBox(height: 30), TextEntry( - label: LoanTextConstants.caution, + label: AppLocalizations.of(context)!.loanCaution, controller: caution, canBeEmpty: true, ), @@ -128,7 +128,9 @@ class AddEditLoanPage extends HookConsumerWidget { displayToast( context, TypeMsg.error, - LoanTextConstants.incorrectOrMissingFields, + AppLocalizations.of( + context, + )!.loanIncorrectOrMissingFields, ); } }, diff --git a/lib/loan/ui/pages/loan_group_page/check_item_card.dart b/lib/loan/ui/pages/loan_group_page/check_item_card.dart index 10abb8cf46..9c5db9b739 100644 --- a/lib/loan/ui/pages/loan_group_page/check_item_card.dart +++ b/lib/loan/ui/pages/loan_group_page/check_item_card.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:titan/loan/class/item.dart'; import 'package:titan/loan/tools/constants.dart'; import 'package:titan/tools/ui/layouts/card_layout.dart'; +import 'package:titan/l10n/app_localizations.dart'; class CheckItemCard extends StatelessWidget { final Item item; @@ -40,8 +41,8 @@ class CheckItemCard extends StatelessWidget { const SizedBox(height: 5), Text( item.loanedQuantity < item.totalQuantity - ? '${item.totalQuantity - item.loanedQuantity} ${LoanTextConstants.available}' - : LoanTextConstants.unavailable, + ? '${item.totalQuantity - item.loanedQuantity} ${AppLocalizations.of(context)!.loanAvailable}' + : AppLocalizations.of(context)!.loanUnavailable, style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, @@ -62,7 +63,7 @@ class CheckItemCard extends StatelessWidget { ), const SizedBox(height: 5), AutoSizeText( - '${LoanTextConstants.duration} : ${item.suggestedLendingDuration.toInt()} ${LoanTextConstants.days}', + '${AppLocalizations.of(context)!.loanDuration} : ${item.suggestedLendingDuration.toInt()} ${AppLocalizations.of(context)!.loanDays}', maxLines: 1, style: TextStyle( fontSize: 13, diff --git a/lib/loan/ui/pages/loan_group_page/end_date_entry.dart b/lib/loan/ui/pages/loan_group_page/end_date_entry.dart index a18d9e3a8a..ae42d7c136 100644 --- a/lib/loan/ui/pages/loan_group_page/end_date_entry.dart +++ b/lib/loan/ui/pages/loan_group_page/end_date_entry.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/loan/providers/end_provider.dart'; import 'package:titan/loan/providers/initial_date_provider.dart'; -import 'package:titan/loan/tools/constants.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/ui/widgets/date_entry.dart'; +import 'package:titan/l10n/app_localizations.dart'; class EndDateEntry extends HookConsumerWidget { const EndDateEntry({super.key}); @@ -22,7 +22,7 @@ class EndDateEntry extends HookConsumerWidget { initialDate: initialDate, ), controller: TextEditingController(text: end), - label: LoanTextConstants.endDate, + label: AppLocalizations.of(context)!.loanEndDate, ); } } diff --git a/lib/loan/ui/pages/loan_group_page/item_bar.dart b/lib/loan/ui/pages/loan_group_page/item_bar.dart index efd2e41cc3..11ee388269 100644 --- a/lib/loan/ui/pages/loan_group_page/item_bar.dart +++ b/lib/loan/ui/pages/loan_group_page/item_bar.dart @@ -9,11 +9,11 @@ import 'package:titan/loan/providers/loaner_provider.dart'; import 'package:titan/loan/providers/loaners_items_provider.dart'; import 'package:titan/loan/providers/selected_items_provider.dart'; import 'package:titan/loan/providers/start_provider.dart'; -import 'package:titan/loan/tools/constants.dart'; import 'package:titan/loan/ui/pages/loan_group_page/check_item_card.dart'; import 'package:titan/tools/constants.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; +import 'package:titan/l10n/app_localizations.dart'; class ItemBar extends HookConsumerWidget { final bool isEdit; @@ -21,6 +21,7 @@ class ItemBar extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final locale = Localizations.localeOf(context); final loaner = ref.watch(loanerProvider); final loanersItems = ref.watch(loanersItemsProvider); final selectedItems = ref.watch(editSelectedListProvider); @@ -36,12 +37,15 @@ class ItemBar extends HookConsumerWidget { loaderColor: ColorConstants.background2, builder: (context, data) { if (loanersItems[loaner] == null) { - return const SizedBox( + return SizedBox( height: 180, child: Center( child: Text( - LoanTextConstants.noItems, - style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500), + AppLocalizations.of(context)!.loanNoItems, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + ), ), ), ); @@ -51,12 +55,12 @@ class ItemBar extends HookConsumerWidget { loaderColor: ColorConstants.background2, builder: (context, itemList) { if (itemList.isEmpty) { - return const SizedBox( + return SizedBox( height: 180, child: Center( child: Text( - LoanTextConstants.noItems, - style: TextStyle( + AppLocalizations.of(context)!.loanNoItems, + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w500, ), @@ -113,6 +117,7 @@ class ItemBar extends HookConsumerWidget { endNotifier.setEndFromSelected( start, selected, + locale.toString(), ); cautionNotifier.setCautionFromSelected( selectedItemsWithQuantity, @@ -178,6 +183,7 @@ class ItemBar extends HookConsumerWidget { endNotifier.setEndFromSelected( start, selected, + locale.toString(), ); cautionNotifier.setCautionFromSelected( selectedItemsWithQuantity, diff --git a/lib/loan/ui/pages/loan_group_page/number_selected_text.dart b/lib/loan/ui/pages/loan_group_page/number_selected_text.dart index 94933f86ec..77a7ced90e 100644 --- a/lib/loan/ui/pages/loan_group_page/number_selected_text.dart +++ b/lib/loan/ui/pages/loan_group_page/number_selected_text.dart @@ -15,6 +15,7 @@ class NumberSelectedText extends HookConsumerWidget { 0, (previousValue, element) => previousValue + element, ), + context, ), ); } diff --git a/lib/loan/ui/pages/loan_group_page/start_date_entry.dart b/lib/loan/ui/pages/loan_group_page/start_date_entry.dart index 6adaf9b7b4..0046e22822 100644 --- a/lib/loan/ui/pages/loan_group_page/start_date_entry.dart +++ b/lib/loan/ui/pages/loan_group_page/start_date_entry.dart @@ -6,15 +6,16 @@ import 'package:titan/loan/providers/initial_date_provider.dart'; import 'package:titan/loan/providers/item_list_provider.dart'; import 'package:titan/loan/providers/selected_items_provider.dart'; import 'package:titan/loan/providers/start_provider.dart'; -import 'package:titan/loan/tools/constants.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/ui/widgets/date_entry.dart'; +import 'package:titan/l10n/app_localizations.dart'; class StartDateEntry extends HookConsumerWidget { const StartDateEntry({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final locale = Localizations.localeOf(context); final items = ref.watch(itemListProvider); final selectedItems = ref.watch(editSelectedListProvider); final endNotifier = ref.watch(endProvider.notifier); @@ -35,19 +36,21 @@ class StartDateEntry extends HookConsumerWidget { ) .toList(); if (selected.isNotEmpty) { - endNotifier.setEndFromSelected(date, selected); + endNotifier.setEndFromSelected(date, selected, locale.toString()); } else { endNotifier.setEnd(""); } - initialDateNotifier.setDate(DateTime.parse(processDateBack(date))); + initialDateNotifier.setDate( + DateTime.parse(processDateBack(date, locale.toString())), + ); }); }, initialDate: start.isNotEmpty - ? DateTime.parse(processDateBack(start)) + ? DateTime.parse(processDateBack(start, locale.toString())) : now, firstDate: DateTime(now.year - 1, now.month, now.day), ), - label: LoanTextConstants.beginDate, + label: AppLocalizations.of(context)!.loanBeginDate, controller: TextEditingController(text: start), ); } diff --git a/lib/loan/ui/pages/main_page/main_page.dart b/lib/loan/ui/pages/main_page/main_page.dart index 544555a48d..739c97f865 100644 --- a/lib/loan/ui/pages/main_page/main_page.dart +++ b/lib/loan/ui/pages/main_page/main_page.dart @@ -8,7 +8,6 @@ import 'package:titan/loan/providers/loan_list_provider.dart'; import 'package:titan/loan/providers/loan_provider.dart'; import 'package:titan/loan/providers/loaner_loan_list_provider.dart'; import 'package:titan/loan/router.dart'; -import 'package:titan/loan/tools/constants.dart'; import 'package:titan/loan/ui/loan.dart'; import 'package:titan/loan/ui/pages/admin_page/loan_card.dart'; import 'package:titan/tools/ui/widgets/admin_button.dart'; @@ -16,6 +15,7 @@ import 'package:titan/tools/ui/widgets/align_left_text.dart'; import 'package:titan/tools/ui/layouts/refresher.dart'; import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class LoanMainPage extends HookConsumerWidget { const LoanMainPage({super.key}); @@ -54,6 +54,7 @@ class LoanMainPage extends HookConsumerWidget { child: Stack( children: [ Refresher( + controller: ScrollController(), onRefresh: () async { await loanListNotifier.loadLoanList(); }, @@ -64,7 +65,7 @@ class LoanMainPage extends HookConsumerWidget { ? Column( children: [ AlignLeftText( - '${onGoingLoan.length} ${LoanTextConstants.loan.toLowerCase()}${onGoingLoan.length > 1 ? 's' : ''} ${LoanTextConstants.onGoing.toLowerCase()}', + '${onGoingLoan.length} ${AppLocalizations.of(context)!.loanLoan.toLowerCase()}${onGoingLoan.length > 1 ? 's' : ''} ${AppLocalizations.of(context)!.loanOnGoing.toLowerCase()}', padding: const EdgeInsets.symmetric( horizontal: 30.0, ), @@ -93,7 +94,7 @@ class LoanMainPage extends HookConsumerWidget { Expanded( child: Center( child: Text( - LoanTextConstants.noLoan, + AppLocalizations.of(context)!.loanNoLoan, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -112,7 +113,7 @@ class LoanMainPage extends HookConsumerWidget { children: [ const SizedBox(height: 30), AlignLeftText( - '${returnedLoan.length} ${LoanTextConstants.loan.toLowerCase()}${returnedLoan.length > 1 ? 's' : ''} ${LoanTextConstants.returned.toLowerCase()}${returnedLoan.length > 1 ? 's' : ''}', + '${returnedLoan.length} ${AppLocalizations.of(context)!.loanLoan.toLowerCase()}${returnedLoan.length > 1 ? 's' : ''} ${AppLocalizations.of(context)!.loanReturned.toLowerCase()}${returnedLoan.length > 1 ? 's' : ''}', padding: const EdgeInsets.symmetric(horizontal: 30.0), color: Colors.grey, ), diff --git a/lib/login/class/create_account.dart b/lib/login/class/create_account.dart deleted file mode 100644 index 83d5391365..0000000000 --- a/lib/login/class/create_account.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:titan/tools/functions.dart'; - -class CreateAccount { - late String name; - late String firstname; - late String? nickname; - late String password; - late DateTime birthday; - late String? phone; - late String floor; - late int? promo; - late String activationToken; - - CreateAccount({ - required this.name, - required this.firstname, - required this.nickname, - required this.password, - required this.birthday, - required this.phone, - required this.floor, - required this.promo, - required this.activationToken, - }); - - CreateAccount.fromJson(Map json) { - name = json['name']; - firstname = json['firstname']; - nickname = json['nickname']; - password = json['password']; - birthday = processDateFromAPIWithoutHour(json["birthday"]); - phone = json['phone'] != "" ? json['phone'] : null; - floor = json['floor']; - promo = json['promo']; - activationToken = json['activation_token']; - } - - Map toJson() { - final Map data = {}; - data['name'] = name; - data['firstname'] = firstname; - data['nickname'] = nickname; - data['password'] = password; - data['birthday'] = processDateToAPIWithoutHour(birthday); - data['phone'] = phone; - data['floor'] = floor; - data['promo'] = promo; - data['activation_token'] = activationToken; - return data; - } - - CreateAccount.empty() { - name = ""; - firstname = ""; - nickname = ""; - password = ""; - birthday = DateTime.now(); - phone = ""; - floor = ""; - activationToken = ""; - } - - CreateAccount copyWith({ - String? name, - String? firstname, - String? nickname, - String? password, - DateTime? birthday, - String? phone, - int? promo, - String? floor, - String? activationToken, - }) { - return CreateAccount( - name: name ?? this.name, - firstname: firstname ?? this.firstname, - nickname: nickname ?? this.nickname, - password: password ?? this.password, - birthday: birthday ?? this.birthday, - phone: phone ?? this.phone, - floor: floor ?? this.floor, - promo: promo, - activationToken: activationToken ?? this.activationToken, - ); - } - - @override - String toString() { - return "CreateAccount {name: $name, firstname: $firstname, nickname: $nickname, password: $password, birthday: $birthday, phone: $phone, promo: $promo, floor: $floor, activationToken: $activationToken}"; - } -} diff --git a/lib/login/class/recover_request.dart b/lib/login/class/recover_request.dart deleted file mode 100644 index c4918019d3..0000000000 --- a/lib/login/class/recover_request.dart +++ /dev/null @@ -1,35 +0,0 @@ -class RecoverRequest { - late String resetToken; - late String newPassword; - - RecoverRequest({required this.resetToken, required this.newPassword}); - - RecoverRequest.fromJson(Map json) { - resetToken = json['reset_token']; - newPassword = json['new_password']; - } - - Map toJson() { - final Map data = {}; - data['reset_token'] = resetToken; - data['new_password'] = newPassword; - return data; - } - - RecoverRequest.empty() { - resetToken = ""; - newPassword = ""; - } - - RecoverRequest copyWith({String? resetToken, String? newPassword}) { - return RecoverRequest( - resetToken: resetToken ?? this.resetToken, - newPassword: newPassword ?? this.newPassword, - ); - } - - @override - String toString() { - return 'RecoverRequest{resetToken: $resetToken, newPassword: $newPassword}'; - } -} diff --git a/lib/login/class/screen_shot.dart b/lib/login/class/screen_shot.dart index 42e60f9f9f..8acf3a9563 100644 --- a/lib/login/class/screen_shot.dart +++ b/lib/login/class/screen_shot.dart @@ -1,11 +1,6 @@ class ScreenShot { final String path; - final String description; final String title; - ScreenShot({ - required this.path, - required this.description, - required this.title, - }); + ScreenShot({required this.path, required this.title}); } diff --git a/lib/login/providers/sign_up_provider.dart b/lib/login/providers/sign_up_provider.dart deleted file mode 100644 index 8c0d642b27..0000000000 --- a/lib/login/providers/sign_up_provider.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:titan/login/class/account_type.dart'; -import 'package:titan/login/class/create_account.dart'; -import 'package:titan/login/class/recover_request.dart'; -import 'package:titan/login/repositories/sign_up_repository.dart'; - -class SignUpProvider extends StateNotifier { - final SignUpRepository repository; - SignUpProvider({required this.repository}) : super(null); - - Future createUser(String email, AccountType accountType) async { - return await repository.createUser(email, accountType); - } - - Future recoverUser(String email) async { - return await repository.recoverUser(email); - } - - Future activateUser(CreateAccount createAccount) async { - return await repository.activateUser(createAccount); - } - - Future resetPassword(RecoverRequest recoverRequest) async { - return await repository.resetPassword(recoverRequest); - } -} - -final signUpProvider = StateNotifierProvider((ref) { - final signUpRepository = ref.watch(signUpRepositoryProvider); - return SignUpProvider(repository: signUpRepository); -}); diff --git a/lib/login/repositories/sign_up_repository.dart b/lib/login/repositories/sign_up_repository.dart deleted file mode 100644 index 19679ff0f2..0000000000 --- a/lib/login/repositories/sign_up_repository.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:titan/auth/providers/openid_provider.dart'; -import 'package:titan/login/class/account_type.dart'; -import 'package:titan/login/class/create_account.dart'; -import 'package:titan/login/class/recover_request.dart'; -import 'package:titan/login/tools/functions.dart'; -import 'package:titan/tools/repository/repository.dart'; - -class SignUpRepository extends Repository { - @override - // ignore: overridden_fields - final ext = "users/"; - - Future createUser(String email, AccountType accountType) async { - try { - final value = await create({ - "email": email, - "account_type": accountTypeToID(accountType), - }, suffix: "create"); - return value["success"]; - } catch (e) { - return false; - } - } - - Future recoverUser(String email) async { - return (await create({"email": email}, suffix: "recover"))["success"]; - } - - Future resetPasswordUser(String token, String password) async { - return await create({ - "reset_token": token, - "new_password": password, - }, suffix: "reset-password"); - } - - Future changePasswordUser( - String userId, - String oldPassword, - String newPassword, - ) async { - return await create({ - "user_id": userId, - "old_password": oldPassword, - "new_password": newPassword, - }, suffix: "change-password"); - } - - Future activateUser(CreateAccount createAccount) async { - try { - final value = await create(createAccount.toJson(), suffix: "activate"); - return value["success"]; - } catch (e) { - return false; - } - } - - Future resetPassword(RecoverRequest recoverRequest) async { - try { - final value = await create( - recoverRequest.toJson(), - suffix: "reset-password", - ); - return value["success"]; - } catch (e) { - return false; - } - } -} - -final signUpRepositoryProvider = Provider((ref) { - final token = ref.watch(tokenProvider); - return SignUpRepository()..setToken(token); -}); diff --git a/lib/login/router.dart b/lib/login/router.dart index ca59e95dc8..2ac01f366f 100644 --- a/lib/login/router.dart +++ b/lib/login/router.dart @@ -1,15 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:titan/drawer/providers/is_web_format_provider.dart'; +import 'package:titan/navigation/providers/is_web_format_provider.dart'; import 'package:titan/login/ui/app_sign_in.dart' deferred as app_sign_in; -import 'package:titan/login/ui/pages/create_account_page/create_account_page.dart' - deferred as create_account_page; -import 'package:titan/login/ui/pages/forget_page/forget_page.dart' - deferred as forget_page; -import 'package:titan/login/ui/pages/recover_password/recover_password_page.dart' - deferred as recover_password_page; -import 'package:titan/login/ui/pages/register_page/register_page.dart' - deferred as register_page; import 'package:titan/login/ui/web/web_sign_in.dart' deferred as web_sign_in; import 'package:titan/tools/middlewares/authenticated_middleware.dart'; import 'package:titan/tools/middlewares/deferred_middleware.dart'; @@ -18,45 +10,8 @@ import 'package:qlevar_router/qlevar_router.dart'; class LoginRouter { final Ref ref; static const String root = '/login'; - static const String createAccount = '/create_account'; - static const String forgotPassword = '/forgot_password'; - static const String mailReceived = '/mail_received'; LoginRouter(this.ref); - QRoute accountRoute() => QRoute( - path: createAccount, - builder: () => register_page.Register(), - pageType: const QMaterialPage(), - middleware: [DeferredLoadingMiddleware(register_page.loadLibrary)], - children: [ - QRoute( - path: mailReceived, - pageType: const QMaterialPage(), - builder: () => create_account_page.CreateAccountPage(), - middleware: [ - DeferredLoadingMiddleware(create_account_page.loadLibrary), - ], - ), - ], - ); - - QRoute passwordRoute() => QRoute( - path: forgotPassword, - builder: () => forget_page.ForgetPassword(), - pageType: const QMaterialPage(), - middleware: [DeferredLoadingMiddleware(forget_page.loadLibrary)], - children: [ - QRoute( - path: mailReceived, - pageType: const QMaterialPage(), - builder: () => recover_password_page.RecoverPasswordPage(), - middleware: [ - DeferredLoadingMiddleware(recover_password_page.loadLibrary), - ], - ), - ], - ); - QRoute route() => QRoute( path: LoginRouter.root, builder: () => (kIsWeb && ref.watch(isWebFormatProvider)) diff --git a/lib/login/tools/constants.dart b/lib/login/tools/constants.dart index 97873f33c4..17dde12393 100644 --- a/lib/login/tools/constants.dart +++ b/lib/login/tools/constants.dart @@ -1,65 +1,2 @@ -class LoginTextConstants { - static const String accountActivated = 'Compte activé'; - static const String accountNotActivated = 'Compte non activé'; - static const String activationCode = 'Code d\'activation'; - static const String birthday = 'Date de naissance'; - static const String canBeEmpty = 'Ce champ peut être vide'; - static const String confirmPassword = 'Confirmer le mot de passe'; - static const String create = 'Créer'; - static const String createAccount = 'Créer un compte'; - static const String createAccountTitle = 'Créer un\ncompte'; - static const String email = 'Email'; - static const String emailEmpty = 'Veuillez entrer une adresse mail'; - static const String emailInvalid = - 'Veuillez entrer une adresse mail de centrale.\nSi vous n\'en possédez pas, veuillez contacter Éclair'; - static const String emailRegExp = - r'^[\w\-.]*@(((etu(-enise)?)\.)?ec-lyon\.fr|centraliens-lyon\.net)$'; - static const String emptyFieldError = 'Ce champ ne peut pas être vide'; - static const String endActivation = 'Finaliser l\'activation'; - static const String endResetPassword = 'Finaliser la \nréinitialisation'; - static const String errorResetPassword = 'Erreur lors de la réinitialisation'; - static const String expectingDate = 'Une date est attendue'; - static const String fillAllFields = 'Veuillez remplir tous les champs'; - static const String firstname = 'Prénom'; - static const String floor = 'Étage'; - static const String forgetPassword = 'Mot de passe\noublié'; - static const String forgotPassword = 'Mot de passe oublié ?'; - static const String invalidToken = 'Code d\'activation invalide'; - static const String loginFailed = 'Échec de la connexion'; - static const String mailSendingError = 'Erreur lors de la création du compte'; - static const String mustBeIntError = 'Ce champ doit être un entier'; - static const String name = 'Nom'; - static const String newPassword = 'Nouveau mot de passe'; - static const String password = 'Mot de passe'; - static const String passwordLengthError = - 'Le mot de passe doit faire au moins 6 caractères'; - static const String passwordUppercaseError = - 'Le mot de passe doit contenir au moins une majuscule'; - static const String passwordLowercaseError = - 'Le mot de passe doit contenir au moins une minucule'; - static const String passwordNumberError = - 'Le mot de passe doit contenir au moins un chiffre'; - static const String passwordSpecialCaracterError = - 'Le mot de passe doit contenir au moins un caractère spécial'; - static const String passwordMustMatch = - 'Les mots de passe doivent correspondre'; - static const String passwordStrengthVeryWeak = 'Très faible'; - static const String passwordStrengthWeak = 'Faible'; - static const String passwordStrengthMedium = 'Moyen'; - static const String passwordStrengthStrong = 'Fort'; - static const String passwordStrengthVeryStrong = 'Très fort'; - static const String phone = 'Téléphone'; - static const String promo = 'Promo entrante (ex : 2023)'; - static const String sendedMail = 'Mail de confirmation envoyé'; - static const String sendedResetMail = 'Mail de réinitialisation envoyé'; - static const String signIn = 'Se connecter'; - static const String register = 'S\'inscrire'; - static const String recievedMail = 'J\'ai reçu le mail'; - static const String recover = 'Réinitialiser'; - static const String resetedPassword = 'Mot de passe réinitialisé'; - static const String resetPasswordTitle = 'Réinitialiser\nle mot de \npasse'; - static const String nickname = 'Surnom'; - static const String welcomeBack = 'Bienvenue'; - - static const String appName = "MyECL"; -} +const String emailRegExp = + r'^[\w\-.]*@(((etu(-enise)?)\.)?ec-lyon\.fr|centraliens-lyon\.net)$'; diff --git a/lib/login/ui/app_sign_in.dart b/lib/login/ui/app_sign_in.dart index c88ffc159e..b6d8ee126c 100644 --- a/lib/login/ui/app_sign_in.dart +++ b/lib/login/ui/app_sign_in.dart @@ -1,17 +1,20 @@ +import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/auth/providers/openid_provider.dart'; -import 'package:titan/login/providers/animation_provider.dart'; -import 'package:titan/login/router.dart'; -import 'package:titan/login/tools/constants.dart'; +import 'package:titan/feed/router.dart'; import 'package:titan/login/ui/auth_page.dart'; import 'package:titan/login/ui/components/sign_in_up_bar.dart'; import 'package:titan/tools/constants.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/providers/path_forwarding_provider.dart'; +import 'package:titan/version/providers/version_verifier_provider.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:url_launcher/url_launcher.dart'; class AppSignIn extends HookConsumerWidget { const AppSignIn({super.key}); @@ -20,7 +23,26 @@ class AppSignIn extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final authNotifier = ref.watch(authTokenProvider.notifier); final pathForwarding = ref.read(pathForwardingProvider); - final controller = ref.watch(backgroundAnimationProvider); + final isLoggedIn = ref.watch(isLoggedInProvider); + final pathForwardingNotifier = ref.watch(pathForwardingProvider.notifier); + final versionVerifier = ref.watch(versionVerifierProvider); + + useEffect(() { + if (isLoggedIn && !versionVerifier.isLoading) { + WidgetsBinding.instance.addPostFrameCallback((_) { + final currentPath = ref.read(pathForwardingProvider); + final targetPath = + currentPath.path == "/" || currentPath.path == "/login" + ? FeedRouter.root + : currentPath.path; + if (!currentPath.isLoggedIn) { + pathForwardingNotifier.login(); + } + QR.to(targetPath); + }); + } + return null; + }, [isLoggedIn, versionVerifier.isLoading]); return LoginTemplate( callback: (AnimationController controller) { @@ -34,18 +56,27 @@ class AppSignIn extends HookConsumerWidget { children: [ Expanded( flex: 3, - child: Align( - alignment: Alignment.centerLeft, - child: Text( - "MyECL", - style: GoogleFonts.elMessiri( - textStyle: const TextStyle( - fontSize: 40, - fontWeight: FontWeight.bold, - color: Colors.white, + child: Row( + children: [ + Expanded( + flex: 2, + child: Align( + alignment: Alignment.centerLeft, + child: AutoSizeText( + getAppName(), + maxLines: 1, + style: GoogleFonts.elMessiri( + textStyle: const TextStyle( + fontSize: 40, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), ), ), - ), + const Spacer(flex: 3), + ], ), ), Expanded( @@ -66,7 +97,7 @@ class AppSignIn extends HookConsumerWidget { data: (data) => data, orElse: () => false, ), - label: LoginTextConstants.signIn, + label: AppLocalizations.of(context)!.loginSignIn, onPressed: () async { await authNotifier.getTokenFromRequest(); ref @@ -79,16 +110,18 @@ class AppSignIn extends HookConsumerWidget { displayToast( context, TypeMsg.error, - LoginTextConstants.loginFailed, + AppLocalizations.of( + context, + )!.loginLoginFailed, ); }, loading: () {}, ); }, - color: ColorConstants.background2, + color: ColorConstants.tertiary, icon: const HeroIcon( HeroIcons.arrowRight, - color: ColorConstants.background2, + color: ColorConstants.tertiary, size: 35.0, ), ), @@ -104,13 +137,14 @@ class AppSignIn extends HookConsumerWidget { alignment: Alignment.centerLeft, child: InkWell( splashColor: const Color.fromRGBO(255, 255, 255, 1), - onTap: () { - QR.to(LoginRouter.createAccount); - controller?.forward(); + onTap: () async { + await launchUrl( + Uri.parse("${getTitanHost()}calypsso/register"), + ); }, - child: const Text( - LoginTextConstants.createAccount, - style: TextStyle( + child: Text( + AppLocalizations.of(context)!.loginCreateAccount, + style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w800, decoration: TextDecoration.underline, @@ -124,13 +158,14 @@ class AppSignIn extends HookConsumerWidget { alignment: Alignment.centerLeft, child: InkWell( splashColor: const Color.fromRGBO(255, 255, 255, 1), - onTap: () { - QR.to(LoginRouter.forgotPassword); - controller?.forward(); + onTap: () async { + await launchUrl( + Uri.parse("${getTitanHost()}calypsso/recover"), + ); }, - child: const Text( - LoginTextConstants.forgotPassword, - style: TextStyle( + child: Text( + AppLocalizations.of(context)!.loginForgotPassword, + style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w800, decoration: TextDecoration.underline, diff --git a/lib/login/ui/components/animation_provider.dart b/lib/login/ui/components/animation_provider.dart deleted file mode 100644 index 8b13789179..0000000000 --- a/lib/login/ui/components/animation_provider.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lib/login/ui/components/background_painter.dart b/lib/login/ui/components/background_painter.dart index 73f8902c15..d2c06ed1d9 100644 --- a/lib/login/ui/components/background_painter.dart +++ b/lib/login/ui/components/background_painter.dart @@ -119,7 +119,7 @@ class BackgroundPainter extends CustomPainter { Point(w, lerpDouble(0, h / 10.2, blueAnim.value)!), ]); - var colors = [ColorConstants.gradient1, ColorConstants.gradient2]; + var colors = [ColorConstants.main, ColorConstants.onMain]; Rect rectShape = Rect.fromLTWH(0, 0, w, h); final Gradient gradient = LinearGradient( @@ -128,7 +128,7 @@ class BackgroundPainter extends CustomPainter { end: Alignment.topRight, ); - paint = Paint()..color = ColorConstants.background2; + paint = Paint()..color = ColorConstants.onTertiary; paint2 = Paint()..shader = gradient.createShader(rectShape); paint3 = Paint()..shader = gradient.createShader(rectShape); @@ -138,7 +138,7 @@ class BackgroundPainter extends CustomPainter { canvas.drawShadow( path, - ColorConstants.background2.withAlpha(125), + ColorConstants.onTertiary.withAlpha(125), 10.0, false, ); diff --git a/lib/login/ui/components/login_field.dart b/lib/login/ui/components/login_field.dart deleted file mode 100644 index c4a303d925..0000000000 --- a/lib/login/ui/components/login_field.dart +++ /dev/null @@ -1,135 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/login/tools/constants.dart'; -import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/ui/widgets/align_left_text.dart'; - -class CreateAccountField extends HookConsumerWidget { - final TextEditingController controller; - final String label; - final int index; - final PageController pageController; - final ValueNotifier currentPage; - final TextInputType keyboardType; - final List autofillHints; - final String hint; - final GlobalKey formKey; - final bool canBeEmpty; - final bool isPassword; - final bool mustBeInt; - final String? Function(String?)? validator; - final dict = { - RegExp(r'[A-Z]'): LoginTextConstants.passwordUppercaseError, - RegExp(r'[a-z]'): LoginTextConstants.passwordLowercaseError, - RegExp(r'[0-9]'): LoginTextConstants.passwordNumberError, - RegExp(r'[!@#$%^&*(),.?":{}|<>\-_[\]+=;]'): - LoginTextConstants.passwordSpecialCaracterError, - }; - CreateAccountField({ - super.key, - required this.controller, - required this.label, - required this.index, - required this.pageController, - required this.currentPage, - required this.formKey, - this.isPassword = false, - this.keyboardType = TextInputType.text, - this.autofillHints = const [], - this.hint = '', - this.canBeEmpty = false, - this.mustBeInt = false, - this.validator, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isPassword = keyboardType == TextInputType.visiblePassword; - final hidePassword = useState(isPassword); - return Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - AlignLeftText(label, fontSize: 20, color: ColorConstants.background2), - const SizedBox(height: 12), - AutofillGroup( - child: Form( - key: formKey, - autovalidateMode: AutovalidateMode.onUserInteraction, - child: TextFormField( - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - keyboardType: keyboardType, - autofillHints: autofillHints, - onFieldSubmitted: (_) { - FocusScope.of(context).requestFocus(FocusNode()); - pageController.animateToPage( - index, - duration: const Duration(milliseconds: 500), - curve: Curves.decelerate, - ); - currentPage.value = index; - }, - obscureText: hidePassword.value, - controller: controller, - cursorColor: Colors.white, - decoration: InputDecoration( - hintText: hint, - hintStyle: TextStyle( - color: Colors.white.withValues(alpha: 0.5), - fontWeight: FontWeight.bold, - fontSize: 20, - ), - suffixIcon: (keyboardType == TextInputType.visiblePassword) - ? IconButton( - icon: Icon( - hidePassword.value - ? Icons.visibility - : Icons.visibility_off, - color: Colors.white, - ), - onPressed: () { - hidePassword.value = !hidePassword.value; - }, - ) - : null, - enabledBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: ColorConstants.background2), - ), - focusedBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: Colors.white), - ), - errorBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: Colors.white), - ), - errorStyle: const TextStyle(color: Colors.white), - ), - validator: (value) { - if (canBeEmpty) { - return null; - } - if (value == null || value.isEmpty) { - return LoginTextConstants.emptyFieldError; - } else if (isPassword && value.length < 6) { - return LoginTextConstants.passwordLengthError; - } - for (var key in dict.keys) { - if (isPassword && !value.contains(key)) { - return dict[key]; - } - } - if (mustBeInt && int.tryParse(value) == null) { - return LoginTextConstants.mustBeIntError; - } - return validator?.call(value); - }, - ), - ), - ), - ], - ); - } -} diff --git a/lib/login/ui/components/page_animation_builder.dart b/lib/login/ui/components/page_animation_builder.dart deleted file mode 100644 index cf95f9ece7..0000000000 --- a/lib/login/ui/components/page_animation_builder.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:titan/login/ui/components/background_painter.dart'; - -class PageAnimationBuilder extends StatelessWidget { - final Widget child; - final Animation animation; - const PageAnimationBuilder({ - super.key, - required this.child, - required this.animation, - }); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Stack( - children: [ - SizedBox.expand( - child: CustomPaint( - painter: BackgroundPainter(animation: animation), - ), - ), - SafeArea(child: Center(child: child)), - ], - ), - ); - } -} diff --git a/lib/login/ui/components/sign_in_up_bar.dart b/lib/login/ui/components/sign_in_up_bar.dart index 57c9af68d6..a28e4bd3ab 100644 --- a/lib/login/ui/components/sign_in_up_bar.dart +++ b/lib/login/ui/components/sign_in_up_bar.dart @@ -35,6 +35,7 @@ class SignInUpBar extends StatelessWidget { : Alignment.center, child: WaitingButton( onTap: onPressed, + isLoading: isLoading, builder: (child) => Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -43,7 +44,7 @@ class SignInUpBar extends StatelessWidget { style: TextStyle( fontSize: 24, fontWeight: FontWeight.w800, - color: color, + color: isLoading ? Colors.grey : color, ), ), child, diff --git a/lib/login/ui/components/text_from_decoration.dart b/lib/login/ui/components/text_from_decoration.dart deleted file mode 100644 index 94452994f4..0000000000 --- a/lib/login/ui/components/text_from_decoration.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:titan/tools/constants.dart'; - -InputDecoration signInRegisterInputDecoration({ - required String hintText, - required bool isSignIn, - ValueNotifier? notifier, -}) { - return InputDecoration( - contentPadding: const EdgeInsets.symmetric(vertical: 18.0), - hintStyle: TextStyle( - fontSize: 18, - color: isSignIn ? Colors.black : Colors.white, - ), - hintText: hintText, - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: isSignIn ? Colors.grey.shade600 : Colors.white, - ), - ), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: isSignIn ? Colors.grey.shade600 : Colors.white, - ), - ), - errorBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: ColorConstants.gradient2), - ), - focusedErrorBorder: const UnderlineInputBorder( - borderSide: BorderSide(width: 2.0, color: ColorConstants.gradient2), - ), - errorStyle: TextStyle( - color: isSignIn ? ColorConstants.gradient2 : Colors.white, - ), - suffixIcon: notifier == null - ? null - : IconButton( - icon: Icon( - notifier.value ? Icons.visibility : Icons.visibility_off, - color: isSignIn ? Colors.grey.shade600 : Colors.white, - ), - onPressed: () { - notifier.value = !notifier.value; - }, - ), - ); -} diff --git a/lib/login/ui/pages/create_account_page/create_account_page.dart b/lib/login/ui/pages/create_account_page/create_account_page.dart deleted file mode 100644 index 8d64afb70b..0000000000 --- a/lib/login/ui/pages/create_account_page/create_account_page.dart +++ /dev/null @@ -1,446 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/auth/providers/openid_provider.dart'; -import 'package:titan/login/class/create_account.dart'; -import 'package:titan/login/providers/sign_up_provider.dart'; -import 'package:titan/login/router.dart'; -import 'package:titan/login/tools/constants.dart'; -import 'package:titan/login/ui/components/login_field.dart'; -import 'package:titan/login/ui/auth_page.dart'; -import 'package:titan/login/ui/components/sign_in_up_bar.dart'; -import 'package:titan/settings/ui/pages/change_pass/password_strength.dart'; -import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:titan/tools/ui/widgets/align_left_text.dart'; -import 'package:titan/tools/ui/widgets/date_entry.dart'; -import 'package:titan/user/class/floors.dart'; -import 'package:qlevar_router/qlevar_router.dart'; -import 'package:smooth_page_indicator/smooth_page_indicator.dart'; - -class CreateAccountPage extends HookConsumerWidget { - const CreateAccountPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final authTokenNotifier = ref.watch(authTokenProvider.notifier); - final signUpNotifier = ref.watch(signUpProvider.notifier); - final code = QR.params['code'] ?? ''; - final isCodeGiven = code != ''; - final activationCode = useTextEditingController(text: code.toString()); - final name = useTextEditingController(); - final password = useTextEditingController(); - final passwordConfirmation = useTextEditingController(); - final firstname = useTextEditingController(); - final nickname = useTextEditingController(); - final birthday = useTextEditingController(); - final phone = useTextEditingController(); - final promo = useTextEditingController(); - final lastIndex = useState(isCodeGiven ? 1 : 0); - List items = Floors.values - .map( - (e) => DropdownMenuItem( - value: capitalize(e.toString().split('.').last), - child: Text(capitalize(e.toString().split('.').last)), - ), - ) - .toList(); - - final floor = useTextEditingController(text: items[0].value.toString()); - final currentPage = useState(isCodeGiven ? 1 : 0); - final pageController = usePageController(initialPage: currentPage.value); - void displayToastWithContext(TypeMsg type, String msg) { - displayToast(context, type, msg); - } - - List> formKeys = [ - GlobalKey(), - GlobalKey(), - GlobalKey(), - GlobalKey(), - GlobalKey(), - GlobalKey(), - GlobalKey(), - GlobalKey(), - GlobalKey(), - GlobalKey(), - ]; - - List steps = [ - CreateAccountField( - controller: activationCode, - label: LoginTextConstants.activationCode, - index: 1, - pageController: pageController, - currentPage: currentPage, - formKey: formKeys[0], - ), - Column( - children: [ - CreateAccountField( - controller: password, - label: LoginTextConstants.password, - index: 2, - pageController: pageController, - currentPage: currentPage, - formKey: formKeys[1], - keyboardType: TextInputType.visiblePassword, - ), - const Spacer(), - PasswordStrength( - newPassword: password, - textColor: ColorConstants.background2, - ), - const Spacer(), - ], - ), - Column( - children: [ - CreateAccountField( - controller: passwordConfirmation, - label: LoginTextConstants.confirmPassword, - index: 3, - pageController: pageController, - currentPage: currentPage, - formKey: formKeys[2], - keyboardType: TextInputType.visiblePassword, - validator: (value) { - if (value != password.text) { - return LoginTextConstants.passwordMustMatch; - } - return null; - }, - ), - const Spacer(), - PasswordStrength( - newPassword: password, - textColor: ColorConstants.background2, - ), - const Spacer(), - ], - ), - CreateAccountField( - controller: name, - label: LoginTextConstants.name, - index: 4, - pageController: pageController, - currentPage: currentPage, - formKey: formKeys[3], - keyboardType: TextInputType.name, - autofillHints: const [AutofillHints.familyName], - ), - CreateAccountField( - controller: firstname, - label: LoginTextConstants.firstname, - index: 5, - pageController: pageController, - currentPage: currentPage, - formKey: formKeys[4], - keyboardType: TextInputType.name, - autofillHints: const [AutofillHints.givenName], - ), - CreateAccountField( - controller: nickname, - label: LoginTextConstants.nickname, - index: 6, - pageController: pageController, - currentPage: currentPage, - formKey: formKeys[5], - keyboardType: TextInputType.name, - canBeEmpty: true, - hint: LoginTextConstants.canBeEmpty, - ), - Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const SizedBox(height: 9), - const AlignLeftText( - LoginTextConstants.birthday, - fontSize: 20, - color: ColorConstants.background2, - ), - const SizedBox(height: 1), - Form( - key: formKeys[6], - autovalidateMode: AutovalidateMode.onUserInteraction, - child: DateEntry( - onTap: () { - DateTime now = DateTime.now(); - getOnlyDayDate( - context, - birthday, - firstDate: DateTime(now.year - 110, now.month, now.day), - initialDate: DateTime(now.year - 21, now.month, now.day), - lastDate: DateTime.now(), - ); - }, - label: LoginTextConstants.birthday, - controller: birthday, - color: Colors.white, - enabledColor: ColorConstants.background2, - errorColor: Colors.white, - ), - ), - ], - ), - CreateAccountField( - controller: phone, - label: LoginTextConstants.phone, - index: 8, - pageController: pageController, - currentPage: currentPage, - formKey: formKeys[7], - keyboardType: TextInputType.phone, - autofillHints: const [AutofillHints.telephoneNumber], - canBeEmpty: true, - hint: LoginTextConstants.canBeEmpty, - ), - CreateAccountField( - controller: promo, - label: LoginTextConstants.promo, - index: 9, - pageController: pageController, - currentPage: currentPage, - formKey: formKeys[8], - keyboardType: TextInputType.number, - canBeEmpty: true, - mustBeInt: true, - hint: LoginTextConstants.canBeEmpty, - ), - Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const SizedBox(height: 8), - const AlignLeftText( - LoginTextConstants.floor, - fontSize: 20, - color: ColorConstants.background2, - ), - const SizedBox(height: 8), - AutofillGroup( - child: DropdownButtonFormField( - items: items, - value: floor.text, - onChanged: (value) { - floor.text = value.toString(); - }, - dropdownColor: ColorConstants.background2, - iconEnabledColor: Colors.grey.shade100.withValues(alpha: 0.8), - style: const TextStyle(fontSize: 20, color: Colors.white), - decoration: const InputDecoration( - contentPadding: EdgeInsets.symmetric(vertical: 10), - isDense: true, - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide(color: ColorConstants.background2), - ), - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide(color: Colors.white), - ), - errorBorder: UnderlineInputBorder( - borderSide: BorderSide(color: Colors.white), - ), - errorStyle: TextStyle(color: Colors.white), - ), - ), - ), - ], - ), - SignInUpBar( - label: LoginTextConstants.endActivation, - isLoading: false, - onPressed: () async { - if (name.text.isNotEmpty && - firstname.text.isNotEmpty && - birthday.text.isNotEmpty && - floor.text.isNotEmpty && - password.text.isNotEmpty && - activationCode.text.isNotEmpty && - passwordConfirmation.text.isNotEmpty && - password.text == passwordConfirmation.text) { - CreateAccount finalCreateAccount = CreateAccount( - name: name.text, - firstname: firstname.text, - nickname: nickname.text.isEmpty ? null : nickname.text, - birthday: DateTime.parse(processDateBack(birthday.text)), - phone: phone.text.isEmpty ? null : phone.text, - promo: promo.text.isEmpty ? null : int.parse(promo.text), - floor: floor.text, - activationToken: activationCode.text.trim(), - password: password.text, - ); - try { - final value = await signUpNotifier.activateUser( - finalCreateAccount, - ); - if (value) { - displayToastWithContext( - TypeMsg.msg, - LoginTextConstants.accountActivated, - ); - authTokenNotifier.deleteToken(); - QR.to(LoginRouter.root); - } else { - displayToastWithContext( - TypeMsg.error, - LoginTextConstants.accountNotActivated, - ); - } - } catch (e) { - displayToastWithContext(TypeMsg.error, e.toString()); - } - } else { - displayToastWithContext( - TypeMsg.error, - LoginTextConstants.fillAllFields, - ); - } - }, - ), - ]; - final len = steps.length; - - return LoginTemplate( - callback: (AnimationController controller) { - if (!controller.isCompleted) { - controller.forward(); - } - }, - child: Padding( - padding: const EdgeInsets.all(30.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Align( - alignment: Alignment.centerLeft, - child: GestureDetector( - onTap: () { - QR.to(LoginRouter.createAccount); - }, - child: const HeroIcon( - HeroIcons.chevronLeft, - color: Colors.white, - size: 30, - ), - ), - ), - Expanded( - flex: 3, - child: Align( - alignment: Alignment.centerLeft, - child: Text( - LoginTextConstants.createAccountTitle, - style: GoogleFonts.elMessiri( - textStyle: const TextStyle( - fontSize: 30, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - ), - ), - Expanded( - flex: 5, - child: Column( - children: [ - const Spacer(), - Expanded( - flex: 6, - child: PageView( - physics: const NeverScrollableScrollPhysics(), - scrollDirection: Axis.horizontal, - controller: pageController, - onPageChanged: (value) { - lastIndex.value = currentPage.value; - currentPage.value = value; - }, - children: steps, - ), - ), - const SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - currentPage.value != (isCodeGiven ? 1 : 0) - ? GestureDetector( - onTap: (() { - FocusScope.of( - context, - ).requestFocus(FocusNode()); - currentPage.value--; - lastIndex.value = currentPage.value; - pageController.previousPage( - duration: const Duration(milliseconds: 500), - curve: Curves.decelerate, - ); - }), - child: const HeroIcon( - HeroIcons.arrowLeft, - color: Colors.white, - size: 30, - ), - ) - : Container(), - currentPage.value != len - 1 - ? GestureDetector( - onTap: (() { - if (currentPage.value >= steps.length - 2 || - formKeys[lastIndex.value].currentState! - .validate()) { - FocusScope.of( - context, - ).requestFocus(FocusNode()); - pageController.nextPage( - duration: const Duration(milliseconds: 500), - curve: Curves.decelerate, - ); - currentPage.value++; - lastIndex.value = currentPage.value; - } - }), - child: const HeroIcon( - HeroIcons.arrowRight, - color: Colors.white, - size: 30, - ), - ) - : Container(), - ], - ), - const Spacer(), - SmoothPageIndicator( - controller: pageController, - count: len, - effect: const WormEffect( - dotColor: ColorConstants.background2, - activeDotColor: Colors.white, - dotWidth: 12, - dotHeight: 12, - ), - onDotClicked: (index) { - if (index < lastIndex.value || - currentPage.value >= steps.length - 2 || - formKeys[lastIndex.value].currentState!.validate()) { - FocusScope.of(context).requestFocus(FocusNode()); - currentPage.value = index; - lastIndex.value = index; - pageController.animateToPage( - index, - duration: const Duration(milliseconds: 500), - curve: Curves.decelerate, - ); - } - }, - ), - const SizedBox(height: 12), - ], - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/login/ui/pages/forget_page/forget_page.dart b/lib/login/ui/pages/forget_page/forget_page.dart deleted file mode 100644 index 674918fc93..0000000000 --- a/lib/login/ui/pages/forget_page/forget_page.dart +++ /dev/null @@ -1,176 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/auth/providers/openid_provider.dart'; -import 'package:titan/login/providers/sign_up_provider.dart'; -import 'package:titan/login/router.dart'; -import 'package:titan/login/tools/constants.dart'; -import 'package:titan/login/ui/auth_page.dart'; -import 'package:titan/login/ui/components/sign_in_up_bar.dart'; -import 'package:titan/login/ui/components/text_from_decoration.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:qlevar_router/qlevar_router.dart'; - -class ForgetPassword extends HookConsumerWidget { - const ForgetPassword({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final signUpNotifier = ref.watch(signUpProvider.notifier); - final email = useTextEditingController(); - void displayToastWithContext(TypeMsg type, String msg) { - displayToast(context, type, msg); - } - - return LoginTemplate( - callback: (AnimationController controller) { - if (!controller.isCompleted) { - controller.forward(); - } - }, - child: Form( - autovalidateMode: AutovalidateMode.onUserInteraction, - child: Padding( - padding: const EdgeInsets.all(30.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Align( - alignment: Alignment.centerLeft, - child: GestureDetector( - onTap: () { - QR.to(LoginRouter.root); - }, - child: const HeroIcon( - HeroIcons.chevronLeft, - color: Colors.white, - size: 30, - ), - ), - ), - Expanded( - flex: 3, - child: Align( - alignment: Alignment.centerLeft, - child: Text( - LoginTextConstants.forgetPassword, - style: GoogleFonts.elMessiri( - textStyle: const TextStyle( - fontSize: 30, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - ), - ), - Expanded( - flex: 5, - child: Column( - children: [ - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16), - child: AutofillGroup( - child: TextFormField( - controller: email, - style: const TextStyle( - fontSize: 18, - color: Colors.white, - ), - decoration: signInRegisterInputDecoration( - isSignIn: false, - hintText: LoginTextConstants.email, - ), - keyboardType: TextInputType.emailAddress, - autofillHints: const [AutofillHints.email], - ), - ), - ), - const SizedBox(height: 30), - SignInUpBar( - label: LoginTextConstants.recover, - isLoading: ref - .watch(loadingProvider) - .maybeWhen(data: (data) => data, orElse: () => false), - onPressed: () async { - final value = await signUpNotifier.recoverUser( - email.text, - ); - if (value) { - displayToastWithContext( - TypeMsg.msg, - LoginTextConstants.sendedResetMail, - ); - email.clear(); - QR.to( - LoginRouter.forgotPassword + - LoginRouter.mailReceived, - ); - } else { - displayToastWithContext( - TypeMsg.error, - LoginTextConstants.mailSendingError, - ); - } - }, - ), - const Spacer(), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - height: 40, - alignment: Alignment.centerLeft, - child: InkWell( - splashColor: const Color.fromRGBO(255, 255, 255, 1), - onTap: () { - QR.to(LoginRouter.root); - }, - child: const Text( - LoginTextConstants.signIn, - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w800, - decoration: TextDecoration.underline, - fontSize: 14, - ), - ), - ), - ), - Container( - height: 40, - alignment: Alignment.centerLeft, - child: InkWell( - splashColor: const Color.fromRGBO(255, 255, 255, 1), - onTap: () { - QR.to( - LoginRouter.forgotPassword + - LoginRouter.mailReceived, - ); - }, - child: const Text( - LoginTextConstants.recievedMail, - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w800, - decoration: TextDecoration.underline, - fontSize: 14, - ), - ), - ), - ), - ], - ), - ], - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/login/ui/pages/recover_password/recover_password_page.dart b/lib/login/ui/pages/recover_password/recover_password_page.dart deleted file mode 100644 index 87fde95a66..0000000000 --- a/lib/login/ui/pages/recover_password/recover_password_page.dart +++ /dev/null @@ -1,245 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/auth/providers/openid_provider.dart'; -import 'package:titan/login/class/recover_request.dart'; -import 'package:titan/login/providers/sign_up_provider.dart'; -import 'package:titan/login/router.dart'; -import 'package:titan/login/tools/constants.dart'; -import 'package:titan/login/ui/components/login_field.dart'; -import 'package:titan/login/ui/auth_page.dart'; -import 'package:titan/login/ui/components/sign_in_up_bar.dart'; -import 'package:titan/settings/ui/pages/change_pass/password_strength.dart'; -import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:qlevar_router/qlevar_router.dart'; -import 'package:smooth_page_indicator/smooth_page_indicator.dart'; - -class RecoverPasswordPage extends HookConsumerWidget { - const RecoverPasswordPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final authTokenNotifier = ref.watch(authTokenProvider.notifier); - final signUpNotifier = ref.watch(signUpProvider.notifier); - final activationCode = useTextEditingController(); - final password = useTextEditingController(); - final currentPage = useState(0); - final lastIndex = useState(0); - final pageController = usePageController(); - void displayToastWithContext(TypeMsg type, String msg) { - displayToast(context, type, msg); - } - - List> formKeys = [ - GlobalKey(), - GlobalKey(), - ]; - - List steps = [ - CreateAccountField( - controller: activationCode, - label: LoginTextConstants.activationCode, - index: 1, - pageController: pageController, - currentPage: currentPage, - formKey: formKeys[0], - ), - Column( - children: [ - CreateAccountField( - controller: password, - label: LoginTextConstants.newPassword, - index: 2, - pageController: pageController, - currentPage: currentPage, - formKey: formKeys[1], - keyboardType: TextInputType.visiblePassword, - ), - const Spacer(), - PasswordStrength( - newPassword: password, - textColor: ColorConstants.background2, - ), - const Spacer(), - ], - ), - SignInUpBar( - label: LoginTextConstants.endResetPassword, - isLoading: false, - onPressed: () async { - if (password.text.isNotEmpty && activationCode.text.isNotEmpty) { - RecoverRequest recoverRequest = RecoverRequest( - resetToken: activationCode.text.trim(), - newPassword: password.text, - ); - final value = await signUpNotifier.resetPassword(recoverRequest); - if (value) { - displayToastWithContext( - TypeMsg.msg, - LoginTextConstants.resetedPassword, - ); - authTokenNotifier.deleteToken(); - QR.to(LoginRouter.root); - } else { - displayToastWithContext( - TypeMsg.error, - LoginTextConstants.invalidToken, - ); - } - } else { - displayToastWithContext( - TypeMsg.error, - LoginTextConstants.fillAllFields, - ); - } - }, - ), - ]; - final len = steps.length; - - return LoginTemplate( - callback: (AnimationController controller) { - if (!controller.isCompleted) { - controller.forward(); - } - }, - child: Padding( - padding: const EdgeInsets.all(30.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Align( - alignment: Alignment.centerLeft, - child: GestureDetector( - onTap: () { - QR.to(LoginRouter.forgotPassword); - }, - child: const HeroIcon( - HeroIcons.chevronLeft, - color: Colors.white, - size: 30, - ), - ), - ), - Expanded( - flex: 3, - child: Align( - alignment: Alignment.centerLeft, - child: Text( - LoginTextConstants.resetPasswordTitle, - style: GoogleFonts.elMessiri( - textStyle: const TextStyle( - fontSize: 30, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - ), - ), - Expanded( - flex: 5, - child: Column( - children: [ - const Spacer(), - Expanded( - flex: 4, - child: PageView( - scrollDirection: Axis.horizontal, - controller: pageController, - onPageChanged: (index) { - lastIndex.value = currentPage.value; - currentPage.value = index; - }, - physics: const BouncingScrollPhysics(), - children: steps, - ), - ), - const SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - currentPage.value != 0 - ? GestureDetector( - onTap: (() { - FocusScope.of( - context, - ).requestFocus(FocusNode()); - currentPage.value--; - lastIndex.value = currentPage.value; - pageController.previousPage( - duration: const Duration(milliseconds: 500), - curve: Curves.decelerate, - ); - }), - child: const HeroIcon( - HeroIcons.arrowLeft, - color: Colors.white, - size: 30, - ), - ) - : Container(), - currentPage.value != len - 1 - ? GestureDetector( - onTap: (() { - if (currentPage.value == steps.length - 1 || - formKeys[lastIndex.value].currentState! - .validate()) { - FocusScope.of( - context, - ).requestFocus(FocusNode()); - pageController.nextPage( - duration: const Duration(milliseconds: 500), - curve: Curves.decelerate, - ); - currentPage.value++; - lastIndex.value = currentPage.value; - } - }), - child: const HeroIcon( - HeroIcons.arrowRight, - color: Colors.white, - size: 30, - ), - ) - : Container(), - ], - ), - const Spacer(), - SmoothPageIndicator( - controller: pageController, - count: len, - effect: const WormEffect( - dotColor: ColorConstants.background2, - activeDotColor: Colors.white, - dotWidth: 12, - dotHeight: 12, - ), - onDotClicked: (index) { - if (index < lastIndex.value || - index == steps.length - 1 || - formKeys[lastIndex.value].currentState!.validate()) { - FocusScope.of(context).requestFocus(FocusNode()); - currentPage.value = index; - lastIndex.value = index; - pageController.animateToPage( - index, - duration: const Duration(milliseconds: 500), - curve: Curves.decelerate, - ); - } - }, - ), - const SizedBox(height: 10), - ], - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/login/ui/pages/register_page/register_page.dart b/lib/login/ui/pages/register_page/register_page.dart deleted file mode 100644 index 51803c6392..0000000000 --- a/lib/login/ui/pages/register_page/register_page.dart +++ /dev/null @@ -1,200 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/auth/providers/openid_provider.dart'; -import 'package:titan/login/class/account_type.dart'; -import 'package:titan/login/providers/sign_up_provider.dart'; -import 'package:titan/login/router.dart'; -import 'package:titan/login/tools/constants.dart'; -import 'package:titan/login/ui/auth_page.dart'; -import 'package:titan/login/ui/components/sign_in_up_bar.dart'; -import 'package:titan/login/ui/components/text_from_decoration.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:qlevar_router/qlevar_router.dart'; - -class Register extends HookConsumerWidget { - const Register({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final signUpNotifier = ref.watch(signUpProvider.notifier); - final mail = useTextEditingController(); - final hidePass = useState(true); - final key = GlobalKey(); - void displayToastWithContext(TypeMsg type, String msg) { - displayToast(context, type, msg); - } - - return LoginTemplate( - callback: (AnimationController controller) { - if (!controller.isCompleted) { - controller.forward(); - } - }, - child: Form( - key: key, - child: Padding( - padding: const EdgeInsets.all(30.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Align( - alignment: Alignment.centerLeft, - child: GestureDetector( - onTap: () { - QR.to(LoginRouter.root); - }, - child: const HeroIcon( - HeroIcons.chevronLeft, - color: Colors.white, - size: 30, - ), - ), - ), - Expanded( - flex: 3, - child: Align( - alignment: Alignment.centerLeft, - child: Text( - LoginTextConstants.createAccountTitle, - style: GoogleFonts.elMessiri( - textStyle: const TextStyle( - fontSize: 30, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - ), - ), - Expanded( - flex: 5, - child: Column( - children: [ - const Spacer(), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16), - child: AutofillGroup( - child: TextFormField( - controller: mail, - style: const TextStyle( - fontSize: 18, - color: Colors.white, - ), - decoration: signInRegisterInputDecoration( - isSignIn: false, - hintText: LoginTextConstants.email, - ), - keyboardType: TextInputType.emailAddress, - autofillHints: const [AutofillHints.email], - validator: (value) { - if (value == null || value.isEmpty) { - return LoginTextConstants.emailEmpty; - } - RegExp regExp = RegExp( - LoginTextConstants.emailRegExp, - ); - if (!regExp.hasMatch(value)) { - return LoginTextConstants.emailInvalid; - } - return null; - }, - ), - ), - ), - const SizedBox(height: 30), - SignInUpBar( - label: LoginTextConstants.create, - isLoading: ref - .watch(loadingProvider) - .maybeWhen(data: (data) => data, orElse: () => false), - onPressed: () async { - if (key.currentState!.validate()) { - final value = await signUpNotifier.createUser( - mail.text, - AccountType.student, - ); - if (value) { - hidePass.value = true; - mail.clear(); - QR.to( - LoginRouter.createAccount + - LoginRouter.mailReceived, - ); - displayToastWithContext( - TypeMsg.msg, - LoginTextConstants.sendedMail, - ); - } else { - displayToastWithContext( - TypeMsg.error, - LoginTextConstants.mailSendingError, - ); - } - } else { - displayToastWithContext( - TypeMsg.error, - LoginTextConstants.emailInvalid, - ); - } - }, - ), - const Spacer(), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - height: 40, - alignment: Alignment.centerLeft, - child: InkWell( - splashColor: const Color.fromRGBO(255, 255, 255, 1), - onTap: () { - QR.to(LoginRouter.root); - }, - child: const Text( - LoginTextConstants.signIn, - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w800, - decoration: TextDecoration.underline, - fontSize: 14, - ), - ), - ), - ), - Container( - height: 40, - alignment: Alignment.centerLeft, - child: InkWell( - splashColor: const Color.fromRGBO(255, 255, 255, 1), - onTap: () { - QR.to( - LoginRouter.createAccount + - LoginRouter.mailReceived, - ); - }, - child: const Text( - LoginTextConstants.recievedMail, - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w800, - decoration: TextDecoration.underline, - fontSize: 14, - ), - ), - ), - ), - ], - ), - ], - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/login/ui/pages/sign_in_page/sign_in_page.dart b/lib/login/ui/pages/sign_in_page/sign_in_page.dart deleted file mode 100644 index a9ef2e5f73..0000000000 --- a/lib/login/ui/pages/sign_in_page/sign_in_page.dart +++ /dev/null @@ -1,163 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/auth/providers/openid_provider.dart'; -import 'package:titan/login/router.dart'; -import 'package:titan/login/tools/constants.dart'; -import 'package:titan/login/ui/auth_page.dart'; -import 'package:titan/login/ui/components/sign_in_up_bar.dart'; -import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:titan/tools/providers/path_forwarding_provider.dart'; -import 'package:qlevar_router/qlevar_router.dart'; - -class SignIn extends HookConsumerWidget { - const SignIn({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final authNotifier = ref.watch(authTokenProvider.notifier); - final pathForwarding = ref.read(pathForwardingProvider); - - return LoginTemplate( - callback: (AnimationController controller) { - if (controller.isCompleted) { - controller.reverse(); - } - }, - child: AutofillGroup( - child: Form( - autovalidateMode: AutovalidateMode.onUserInteraction, - child: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - children: [ - Expanded( - flex: 3, - child: Align( - alignment: Alignment.centerLeft, - child: Text( - LoginTextConstants.appName, - style: GoogleFonts.elMessiri( - textStyle: const TextStyle( - fontSize: 40, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - ), - ), - Expanded( - flex: 5, - child: Column( - children: [ - Expanded( - flex: 2, - child: Column( - children: [ - Expanded( - child: Image(image: AssetImage(getTitanLogo())), - ), - SignInUpBar( - isLoading: ref - .watch(loadingProvider) - .maybeWhen( - data: (data) => data, - orElse: () => false, - ), - label: LoginTextConstants.signIn, - onPressed: () async { - await authNotifier.getTokenFromRequest(); - ref - .watch(authTokenProvider) - .when( - data: (token) { - QR.to(pathForwarding.path); - }, - error: (e, s) { - displayToast( - context, - TypeMsg.error, - LoginTextConstants.loginFailed, - ); - }, - loading: () {}, - ); - }, - color: ColorConstants.background2, - icon: const HeroIcon( - HeroIcons.arrowRight, - color: ColorConstants.background2, - size: 35.0, - ), - ), - ], - ), - ), - const Spacer(flex: 1), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - height: 40, - alignment: Alignment.centerLeft, - child: InkWell( - splashColor: const Color.fromRGBO( - 255, - 255, - 255, - 1, - ), - onTap: () { - QR.to(LoginRouter.createAccount); - }, - child: const Text( - LoginTextConstants.createAccount, - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w800, - decoration: TextDecoration.underline, - fontSize: 14, - ), - ), - ), - ), - Container( - height: 40, - alignment: Alignment.centerLeft, - child: InkWell( - splashColor: const Color.fromRGBO( - 255, - 255, - 255, - 1, - ), - onTap: () { - QR.to(LoginRouter.forgotPassword); - }, - child: const Text( - LoginTextConstants.forgotPassword, - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w800, - decoration: TextDecoration.underline, - fontSize: 14, - ), - ), - ), - ), - ], - ), - ], - ), - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/login/ui/web/left_panel.dart b/lib/login/ui/web/left_panel.dart index b743cbff5c..d4f6a93d43 100644 --- a/lib/login/ui/web/left_panel.dart +++ b/lib/login/ui/web/left_panel.dart @@ -1,15 +1,14 @@ import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/auth/providers/openid_provider.dart'; -import 'package:titan/login/providers/animation_provider.dart'; -import 'package:titan/login/router.dart'; -import 'package:titan/login/tools/constants.dart'; +import 'package:titan/tools/constants.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/providers/path_forwarding_provider.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:url_launcher/url_launcher.dart'; class LeftPanel extends HookConsumerWidget { const LeftPanel({super.key}); @@ -18,7 +17,6 @@ class LeftPanel extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final authNotifier = ref.watch(authTokenProvider.notifier); final pathForwarding = ref.read(pathForwardingProvider); - final controller = ref.watch(backgroundAnimationProvider); final isLoading = ref .watch(loadingProvider) .maybeWhen(data: (data) => data, orElse: () => false); @@ -37,8 +35,8 @@ class LeftPanel extends HookConsumerWidget { children: [ Image.asset(getTitanLogo(), width: 70, height: 70), const SizedBox(width: 20), - const Text( - 'MyECL', + Text( + getAppName(), style: TextStyle( fontSize: 30, fontWeight: FontWeight.bold, @@ -47,15 +45,18 @@ class LeftPanel extends HookConsumerWidget { const SizedBox(width: 15), const Text( "-", - style: TextStyle(fontSize: 25, color: Colors.black), + style: TextStyle( + fontSize: 25, + color: ColorConstants.onTertiary, + ), ), const SizedBox(width: 15), - const Text( - "L'application de l'associatif centralien", + Text( + AppLocalizations.of(context)!.loginShortDescription, style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, - color: Colors.black, + color: ColorConstants.onTertiary, ), ), ], @@ -71,14 +72,7 @@ class LeftPanel extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ const Spacer(), - Expanded( - flex: 5, - child: SvgPicture.asset( - 'assets/images/login.svg', - width: 350, - height: double.infinity, - ), - ), + Expanded(flex: 5, child: Image.asset('assets/images/login.webp')), const SizedBox(height: 70), WaitingButton( onTap: () async { @@ -93,7 +87,7 @@ class LeftPanel extends HookConsumerWidget { displayToast( context, TypeMsg.error, - LoginTextConstants.loginFailed, + AppLocalizations.of(context)!.loginLoginFailed, ); }, loading: () {}, @@ -104,22 +98,14 @@ class LeftPanel extends HookConsumerWidget { height: 60, decoration: BoxDecoration( gradient: const LinearGradient( - colors: [ - Color(0xFFFF8A14), - Color.fromARGB(255, 255, 114, 0), - ], + colors: [ColorConstants.main, ColorConstants.onMain], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: const Color.fromARGB( - 255, - 255, - 114, - 0, - ).withValues(alpha: 0.2), + color: ColorConstants.onMain.withValues(alpha: 0.2), spreadRadius: 3, blurRadius: 7, offset: const Offset(0, 3), @@ -131,12 +117,12 @@ class LeftPanel extends HookConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text( - LoginTextConstants.signIn, - style: TextStyle( + Text( + AppLocalizations.of(context)!.loginSignIn, + style: const TextStyle( fontSize: 30, fontWeight: FontWeight.bold, - color: Colors.white, + color: ColorConstants.background, ), ), Container( @@ -145,12 +131,12 @@ class LeftPanel extends HookConsumerWidget { ? const Padding( padding: EdgeInsets.all(12.0), child: CircularProgressIndicator( - color: Colors.white, + color: ColorConstants.background, ), ) : const HeroIcon( HeroIcons.arrowRight, - color: Colors.white, + color: ColorConstants.background, size: 35.0, ), ), @@ -164,33 +150,35 @@ class LeftPanel extends HookConsumerWidget { children: [ const Spacer(), GestureDetector( - onTap: () { - QR.to(LoginRouter.createAccount); - controller?.forward(); + onTap: () async { + await launchUrl( + Uri.parse("${getTitanHost()}calypsso/register"), + ); }, - child: const Text( - LoginTextConstants.createAccount, - style: TextStyle( + child: Text( + AppLocalizations.of(context)!.loginCreateAccount, + style: const TextStyle( fontSize: 15, fontWeight: FontWeight.bold, decoration: TextDecoration.underline, - color: Color.fromARGB(255, 48, 48, 48), + color: ColorConstants.onTertiary, ), ), ), const Spacer(flex: 4), GestureDetector( - onTap: () { - QR.to(LoginRouter.forgotPassword); - controller?.forward(); + onTap: () async { + await launchUrl( + Uri.parse("${getTitanHost()}calypsso/recover"), + ); }, - child: const Text( - LoginTextConstants.forgotPassword, - style: TextStyle( + child: Text( + AppLocalizations.of(context)!.loginForgotPassword, + style: const TextStyle( fontSize: 15, fontWeight: FontWeight.bold, decoration: TextDecoration.underline, - color: Color.fromARGB(255, 48, 48, 48), + color: ColorConstants.onTertiary, ), ), ), diff --git a/lib/login/ui/web/right_panel.dart b/lib/login/ui/web/right_panel.dart index d0da4f7b8c..2d9cf16bfc 100644 --- a/lib/login/ui/web/right_panel.dart +++ b/lib/login/ui/web/right_panel.dart @@ -1,9 +1,9 @@ import 'dart:ui'; -import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/login/class/screen_shot.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart'; @@ -29,41 +29,36 @@ class RightPanel extends HookConsumerWidget { }); final isHovering = useState(false); + final localizeWithContext = AppLocalizations.of(context)!; + final screenShots = [ ScreenShot( path: 'assets/web/Calendrier.webp', - title: 'BDE - BDS - BDA', - description: 'Les évènements à venir', + title: localizeWithContext.loginUpcomingEvents, ), ScreenShot( path: 'assets/web/AMAP.webp', - title: 'Planet&Co', - description: 'Commande de fruit et légumes', + title: localizeWithContext.loginFruitVegetableOrders, ), ScreenShot( path: 'assets/web/Cine.webp', - title: 'Club Cinéma', - description: 'Les projections à venir', + title: localizeWithContext.loginUpcomingScreenings, ), ScreenShot( path: 'assets/web/Parametres.webp', - title: 'Éclair', - description: 'Personnalisation de l\'interface', + title: localizeWithContext.loginInterfaceCustomization, ), ScreenShot( path: 'assets/web/Pret.webp', - title: '', - description: 'Gestion des prêts de matériel', + title: localizeWithContext.loginMaterialLoans, ), ScreenShot( path: 'assets/web/Tombola.webp', - title: '', - description: 'Les tombolas proposé par plusieurs associations', + title: localizeWithContext.loginRaffles, ), ScreenShot( path: 'assets/web/Vote.webp', - title: 'CAA', - description: "L'éléction des nouveaux mandats", + title: localizeWithContext.loginNewTermsElections, ), ]; @@ -82,40 +77,13 @@ class RightPanel extends HookConsumerWidget { return Column( children: [ const SizedBox(height: 50), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(width: 20), - if (screenShot.title.isNotEmpty) - Text( - screenShot.title, - style: const TextStyle( - fontSize: 25, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - if (screenShot.title.isNotEmpty) - const Text( - " - ", - style: TextStyle( - fontSize: 25, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - Expanded( - child: AutoSizeText( - screenShot.description, - maxLines: 1, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - ], + Text( + screenShot.title, + style: const TextStyle( + fontSize: 25, + fontWeight: FontWeight.bold, + color: Colors.white, + ), ), Expanded( child: Container( @@ -221,10 +189,14 @@ class RightPanel extends HookConsumerWidget { ], ), const Spacer(), - Image.asset('assets/images/eclair.png', width: 120, height: 120), + Image.asset( + 'assets/images/proximapp.png', + width: 120, + height: 120, + ), const SizedBox(height: 30), - const Text( - "Développé par ECLAIR", + Text( + AppLocalizations.of(context)!.loginMadeBy, style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold), ), const SizedBox(height: 50), diff --git a/lib/main.dart b/lib/main.dart index 95c12ab6af..521d92ddda 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,29 +3,29 @@ import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:titan/login/providers/animation_provider.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:flutter/foundation.dart' show kIsWeb; -import 'package:titan/drawer/providers/animation_provider.dart'; -import 'package:titan/drawer/providers/swipe_provider.dart'; -import 'package:titan/drawer/providers/top_bar_callback_provider.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/navigation/providers/navbar_animation.dart'; +import 'package:titan/navigation/providers/navbar_module_list.dart'; import 'package:titan/router.dart'; import 'package:titan/service/tools/setup.dart'; +import 'package:titan/settings/providers/module_list_provider.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/plausible/plausible_observer.dart'; +import 'package:titan/tools/providers/locale_notifier.dart'; import 'package:titan/tools/providers/path_forwarding_provider.dart'; +import 'package:titan/tools/trads/en_timeago.dart'; +import 'package:titan/tools/trads/fr_timeago.dart'; import 'package:titan/tools/ui/layouts/app_template.dart'; import 'package:qlevar_router/qlevar_router.dart'; -import 'package:qlevar_router/qlevar_router.dart' as qqr; import 'package:timeago/timeago.dart' as timeago; import 'package:app_links/app_links.dart'; void main() async { - await dotenv.load(); QR.setUrlStrategy(); // We set the default page type to QMaterialPage // See https://pub.dev/packages/qlevar_router#page-transition @@ -38,8 +38,11 @@ void main() async { FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler); } await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - timeago.setLocaleMessages('fr', timeago.FrMessages()); - timeago.setLocaleMessages('fr_short', timeago.FrShortMessages()); + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); + timeago.setLocaleMessages('fr', CustomFrMessages()); + timeago.setLocaleMessages('fr_short', CustomFrShortMessages()); + timeago.setLocaleMessages('en', CustomEnMessages()); + timeago.setLocaleMessages('en_short', CustomEnShortMessages()); runApp(ProviderScope(child: MyApp())); } @@ -49,15 +52,18 @@ class MyApp extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final appRouter = ref.watch(appRouterProvider); - final animationController = useAnimationController( - duration: const Duration(seconds: 2), + final navbarAnimationController = useAnimationController( + duration: const Duration(milliseconds: 200), + initialValue: 1.0, ); - final animationNotifier = ref.read(backgroundAnimationProvider.notifier); + final navbarAnimationNotifier = ref.read(navbarAnimationProvider.notifier); final navigatorKey = GlobalKey(); final plausible = getPlausible(); final pathForwardingNotifier = ref.watch(pathForwardingProvider.notifier); - Future(() => animationNotifier.setController(animationController)); + Future( + () => navbarAnimationNotifier.setController(navbarAnimationController), + ); if (!kIsWeb) { useEffect(() { final appLinks = AppLinks(); @@ -66,14 +72,25 @@ class MyApp extends HookConsumerWidget { try { appLinks.uriLinkStream.listen((Uri? uri) { if (uri != null) { + final navbarListModuleNotifier = ref.watch( + navbarListModuleProvider.notifier, + ); + final modulesNotifier = ref.watch(modulesProvider.notifier); + final navbarListModule = ref.watch(navbarListModuleProvider); + final navbarListModuleRoot = navbarListModule + .map((module) => module.root) + .toList(); final Map queryParams = uri.queryParameters; final newPath = "/${uri.host}"; + final module = modulesNotifier.getModuleByRoot(newPath); + if (!navbarListModuleRoot.contains(newPath)) { + navbarListModuleNotifier.pushModule(module); + } pathForwardingNotifier.forward( newPath, queryParameters: queryParams, ); - QR.toName(newPath); } }); } catch (err) { @@ -90,60 +107,38 @@ class MyApp extends HookConsumerWidget { }, []); } - final popScope = PopScope( - canPop: false, - onPopInvokedWithResult: (didPop, result) async { - final topBarCallBack = ref.watch(topBarCallBackProvider); - if (QR.currentPath.split('/').length <= 2) { - final animation = ref.watch(animationProvider); - if (animation != null) { - final controller = ref.watch(swipeControllerProvider(animation)); - if (controller.isCompleted) { - SystemChannels.platform.invokeMethod('SystemNavigator.pop'); - } else { - final controllerNotifier = ref.watch( - swipeControllerProvider(animation).notifier, - ); - controllerNotifier.toggle(); - topBarCallBack.onMenu?.call(); - } - } - return; + return MaterialApp.router( + debugShowCheckedModeBanner: false, + title: getAppName(), + scrollBehavior: MyCustomScrollBehavior(), + locale: ref.watch(localeProvider), + supportedLocales: const [Locale('en', 'US'), Locale('fr', 'FR')], + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + themeMode: ThemeMode.light, + theme: ThemeData( + primarySwatch: Colors.red, + textTheme: GoogleFonts.latoTextTheme(Theme.of(context).textTheme), + brightness: Brightness.light, + ), + routeInformationParser: const QRouteInformationParser(), + builder: (context, child) { + if (child == null) { + return const SizedBox(); } - QR.back(); - topBarCallBack.onBack?.call(); + return AppTemplate(child: child); }, - child: MaterialApp.router( - debugShowCheckedModeBanner: false, - title: 'MyECL', - scrollBehavior: MyCustomScrollBehavior(), - supportedLocales: const [Locale('en', 'US'), Locale('fr', 'FR')], - localizationsDelegates: const [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - theme: ThemeData( - primarySwatch: Colors.orange, - textTheme: GoogleFonts.latoTextTheme(Theme.of(context).textTheme), - brightness: Brightness.light, - ), - routeInformationParser: const QRouteInformationParser(), - builder: (context, child) { - if (child == null) { - return const SizedBox(); - } - return AppTemplate(child: child); - }, - routerDelegate: QRouterDelegate( - appRouter.routes, - observers: [if (plausible != null) PlausibleObserver(plausible)], - initPath: AppRouter.root, - navKey: navigatorKey, - ), + routerDelegate: QRouterDelegate( + appRouter.routes, + observers: [if (plausible != null) PlausibleObserver(plausible)], + initPath: AppRouter.root, + navKey: navigatorKey, ), ); - return popScope; } } diff --git a/lib/navigation/class/module.dart b/lib/navigation/class/module.dart new file mode 100644 index 0000000000..b74f38c6e4 --- /dev/null +++ b/lib/navigation/class/module.dart @@ -0,0 +1,31 @@ +import 'package:either_dart/either.dart'; +import 'package:flutter/widgets.dart'; +import 'package:heroicons/heroicons.dart'; + +class Module { + final String Function(BuildContext) getName; + final String Function(BuildContext) getDescription; + String root; + + Module({ + required this.getName, + required this.getDescription, + required this.root, + }); + + Module copy({ + String Function(BuildContext)? getName, + + String Function(BuildContext)? description, + Either? icon, + String? root, + bool? selected, + }) => Module( + getName: getName ?? this.getName, + getDescription: getDescription, + root: root ?? this.root, + ); + + @override + String toString() => root; +} diff --git a/lib/drawer/providers/animation_provider.dart b/lib/navigation/providers/animation_provider.dart similarity index 100% rename from lib/drawer/providers/animation_provider.dart rename to lib/navigation/providers/animation_provider.dart diff --git a/lib/drawer/providers/display_quit_popup.dart b/lib/navigation/providers/display_quit_popup.dart similarity index 100% rename from lib/drawer/providers/display_quit_popup.dart rename to lib/navigation/providers/display_quit_popup.dart diff --git a/lib/drawer/providers/is_web_format_provider.dart b/lib/navigation/providers/is_web_format_provider.dart similarity index 100% rename from lib/drawer/providers/is_web_format_provider.dart rename to lib/navigation/providers/is_web_format_provider.dart diff --git a/lib/navigation/providers/navbar_animation.dart b/lib/navigation/providers/navbar_animation.dart new file mode 100644 index 0000000000..4e15c62c50 --- /dev/null +++ b/lib/navigation/providers/navbar_animation.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class NavbarAnimationProvider extends StateNotifier { + NavbarAnimationProvider() : super(null); + + int _modalCount = 0; + + void setController(AnimationController controller) { + state = controller; + } + + void toggle() { + if (state == null) { + return; + } + if (state!.isCompleted) { + state!.reverse(); + } else { + state!.forward(); + } + } + + void show() { + if (state == null) { + return; + } + if (state!.isDismissed) { + state!.forward(); + } + } + + void hide() { + if (state == null) { + return; + } + if (state!.isCompleted) { + state!.reverse(); + } + } + + void hideForModal() { + _modalCount++; + if (_modalCount == 1) { + hide(); + } + } + + void showForModal() { + _modalCount--; + if (_modalCount == 0) { + show(); + } + } + + double get value { + if (state == null) { + return 0; + } + return state!.value; + } + + AnimationController? get animation { + return state; + } + + int get modalCount => _modalCount; +} + +final navbarAnimationProvider = + StateNotifierProvider((ref) { + return NavbarAnimationProvider(); + }); diff --git a/lib/navigation/providers/navbar_module_list.dart b/lib/navigation/providers/navbar_module_list.dart new file mode 100644 index 0000000000..33191f1dc7 --- /dev/null +++ b/lib/navigation/providers/navbar_module_list.dart @@ -0,0 +1,61 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/navigation/class/module.dart'; +import 'package:titan/settings/providers/module_list_provider.dart'; +import 'package:titan/tools/providers/prefered_module_root_list_provider.dart'; + +class ModuleListNotifier extends StateNotifier> { + final int maxNumberOfModules; + final List allModules; + + ModuleListNotifier( + this.allModules, + List preferedRoots, { + this.maxNumberOfModules = 2, + }) : super(_initState(allModules, preferedRoots, maxNumberOfModules)); + + static List _initState( + List allModules, + List preferedRoots, + int max, + ) { + final preferredModules = allModules + .where((m) => preferedRoots.contains(m.root)) + .toList(); + + final filled = List.from(preferredModules); + if (filled.length < max) { + for (final m in allModules) { + if (!filled.contains(m)) { + filled.add(m); + if (filled.length == max) break; + } + } + } + + return filled.take(max).toList(); + } + + void pushModule(Module module) { + final updated = List.from(state); + + final idx = updated.indexWhere((m) => m.root == module.root); + if (idx != -1) { + updated.removeAt(idx); + updated.insert(0, module); + } else { + updated.insert(0, module); + if (updated.length > maxNumberOfModules) { + updated.removeLast(); + } + } + + state = updated; + } +} + +final navbarListModuleProvider = + StateNotifierProvider>((ref) { + final modules = ref.watch(modulesProvider); + final preferedRoots = ref.watch(preferedModuleListRootProvider); + return ModuleListNotifier(modules, preferedRoots); + }); diff --git a/lib/navigation/providers/navbar_visibility_provider.dart b/lib/navigation/providers/navbar_visibility_provider.dart new file mode 100644 index 0000000000..ecec8e2609 --- /dev/null +++ b/lib/navigation/providers/navbar_visibility_provider.dart @@ -0,0 +1,93 @@ +import 'dart:async'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class NavbarVisibilityNotifier extends StateNotifier { + NavbarVisibilityNotifier() : super(true); + + bool lastRequestedState = true; + Timer? _delayTimer; + + void _updateState(bool visible) { + lastRequestedState = visible; + if (state != visible) { + state = visible; + } + } + + void show() => _updateState(true); + + void hide() => _updateState(false); + + void showDelayed({Duration delay = const Duration(milliseconds: 500)}) { + _delayTimer?.cancel(); + _delayTimer = Timer(delay, () { + _updateState(true); + }); + } + + void cancelDelayedShow() { + _delayTimer?.cancel(); + } + + void toggle() => _updateState(!state); + + void forceShow() { + _delayTimer?.cancel(); + lastRequestedState = true; + state = true; + } + + void hideWithoutAutoShow() { + _delayTimer?.cancel(); + lastRequestedState = false; + state = false; + } + + void showTemporarily() { + _delayTimer?.cancel(); + if (!state) { + forceShow(); + } + } + + @override + void dispose() { + _delayTimer?.cancel(); + super.dispose(); + } +} + +final navbarVisibilityProvider = + StateNotifierProvider((ref) { + return NavbarVisibilityNotifier(); + }); + +class ScrollDirectionNotifier extends StateNotifier { + ScrollDirectionNotifier() : super(ScrollDirection.idle); + + double _lastScrollOffset = 0; + + void updateScrollDirection(double scrollOffset) { + final double scrollDelta = scrollOffset - _lastScrollOffset; + + if (scrollDelta > 0) { + state = ScrollDirection.down; + } else if (scrollDelta < 0) { + state = ScrollDirection.up; + } + + _lastScrollOffset = scrollOffset; + } + + void resetDirection() { + state = ScrollDirection.idle; + _lastScrollOffset = 0; + } +} + +enum ScrollDirection { up, down, idle } + +final scrollDirectionProvider = + StateNotifierProvider((ref) { + return ScrollDirectionNotifier(); + }); diff --git a/lib/drawer/providers/should_setup_provider.dart b/lib/navigation/providers/should_setup_provider.dart similarity index 100% rename from lib/drawer/providers/should_setup_provider.dart rename to lib/navigation/providers/should_setup_provider.dart diff --git a/lib/navigation/ui/all_module_page.dart b/lib/navigation/ui/all_module_page.dart new file mode 100644 index 0000000000..865ee92cd8 --- /dev/null +++ b/lib/navigation/ui/all_module_page.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/navigation/providers/navbar_module_list.dart'; +import 'package:titan/navigation/providers/navbar_visibility_provider.dart'; +import 'package:titan/navigation/ui/scroll_to_hide_navbar.dart'; +import 'package:titan/router.dart'; +import 'package:titan/settings/providers/module_list_provider.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/providers/path_forwarding_provider.dart'; +import 'package:titan/tools/providers/prefered_module_root_list_provider.dart'; +import 'package:titan/tools/ui/styleguide/list_item.dart'; +import 'package:titan/tools/ui/widgets/top_bar.dart'; + +class AllModulePage extends HookConsumerWidget { + const AllModulePage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final modules = ref.watch(modulesProvider); + final navbarListModuleNotifier = ref.watch( + navbarListModuleProvider.notifier, + ); + final navbarVisibilityNotifier = ref.read( + navbarVisibilityProvider.notifier, + ); + final scrollController = useScrollController(); + final preferedModuleRootList = ref.watch(preferedModuleListRootProvider); + final preferedModuleRootListNotifier = ref.watch( + preferedModuleListRootProvider.notifier, + ); + return Container( + color: ColorConstants.background, + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + TopBar(root: AppRouter.allModules), + Expanded( + child: ScrollToHideNavbar( + controller: scrollController, + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + controller: scrollController, + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + children: [ + ...modules.map( + (module) => Padding( + padding: const EdgeInsets.symmetric(vertical: 5.0), + child: Row( + children: [ + GestureDetector( + onTap: () { + if (preferedModuleRootList.contains( + module.root, + )) { + preferedModuleRootListNotifier + .removePreferedModulesRoot( + module.root, + ); + } else if (preferedModuleRootList.length < + 2) { + preferedModuleRootListNotifier + .addPreferedModulesRoot(module.root); + } + }, + child: HeroIcon( + HeroIcons.bookmark, + style: + preferedModuleRootList.contains( + module.root, + ) + ? HeroIconStyle.solid + : HeroIconStyle.outline, + size: 20, + color: + preferedModuleRootList.contains( + module.root, + ) + ? ColorConstants.main + : ColorConstants.tertiary, + ), + ), + SizedBox(width: 20), + Expanded( + child: ListItem( + title: module.getName(context), + subtitle: module.getDescription(context), + onTap: () { + navbarListModuleNotifier.pushModule( + module, + ); + final pathForwardingNotifier = ref.watch( + pathForwardingProvider.notifier, + ); + pathForwardingNotifier.forward( + module.root, + ); + + QR.to(module.root); + navbarVisibilityNotifier.show(); + }, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/navigation/ui/navigation_template.dart b/lib/navigation/ui/navigation_template.dart new file mode 100644 index 0000000000..7f215608e1 --- /dev/null +++ b/lib/navigation/ui/navigation_template.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/feed/router.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/class/module.dart'; +import 'package:titan/navigation/providers/display_quit_popup.dart'; +import 'package:titan/navigation/providers/navbar_animation.dart'; +import 'package:titan/navigation/providers/navbar_module_list.dart'; +import 'package:titan/navigation/providers/navbar_visibility_provider.dart'; +import 'package:titan/navigation/providers/should_setup_provider.dart'; +import 'package:titan/router.dart'; +import 'package:titan/service/tools/setup.dart'; +import 'package:titan/navigation/ui/quit_dialog.dart'; +import 'package:titan/tools/providers/path_forwarding_provider.dart'; +import 'package:titan/tools/ui/styleguide/navbar.dart'; +import 'package:titan/user/providers/user_provider.dart'; + +final GlobalKey rootNavigatorKey = GlobalKey(); + +class NavigationTemplate extends HookConsumerWidget { + static Duration duration = const Duration(milliseconds: 200); + static const double maxSlide = 255; + static const dragRightStartVal = 60; + static const dragLeftStartVal = maxSlide - 20; + static bool shouldDrag = false; + final Widget child; + + const NavigationTemplate({super.key, required this.child}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final user = ref.watch(userProvider); + final navbarListModule = ref.watch(navbarListModuleProvider); + final displayQuit = ref.watch(displayQuitProvider); + final shouldSetup = ref.watch(shouldSetupProvider); + final shouldSetupNotifier = ref.read(shouldSetupProvider.notifier); + final animation = ref.watch(navbarAnimationProvider); + final pathForwarding = ref.read(pathForwardingProvider); + final pathForwardingNotifier = ref.read(pathForwardingProvider.notifier); + + Future(() { + if (!kIsWeb && user.id != "" && shouldSetup) { + setUpNotification(ref); + shouldSetupNotifier.setShouldSetup(); + } + }); + + MediaQuery.of(context).viewInsets.bottom; + + return Builder( + builder: (context) { + return Scaffold( + body: Stack( + children: [ + child, + if (pathForwarding.isLoggedIn && user.id != "") + Positioned( + left: 0, + bottom: 0, + right: 0, + child: Consumer( + builder: (context, ref, child) { + final navbarVisible = ref.watch(navbarVisibilityProvider); + return AnimatedSlide( + offset: Offset(0, navbarVisible ? 0 : 1), + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: AnimatedOpacity( + opacity: navbarVisible ? 1.0 : 0.0, + duration: const Duration(milliseconds: 300), + child: AnimatedBuilder( + animation: animation!, + builder: (context, child) => IgnorePointer( + ignoring: animation.value != 1.0, + child: Visibility( + visible: + animation.isCompleted && + animation.value == 1.0 && + View.of(context).viewInsets.bottom == 0, + child: Opacity( + opacity: animation.value, + child: FloatingNavbar( + items: [ + FloatingNavbarItem( + module: FeedRouter.module, + onTap: () { + pathForwardingNotifier.forward( + FeedRouter.root, + ); + WidgetsBinding.instance + .addPostFrameCallback((_) { + QR.to(FeedRouter.root); + }); + }, + ), + ...navbarListModule.map((module) { + return FloatingNavbarItem( + module: module, + onTap: () { + pathForwardingNotifier.forward( + module.root, + ); + QR.to(module.root); + }, + ); + }), + FloatingNavbarItem( + module: Module( + getName: (context) => + AppLocalizations.of( + context, + )!.moduleOthers, + getDescription: (context) => + AppLocalizations.of( + context, + )!.moduleOthersDescription, + root: AppRouter.allModules, + ), + onTap: () { + pathForwardingNotifier.forward( + AppRouter.allModules, + ); + WidgetsBinding.instance + .addPostFrameCallback((_) { + QR.to(AppRouter.allModules); + }); + }, + ), + ], + ), + ), + ), + ), + ), + ), + ); + }, + ), + ), + if (displayQuit) const QuitDialog(), + ], + ), + ); + }, + ); + } +} diff --git a/lib/drawer/ui/quit_dialog.dart b/lib/navigation/ui/quit_dialog.dart similarity index 73% rename from lib/drawer/ui/quit_dialog.dart rename to lib/navigation/ui/quit_dialog.dart index 6ed944391a..b5197c57e4 100644 --- a/lib/drawer/ui/quit_dialog.dart +++ b/lib/navigation/ui/quit_dialog.dart @@ -1,13 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:qlevar_router/qlevar_router.dart'; import 'package:titan/auth/providers/openid_provider.dart'; -import 'package:titan/drawer/providers/display_quit_popup.dart'; -import 'package:titan/drawer/tools/constants.dart'; +import 'package:titan/login/router.dart'; +import 'package:titan/navigation/providers/display_quit_popup.dart'; import 'package:titan/service/providers/firebase_token_expiration_provider.dart'; import 'package:titan/service/providers/messages_provider.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; +import 'package:titan/l10n/app_localizations.dart'; class QuitDialog extends HookConsumerWidget { const QuitDialog({super.key}); @@ -26,8 +28,8 @@ class QuitDialog extends HookConsumerWidget { child: GestureDetector( onTap: () {}, child: CustomDialogBox( - descriptions: DrawerTextConstants.loginOut, - title: DrawerTextConstants.logOut, + descriptions: AppLocalizations.of(context)!.drawerLoginOut, + title: AppLocalizations.of(context)!.drawerLogOut, onYes: () { auth.deleteToken(); if (!kIsWeb) { @@ -35,8 +37,13 @@ class QuitDialog extends HookConsumerWidget { ref.watch(firebaseTokenExpirationProvider.notifier).reset(); } isCachingNotifier.set(false); - displayToast(context, TypeMsg.msg, DrawerTextConstants.logOut); + displayToast( + context, + TypeMsg.msg, + AppLocalizations.of(context)!.drawerLogOut, + ); displayQuitNotifier.setDisplay(false); + QR.to(LoginRouter.root); }, onNo: () { displayQuitNotifier.setDisplay(false); diff --git a/lib/navigation/ui/scroll_to_hide_navbar.dart b/lib/navigation/ui/scroll_to_hide_navbar.dart new file mode 100644 index 0000000000..ff93c11a71 --- /dev/null +++ b/lib/navigation/ui/scroll_to_hide_navbar.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/navigation/providers/navbar_visibility_provider.dart'; + +class ScrollToHideNavbar extends ConsumerStatefulWidget { + final Widget child; + final ScrollController controller; + final Duration hideDelay; + final Duration showDelay; + + const ScrollToHideNavbar({ + super.key, + required this.child, + required this.controller, + this.hideDelay = const Duration(milliseconds: 100), + this.showDelay = const Duration(milliseconds: 500), + }); + + @override + ConsumerState createState() => _ScrollToHideNavbarState(); +} + +class _ScrollToHideNavbarState extends ConsumerState { + double _previousOffset = 0; + bool _isScrollingDown = false; + bool _isAtTop = true; + bool _isAtBottom = false; + bool _isOverScrolling = false; + + @override + void initState() { + super.initState(); + widget.controller.addListener(_scrollListener); + } + + @override + void dispose() { + widget.controller.removeListener(_scrollListener); + + ref.read(navbarVisibilityProvider.notifier).cancelDelayedShow(); + super.dispose(); + } + + void _scrollListener() { + final navbarVisibilityNotifier = ref.read( + navbarVisibilityProvider.notifier, + ); + final ScrollPosition position = widget.controller.position; + + final double currentOffset = position.pixels; + final double maxScrollExtent = position.maxScrollExtent; + final double scrollDelta = currentOffset - _previousOffset; + + _isAtTop = currentOffset <= 0; + _isAtBottom = currentOffset >= maxScrollExtent; + _isOverScrolling = currentOffset < 0 || currentOffset > maxScrollExtent; + + if (currentOffset < 0) { + navbarVisibilityNotifier.forceShow(); + _previousOffset = currentOffset; + return; + } + + if (_isAtTop) { + navbarVisibilityNotifier.forceShow(); + _previousOffset = 0; + return; + } + + if (currentOffset > maxScrollExtent) { + _previousOffset = currentOffset; + return; + } + + if (_isAtBottom) { + _previousOffset = currentOffset; + return; + } + + if (!_isOverScrolling && scrollDelta.abs() > 1.0) { + _isScrollingDown = scrollDelta > 0; + + if (_isScrollingDown) { + navbarVisibilityNotifier.cancelDelayedShow(); + navbarVisibilityNotifier.hide(); + } else { + navbarVisibilityNotifier.showDelayed(delay: widget.showDelay); + } + } + + _previousOffset = currentOffset; + } + + @override + Widget build(BuildContext context) { + return widget.child; + } +} diff --git a/lib/others/tools/constants.dart b/lib/others/tools/constants.dart index c5e08b6afd..8b13789179 100644 --- a/lib/others/tools/constants.dart +++ b/lib/others/tools/constants.dart @@ -1,12 +1 @@ -class OthersTextConstants { - static const String checkInternetConnection = - "Veuillez vérifier votre connexion internet"; - static const String retry = "Réessayer"; - static const String tooOldVersion = - "Votre version de l'application est trop ancienne.\n\nVeuillez mettre à jour l'application."; - static const String unableToConnectToServer = - "Impossible de se connecter au serveur"; - static const String version = "Version"; - static const String noModule = - "Aucun module disponible, veuillez réessayer ultérieurement 😢😢"; -} + diff --git a/lib/others/ui/loading_page.dart b/lib/others/ui/loading_page.dart index ae2d8acfd5..e0c98dc3dc 100644 --- a/lib/others/ui/loading_page.dart +++ b/lib/others/ui/loading_page.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:titan/auth/providers/openid_provider.dart'; import 'package:titan/login/router.dart'; import 'package:titan/router.dart'; +import 'package:titan/tools/constants.dart'; import 'package:titan/tools/providers/path_forwarding_provider.dart'; import 'package:titan/tools/ui/widgets/loader.dart'; import 'package:titan/user/providers/user_provider.dart'; @@ -44,6 +45,9 @@ class LoadingPage extends ConsumerWidget { loading: () {}, error: (error, stack) => QR.to(AppRouter.noInternet), ); - return const Scaffold(body: Loader()); + return const Scaffold( + backgroundColor: ColorConstants.background, + body: Loader(), + ); } } diff --git a/lib/others/ui/no_internet_page.dart b/lib/others/ui/no_internet_page.dart index 01010cb7a8..41fa322577 100644 --- a/lib/others/ui/no_internet_page.dart +++ b/lib/others/ui/no_internet_page.dart @@ -3,9 +3,9 @@ import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/auth/providers/is_connected_provider.dart'; import 'package:titan/home/router.dart'; -import 'package:titan/others/tools/constants.dart'; import 'package:titan/tools/constants.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class NoInternetPage extends HookConsumerWidget { const NoInternetPage({super.key}); @@ -16,6 +16,7 @@ class NoInternetPage extends HookConsumerWidget { final isConnectedNotifier = ref.watch(isConnectedProvider.notifier); return Scaffold( body: Container( + color: ColorConstants.background, padding: const EdgeInsets.all(30), height: MediaQuery.of(context).size.height * 0.90, child: Center( @@ -24,16 +25,18 @@ class NoInternetPage extends HookConsumerWidget { children: [ const HeroIcon(HeroIcons.signalSlash, size: 150), const SizedBox(height: 20), - const Center( + Center( child: Text( - OthersTextConstants.unableToConnectToServer, + AppLocalizations.of(context)!.othersUnableToConnectToServer, textAlign: TextAlign.center, - style: TextStyle(fontSize: 20), + style: const TextStyle(fontSize: 20), ), ), const SizedBox(height: 20), - const Center( - child: Text(OthersTextConstants.checkInternetConnection), + Center( + child: Text( + AppLocalizations.of(context)!.othersCheckInternetConnection, + ), ), const SizedBox(height: 40), GestureDetector( @@ -64,17 +67,17 @@ class NoInternetPage extends HookConsumerWidget { ], borderRadius: BorderRadius.circular(15), ), - child: const Row( + child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - HeroIcon( + const HeroIcon( HeroIcons.arrowPath, size: 35, color: Colors.white, ), Text( - OthersTextConstants.retry, - style: TextStyle( + AppLocalizations.of(context)!.othersRetry, + style: const TextStyle( fontSize: 25, color: Colors.white, fontWeight: FontWeight.bold, diff --git a/lib/others/ui/no_module.dart b/lib/others/ui/no_module.dart index 78bbedcef4..d08943dc5d 100644 --- a/lib/others/ui/no_module.dart +++ b/lib/others/ui/no_module.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/providers/module_root_list_provider.dart'; -import 'package:titan/others/tools/constants.dart'; +import 'package:titan/super_admin/providers/module_root_list_provider.dart'; +import 'package:titan/tools/constants.dart'; import 'package:titan/tools/providers/path_forwarding_provider.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class NoModulePage extends HookConsumerWidget { const NoModulePage({super.key}); @@ -19,23 +20,24 @@ class NoModulePage extends HookConsumerWidget { }, orElse: () {}, ); - return const Scaffold( + return Scaffold( + backgroundColor: ColorConstants.background, body: Padding( - padding: EdgeInsets.symmetric(horizontal: 30), + padding: const EdgeInsets.symmetric(horizontal: 30), child: Column( children: [ - Spacer(flex: 2), - HeroIcon(HeroIcons.cubeTransparent, size: 100), - SizedBox(height: 50), + const Spacer(flex: 2), + const HeroIcon(HeroIcons.cubeTransparent, size: 100), + const SizedBox(height: 50), Center( child: Text( - OthersTextConstants.noModule, + AppLocalizations.of(context)!.othersNoModule, textAlign: TextAlign.center, - style: TextStyle(fontSize: 20), + style: const TextStyle(fontSize: 20), ), ), - Spacer(flex: 3), - SizedBox(height: 20), + const Spacer(flex: 3), + const SizedBox(height: 20), ], ), ), diff --git a/lib/others/ui/update_page.dart b/lib/others/ui/update_page.dart index 5e6013d8b8..a6b93f4615 100644 --- a/lib/others/ui/update_page.dart +++ b/lib/others/ui/update_page.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/others/tools/constants.dart'; +import 'package:titan/tools/constants.dart'; import 'package:titan/version/providers/titan_version_provider.dart'; +import 'package:titan/l10n/app_localizations.dart'; class UpdatePage extends HookConsumerWidget { const UpdatePage({super.key}); @@ -11,6 +12,7 @@ class UpdatePage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final titanVersion = ref.watch(titanVersionProvider); return Scaffold( + backgroundColor: ColorConstants.background, body: Padding( padding: const EdgeInsets.symmetric(horizontal: 30), child: Column( @@ -18,16 +20,16 @@ class UpdatePage extends HookConsumerWidget { const Spacer(flex: 2), const HeroIcon(HeroIcons.bellAlert, size: 100), const SizedBox(height: 50), - const Center( + Center( child: Text( - OthersTextConstants.tooOldVersion, + AppLocalizations.of(context)!.othersTooOldVersion, textAlign: TextAlign.center, - style: TextStyle(fontSize: 20), + style: const TextStyle(fontSize: 20), ), ), const Spacer(flex: 3), Text( - "${OthersTextConstants.version} $titanVersion", + "${AppLocalizations.of(context)!.othersVersion} $titanVersion", style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w500, diff --git a/lib/paiement/class/history_interval.dart b/lib/paiement/class/history_interval.dart index 067ac920b4..eb8666aa5e 100644 --- a/lib/paiement/class/history_interval.dart +++ b/lib/paiement/class/history_interval.dart @@ -23,5 +23,8 @@ class HistoryInterval { DateTime.now().year, DateTime.now().month, DateTime.now().day, + 23, + 59, + 59, ); } diff --git a/lib/paiement/class/invoice.dart b/lib/paiement/class/invoice.dart new file mode 100644 index 0000000000..14806ff274 --- /dev/null +++ b/lib/paiement/class/invoice.dart @@ -0,0 +1,133 @@ +import 'package:titan/paiement/class/store.dart'; +import 'package:titan/paiement/class/structure.dart'; +import 'package:titan/tools/functions.dart'; + +class InvoiceDetail { + final int total; + final StoreSimple store; + + InvoiceDetail({required this.total, required this.store}); + + factory InvoiceDetail.fromJson(Map json) { + return InvoiceDetail( + total: json['total'], + store: StoreSimple.fromJson(json['store']), + ); + } + + Map toJson() { + return {'total': total, 'store': store.toJson()}; + } + + @override + String toString() { + return 'InvoiceDetail {total: $total, store: $store}'; + } +} + +class Invoice { + final String id; + final String reference; + final Structure structure; + final DateTime creation; + final DateTime startDate; + final DateTime endDate; + final int total; + final List details; + final bool paid; + final bool received; + + Invoice({ + required this.id, + required this.reference, + required this.structure, + required this.creation, + required this.startDate, + required this.endDate, + required this.total, + required this.details, + required this.paid, + required this.received, + }); + + Invoice.fromJson(Map json) + : id = json['id'], + reference = json['reference'], + structure = Structure.fromJson(json['structure']), + creation = processDateFromAPI(json['creation']), + startDate = processDateFromAPI(json['start_date']), + endDate = processDateFromAPI(json['end_date']), + total = json['total'], + details = List.from( + json['details'].map((item) => InvoiceDetail.fromJson(item)), + ), + paid = json['paid'], + received = json['received']; + + Map toJson() { + return { + 'id': id, + 'reference': reference, + 'structure': structure.toJson(), + 'creation': processDateToAPI(creation), + 'start_date': processDateToAPI(startDate), + 'end_date': processDateToAPI(endDate), + 'total': total, + 'detail': details.map((item) => item.toJson()).toList(), + 'paid': paid, + 'received': received, + }; + } + + Invoice.empty() + : id = '', + reference = '', + structure = Structure.empty(), + creation = DateTime.now(), + startDate = DateTime.now(), + endDate = DateTime.now(), + total = 0, + details = [], + paid = false, + received = false; + + Invoice copyWith({ + String? id, + String? reference, + Structure? structure, + DateTime? creation, + DateTime? startDate, + DateTime? endDate, + int? total, + List? details, + bool? paid, + bool? received, + }) { + return Invoice( + id: id ?? this.id, + reference: reference ?? this.reference, + structure: structure ?? this.structure, + creation: creation ?? this.creation, + startDate: startDate ?? this.startDate, + endDate: endDate ?? this.endDate, + total: total ?? this.total, + details: details ?? this.details, + paid: paid ?? this.paid, + received: received ?? this.received, + ); + } + + @override + String toString() { + return 'Invoice {id: $id,\n' + 'reference: $reference,\n' + 'structure: $structure,\n' + 'creation: $creation,\n' + 'startDate: $startDate,\n' + 'endDate: $endDate,\n' + 'total: $total,\n' + 'detail: $details,\n' + 'paid: $paid,\n' + 'received: $received}'; + } +} diff --git a/lib/paiement/class/payment_request.dart b/lib/paiement/class/payment_request.dart new file mode 100644 index 0000000000..244e8296cb --- /dev/null +++ b/lib/paiement/class/payment_request.dart @@ -0,0 +1,106 @@ +import 'package:titan/tools/functions.dart'; + +enum RequestStatus { proposed, accepted, refused, expired } + +class PaymentRequest { + final String id; + final String walletId; + final DateTime creation; + final int total; + final String storeId; + final String name; + final String? storeNote; + final String module; + final String objectId; + final RequestStatus status; + final String? transactionId; + + PaymentRequest({ + required this.id, + required this.walletId, + required this.creation, + required this.total, + required this.storeId, + required this.name, + this.storeNote, + required this.module, + required this.objectId, + required this.status, + this.transactionId, + }); + + PaymentRequest.fromJson(Map json) + : id = json['id'], + walletId = json['wallet_id'], + creation = processDateFromAPI(json['creation']), + total = json['total'], + storeId = json['store_id'], + name = json['name'], + storeNote = json['store_note'], + module = json['module'], + objectId = json['object_id'], + status = RequestStatus.values.firstWhere( + (e) => e.toString().split('.').last == json['status'], + ), + transactionId = json['transaction_id']; + + Map toJson() => { + 'id': id, + 'wallet_id': walletId, + 'creation': processDateToAPI(creation), + 'total': total, + 'store_id': storeId, + 'name': name, + 'store_note': storeNote, + 'module': module, + 'object_id': objectId, + 'status': status.toString().split('.').last, + 'transaction_id': transactionId, + }; + + @override + String toString() { + return 'PaymentRequest {id: $id, walletId: $walletId, creation: $creation, total: $total, storeId: $storeId, name: $name, status: $status}'; + } + + PaymentRequest.empty() + : id = '', + walletId = '', + creation = DateTime.now(), + total = 0, + storeId = '', + name = '', + storeNote = null, + module = '', + objectId = '', + status = RequestStatus.proposed, + transactionId = null; + + PaymentRequest copyWith({ + String? id, + String? walletId, + DateTime? creation, + int? total, + String? storeId, + String? name, + String? storeNote, + String? module, + String? objectId, + RequestStatus? status, + String? transactionId, + }) { + return PaymentRequest( + id: id ?? this.id, + walletId: walletId ?? this.walletId, + creation: creation ?? this.creation, + total: total ?? this.total, + storeId: storeId ?? this.storeId, + name: name ?? this.name, + storeNote: storeNote ?? this.storeNote, + module: module ?? this.module, + objectId: objectId ?? this.objectId, + status: status ?? this.status, + transactionId: transactionId ?? this.transactionId, + ); + } +} diff --git a/lib/paiement/class/request_validation.dart b/lib/paiement/class/request_validation.dart new file mode 100644 index 0000000000..646e7ac0cf --- /dev/null +++ b/lib/paiement/class/request_validation.dart @@ -0,0 +1,47 @@ +import 'package:titan/tools/functions.dart'; + +class RequestValidationData { + final String requestId; + final String key; + final DateTime iat; + final int tot; + + RequestValidationData({ + required this.requestId, + required this.key, + required this.iat, + required this.tot, + }); + + Map toJson() => { + 'request_id': requestId, + 'key': key, + 'iat': processDateToAPI(iat), + 'tot': tot, + }; + + @override + String toString() { + return 'RequestValidationData {requestId: $requestId, key: $key, iat: $iat, tot: $tot}'; + } +} + +class RequestValidation extends RequestValidationData { + final String signature; + + RequestValidation({ + required super.requestId, + required super.key, + required super.iat, + required super.tot, + required this.signature, + }); + + @override + Map toJson() => {...super.toJson(), 'signature': signature}; + + @override + String toString() { + return 'RequestValidation {requestId: $requestId, key: $key, iat: $iat, tot: $tot, signature: $signature}'; + } +} diff --git a/lib/paiement/class/store.dart b/lib/paiement/class/store.dart index 94823fa936..f67e86768c 100644 --- a/lib/paiement/class/store.dart +++ b/lib/paiement/class/store.dart @@ -1,15 +1,37 @@ import 'package:titan/paiement/class/structure.dart'; -class Store { +class StoreSimple { final String id; final String name; final String walletId; + + StoreSimple({required this.id, required this.name, required this.walletId}); + + factory StoreSimple.fromJson(Map json) { + return StoreSimple( + id: json['id'], + name: json['name'], + walletId: json['wallet_id'], + ); + } + + Map toJson() { + return {'id': id, 'name': name, 'wallet_id': walletId}; + } + + @override + String toString() { + return 'StoreSimple(id: $id, name: $name, walletId: $walletId)'; + } +} + +class Store extends StoreSimple { final Structure structure; Store({ - required this.id, - required this.name, - required this.walletId, + required super.id, + required super.name, + required super.walletId, required this.structure, }); @@ -22,6 +44,7 @@ class Store { ); } + @override Map toJson() { return { 'id': id, diff --git a/lib/paiement/class/structure.dart b/lib/paiement/class/structure.dart index 9c1c3eefa2..d73050afe7 100644 --- a/lib/paiement/class/structure.dart +++ b/lib/paiement/class/structure.dart @@ -2,34 +2,66 @@ import 'package:titan/admin/class/association_membership_simple.dart'; import 'package:titan/user/class/simple_users.dart'; class Structure { + final String id; final String name; final AssociationMembership associationMembership; - final String id; final SimpleUser managerUser; + final String shortId; + final String siegeAddressStreet; + final String siegeAddressCity; + final String siegeAddressZipcode; + final String siegeAddressCountry; + final String? siret; + final String iban; + final String bic; Structure({ + required this.id, required this.name, required this.associationMembership, - required this.id, required this.managerUser, + required this.shortId, + required this.siegeAddressStreet, + required this.siegeAddressCity, + required this.siegeAddressZipcode, + required this.siegeAddressCountry, + this.siret, + required this.iban, + required this.bic, }); factory Structure.fromJson(Map json) { return Structure( + id: json['id'], + shortId: json['short_id'], name: json['name'], + siegeAddressStreet: json['siege_address_street'], + siegeAddressCity: json['siege_address_city'], + siegeAddressZipcode: json['siege_address_zipcode'], + siegeAddressCountry: json['siege_address_country'], + siret: json['siret'], + iban: json['iban'], + bic: json['bic'], associationMembership: json['association_membership'] != null ? AssociationMembership.fromJson(json['association_membership']) : AssociationMembership.empty(), - id: json['id'], managerUser: SimpleUser.fromJson(json['manager_user']), ); } Map toJson() { return { - 'name': name, 'id': id, + 'name': name, + 'short_id': shortId, 'manager_user_id': managerUser.id, + 'siege_address_street': siegeAddressStreet, + 'siege_address_city': siegeAddressCity, + 'siege_address_zipcode': siegeAddressZipcode, + 'siege_address_country': siegeAddressCountry, + 'siret': siret, + 'iban': iban, + 'bic': bic, 'association_membership_id': associationMembership.id != '' ? associationMembership.id : null, @@ -38,29 +70,64 @@ class Structure { @override String toString() { - return 'Structure{name: $name, associationMembership: $associationMembership, id: $id, managerUserId: $managerUser}'; + return 'Structure{id: $id\n' + 'name: $name\n' + 'shortId: $shortId\n' + 'siegeAddressStreet: $siegeAddressStreet\n' + 'siegeAddressCity: $siegeAddressCity\n' + 'siegeAddressZipcode: $siegeAddressZipcode\n' + 'siegeAddressCountry: $siegeAddressCountry\n' + 'siret: $siret\n' + 'iban: $iban\n' + 'bic: $bic\n' + 'associationMembership: $associationMembership\n' + 'managerUser: $managerUser}'; } Structure copyWith({ + String? id, + String? shortId, String? name, AssociationMembership? associationMembership, - String? id, SimpleUser? managerUser, + String? siegeAddressStreet, + String? siegeAddressCity, + String? siegeAddressZipcode, + String? siegeAddressCountry, + String? siret, + String? iban, + String? bic, }) { return Structure( + id: id ?? this.id, + shortId: shortId ?? this.shortId, name: name ?? this.name, + siegeAddressStreet: siegeAddressStreet ?? this.siegeAddressStreet, + siegeAddressCity: siegeAddressCity ?? this.siegeAddressCity, + siegeAddressZipcode: siegeAddressZipcode ?? this.siegeAddressZipcode, + siegeAddressCountry: siegeAddressCountry ?? this.siegeAddressCountry, + siret: siret ?? this.siret, + iban: iban ?? this.iban, + bic: bic ?? this.bic, associationMembership: associationMembership ?? this.associationMembership, - id: id ?? this.id, managerUser: managerUser ?? this.managerUser, ); } Structure.empty() : this( + id: '', + shortId: '', name: '', + siegeAddressStreet: '', + siegeAddressCity: '', + siegeAddressZipcode: '', + siegeAddressCountry: '', + siret: null, + iban: '', + bic: '', associationMembership: AssociationMembership.empty(), - id: '', managerUser: SimpleUser.empty(), ); } diff --git a/lib/paiement/providers/bank_account_holder_provider.dart b/lib/paiement/providers/bank_account_holder_provider.dart new file mode 100644 index 0000000000..62240d4735 --- /dev/null +++ b/lib/paiement/providers/bank_account_holder_provider.dart @@ -0,0 +1,33 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/paiement/class/structure.dart'; +import 'package:titan/paiement/repositories/bank_account_holder_repository.dart'; +import 'package:titan/tools/providers/single_notifier.dart'; + +class BankAccountHolderNotifier extends SingleNotifier { + final BankAccountHolderRepository bankAccountHolderRepository; + BankAccountHolderNotifier({required this.bankAccountHolderRepository}) + : super(const AsyncValue.loading()); + + Future> getBankAccountHolder() async { + return await load(bankAccountHolderRepository.getBankAccountHolder); + } + + Future updateBankAccountHolder(Structure structure) async { + return await add( + (_) => bankAccountHolderRepository.updateBankAccountHolder(structure), + structure, + ); + } +} + +final bankAccountHolderProvider = + StateNotifierProvider>(( + ref, + ) { + final bankAccountHolderRepository = ref.watch( + bankAccountHolderRepositoryProvider, + ); + return BankAccountHolderNotifier( + bankAccountHolderRepository: bankAccountHolderRepository, + )..getBankAccountHolder(); + }); diff --git a/lib/paiement/providers/device_provider.dart b/lib/paiement/providers/device_provider.dart index 718bb3ce83..c18a8bd02c 100644 --- a/lib/paiement/providers/device_provider.dart +++ b/lib/paiement/providers/device_provider.dart @@ -4,9 +4,9 @@ import 'package:titan/paiement/class/wallet_device.dart'; import 'package:titan/paiement/repositories/devices_repository.dart'; import 'package:titan/tools/providers/single_notifier.dart'; -class DeviceListNotifier extends SingleNotifier { +class DeviceNotifier extends SingleNotifier { final DevicesRepository devicesRepository; - DeviceListNotifier({required this.devicesRepository}) + DeviceNotifier({required this.devicesRepository}) : super(const AsyncValue.loading()); Future> getDevice(String deviceId) async { @@ -26,7 +26,7 @@ class DeviceListNotifier extends SingleNotifier { } final deviceProvider = - StateNotifierProvider>((ref) { + StateNotifierProvider>((ref) { final deviceListRepository = ref.watch(devicesRepositoryProvider); - return DeviceListNotifier(devicesRepository: deviceListRepository); + return DeviceNotifier(devicesRepository: deviceListRepository); }); diff --git a/lib/paiement/providers/invoice_list_provider.dart b/lib/paiement/providers/invoice_list_provider.dart new file mode 100644 index 0000000000..391b2f1a72 --- /dev/null +++ b/lib/paiement/providers/invoice_list_provider.dart @@ -0,0 +1,90 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/paiement/class/invoice.dart'; +import 'package:titan/paiement/class/structure.dart'; +import 'package:titan/paiement/repositories/invoices_repository.dart'; +import 'package:titan/tools/providers/list_notifier.dart'; + +class InvoiceListNotifier extends ListNotifier { + final InvoiceRepository invoicesRepository; + InvoiceListNotifier({required this.invoicesRepository}) + : super(const AsyncValue.loading()); + + Future>> getInvoices({ + int page = 1, + int pageLimit = 20, + List? structuresIds, + DateTime? startDate, + DateTime? endDate, + }) async { + return await loadList( + () async => invoicesRepository.getInvoices( + page, + pageLimit, + structuresIds, + startDate, + endDate, + ), + ); + } + + Future>> getStructureInvoices( + String structuresId, { + int page = 1, + int pageLimit = 20, + DateTime? startDate, + DateTime? endDate, + }) async { + return await loadList( + () async => invoicesRepository.getStructureInvoices( + structuresId, + page, + pageLimit, + startDate, + endDate, + ), + ); + } + + Future createInvoice(Structure structure) async { + return await add( + (_) => invoicesRepository.createInvoice(structure.id), + Invoice.empty(), + ); + } + + Future updateInvoicePaidStatus(Invoice invoice, bool paid) async { + return await update( + (_) => invoicesRepository.updateInvoicePaidStatus(invoice.id, paid), + (invoices, invoice) => + invoices..[invoices.indexWhere((s) => s.id == invoice.id)] = invoice, + invoice.copyWith(paid: paid), + ); + } + + Future updateInvoiceReceivedStatus(Invoice invoice, bool paid) async { + return await update( + (_) => invoicesRepository.updateInvoiceReceivedStatus(invoice.id), + (invoices, invoice) => + invoices..[invoices.indexWhere((s) => s.id == invoice.id)] = invoice, + invoice.copyWith(received: true), + ); + } + + Future deleteInvoice(Invoice invoice) async { + return await delete( + invoicesRepository.deleteInvoice, + (invoices, invoice) => invoices..remove(invoice), + invoice.id, + invoice, + ); + } +} + +final invoiceListProvider = + StateNotifierProvider>>(( + ref, + ) { + final invoicesRepository = ref.watch(invoiceRepositoryProvider); + return InvoiceListNotifier(invoicesRepository: invoicesRepository) + ..getInvoices(); + }); diff --git a/lib/paiement/providers/invoice_pdf_provider.dart b/lib/paiement/providers/invoice_pdf_provider.dart new file mode 100644 index 0000000000..09af8adc58 --- /dev/null +++ b/lib/paiement/providers/invoice_pdf_provider.dart @@ -0,0 +1,19 @@ +import 'dart:typed_data'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/paiement/repositories/invoice_pdf_repository.dart'; + +class InvoicePdfNotifier extends FamilyAsyncNotifier { + @override + Future build(String arg) async { + final InvoicePdfRepository invoicePdfRepository = ref.watch( + invoicePdfRepositoryProvider, + ); + return await invoicePdfRepository.getInvoicePdf(arg); + } +} + +final invoicePdfProvider = + AsyncNotifierProvider.family( + InvoicePdfNotifier.new, + ); diff --git a/lib/paiement/providers/invoice_provider.dart b/lib/paiement/providers/invoice_provider.dart new file mode 100644 index 0000000000..33921ba96e --- /dev/null +++ b/lib/paiement/providers/invoice_provider.dart @@ -0,0 +1,14 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/paiement/class/invoice.dart'; + +class InvoiceNotifier extends StateNotifier { + InvoiceNotifier() : super(Invoice.empty()); + + void setInvoice(Invoice invoice) { + state = invoice; + } + + void clearInvoice() { + state = Invoice.empty(); + } +} diff --git a/lib/paiement/providers/is_payment_admin.dart b/lib/paiement/providers/is_payment_admin.dart index 4fdca3f91d..32ef97b276 100644 --- a/lib/paiement/providers/is_payment_admin.dart +++ b/lib/paiement/providers/is_payment_admin.dart @@ -1,7 +1,21 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/paiement/providers/bank_account_holder_provider.dart'; import 'package:titan/paiement/providers/my_structures_provider.dart'; -final isPaymentAdminProvider = StateProvider((ref) { +final isStructureAdminProvider = StateProvider((ref) { final myStructures = ref.watch(myStructuresProvider); return myStructures.isNotEmpty; }); + +final isBankAccountHolderProvider = Provider((ref) { + final bankAccountHolder = ref.watch(bankAccountHolderProvider); + final myStructures = ref.watch(myStructuresProvider); + return bankAccountHolder.maybeWhen( + data: (bankAccountHolder) { + return myStructures.any( + (structure) => structure.id == bankAccountHolder.id, + ); + }, + orElse: () => false, + ); +}); diff --git a/lib/paiement/providers/last_used_store_id_provider.dart b/lib/paiement/providers/last_used_store_id_provider.dart new file mode 100644 index 0000000000..06d3e6e207 --- /dev/null +++ b/lib/paiement/providers/last_used_store_id_provider.dart @@ -0,0 +1,29 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:async'; + +class LastUsedStoreIdNotifier extends StateNotifier { + final key = 'last_used_store'; + LastUsedStoreIdNotifier() : super(''); + + Future loadLastUsedStoreId() async { + final prefs = await SharedPreferences.getInstance(); + final savedStoreId = prefs.getString(key); + + if (savedStoreId != null) { + state = savedStoreId; + } + } + + Future saveLastUsedStoreIdToSharedPreferences(String storeId) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(key, storeId); + } +} + +final lastUsedStoreIdProvider = + StateNotifierProvider((ref) { + final lastUsedStoreIdNotifier = LastUsedStoreIdNotifier(); + lastUsedStoreIdNotifier.loadLastUsedStoreId(); + return lastUsedStoreIdNotifier; + }); diff --git a/lib/paiement/providers/my_structures_provider.dart b/lib/paiement/providers/my_structures_provider.dart index d3b7b4c49d..4d6a478061 100644 --- a/lib/paiement/providers/my_structures_provider.dart +++ b/lib/paiement/providers/my_structures_provider.dart @@ -1,4 +1,5 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/paiement/class/structure.dart'; import 'package:titan/paiement/providers/structure_list_provider.dart'; import 'package:titan/user/providers/user_provider.dart'; @@ -9,7 +10,7 @@ final myStructuresProvider = StateProvider((ref) { data: (structures) => structures .where((structure) => structure.managerUser.id == user.id) .toList(), - loading: () => [], - error: (error, stack) => [], + loading: () => List.empty(), + error: (error, stack) => List.empty(), ); }); diff --git a/lib/paiement/providers/payment_requests_provider.dart b/lib/paiement/providers/payment_requests_provider.dart new file mode 100644 index 0000000000..c214344187 --- /dev/null +++ b/lib/paiement/providers/payment_requests_provider.dart @@ -0,0 +1,46 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/paiement/class/payment_request.dart'; +import 'package:titan/paiement/class/request_validation.dart'; +import 'package:titan/paiement/repositories/requests_repository.dart'; +import 'package:titan/tools/providers/list_notifier.dart'; + +class PaymentRequestsNotifier extends ListNotifier { + final RequestsRepository requestsRepository; + PaymentRequestsNotifier({required this.requestsRepository}) + : super(const AsyncValue.loading()); + + Future>> getRequests() async { + return await loadList(requestsRepository.getRequests); + } + + Future acceptRequest( + PaymentRequest request, + RequestValidation validation, + ) async { + return await update( + (_) => requestsRepository.acceptRequest(request.id, validation), + (requests, request) => + requests..[requests.indexWhere((r) => r.id == request.id)] = request, + request.copyWith(status: RequestStatus.accepted), + ); + } + + Future refuseRequest(PaymentRequest request) async { + return await update( + (_) => requestsRepository.refuseRequest(request.id), + (requests, request) => + requests..[requests.indexWhere((r) => r.id == request.id)] = request, + request.copyWith(status: RequestStatus.refused), + ); + } +} + +final paymentRequestsProvider = + StateNotifierProvider< + PaymentRequestsNotifier, + AsyncValue> + >((ref) { + final requestsRepository = ref.watch(requestsRepositoryProvider); + return PaymentRequestsNotifier(requestsRepository: requestsRepository) + ..getRequests(); + }); diff --git a/lib/paiement/providers/selected_store_provider.dart b/lib/paiement/providers/selected_store_provider.dart index 1f93f1778b..43c6cdb59a 100644 --- a/lib/paiement/providers/selected_store_provider.dart +++ b/lib/paiement/providers/selected_store_provider.dart @@ -1,26 +1,36 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/paiement/class/user_store.dart'; +import 'package:titan/paiement/providers/last_used_store_id_provider.dart'; import 'package:titan/paiement/providers/my_stores_provider.dart'; class SelectedStoreNotifier extends StateNotifier { - SelectedStoreNotifier(super.store); + final LastUsedStoreIdNotifier lastUsedStoreIdNotifier; + SelectedStoreNotifier(this.lastUsedStoreIdNotifier, super.store); void updateStore(UserStore store) { state = store; + lastUsedStoreIdNotifier.saveLastUsedStoreIdToSharedPreferences(store.id); } } final selectedStoreProvider = StateNotifierProvider((ref) { final myStores = ref.watch(myStoresProvider); + final lastUsedStoreId = ref.read(lastUsedStoreIdProvider); + final lastUsedStoreIdNotifier = ref.read( + lastUsedStoreIdProvider.notifier, + ); final store = myStores.maybeWhen( orElse: () => UserStore.empty(), data: (value) { if (value.isEmpty) { return UserStore.empty(); } - return value.first; + return value.firstWhere( + (store) => store.id == lastUsedStoreId, + orElse: () => value.first, + ); }, ); - return SelectedStoreNotifier(store); + return SelectedStoreNotifier(lastUsedStoreIdNotifier, store); }); diff --git a/lib/paiement/repositories/bank_account_holder_repository.dart b/lib/paiement/repositories/bank_account_holder_repository.dart new file mode 100644 index 0000000000..1c69ed3e52 --- /dev/null +++ b/lib/paiement/repositories/bank_account_holder_repository.dart @@ -0,0 +1,34 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/auth/providers/openid_provider.dart'; +import 'package:titan/paiement/class/structure.dart'; +import 'package:titan/tools/exception.dart'; +import 'package:titan/tools/repository/repository.dart'; + +class BankAccountHolderRepository extends Repository { + @override + // ignore: overridden_fields + final ext = 'mypayment/bank-account-holder'; + + Future getBankAccountHolder() async { + try { + return Structure.fromJson(await getOne("")); + } on AppException catch (e) { + if (e.type == ErrorType.tokenExpire) rethrow; + return Structure.empty(); + } catch (e) { + return Structure.empty(); + } + } + + Future updateBankAccountHolder(Structure structure) async { + return Structure.fromJson( + await create({'holder_structure_id': structure.id}), + ); + } +} + +final bankAccountHolderRepositoryProvider = + Provider((ref) { + final token = ref.watch(tokenProvider); + return BankAccountHolderRepository()..setToken(token); + }); diff --git a/lib/paiement/repositories/devices_repository.dart b/lib/paiement/repositories/devices_repository.dart index a92ee5598d..977879a41f 100644 --- a/lib/paiement/repositories/devices_repository.dart +++ b/lib/paiement/repositories/devices_repository.dart @@ -7,7 +7,7 @@ import 'package:titan/tools/repository/repository.dart'; class DevicesRepository extends Repository { @override // ignore: overridden_fields - final ext = 'myeclpay/users/me/wallet/devices'; + final ext = 'mypayment/users/me/wallet/devices'; Future registerDevice(CreateDevice body) async { return WalletDevice.fromJson(await create(body.toJson())); diff --git a/lib/paiement/repositories/funding_repository.dart b/lib/paiement/repositories/funding_repository.dart index aabe1d8dca..94011224a5 100644 --- a/lib/paiement/repositories/funding_repository.dart +++ b/lib/paiement/repositories/funding_repository.dart @@ -8,7 +8,7 @@ import 'package:titan/tools/repository/repository.dart'; class FundingRepository extends Repository { @override // ignore: overridden_fields - final ext = 'myeclpay/transfer/'; + final ext = 'mypayment/transfer/'; Future getAdminPaymentUrl(Transfer transfer) async { return await create(transfer.toJson(), suffix: "admin"); diff --git a/lib/paiement/repositories/invoice_pdf_repository.dart b/lib/paiement/repositories/invoice_pdf_repository.dart new file mode 100644 index 0000000000..afb36f7e7a --- /dev/null +++ b/lib/paiement/repositories/invoice_pdf_repository.dart @@ -0,0 +1,20 @@ +import 'dart:typed_data'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/auth/providers/openid_provider.dart'; +import 'package:titan/tools/repository/pdf_repository.dart'; + +class InvoicePdfRepository extends PdfRepository { + @override + // ignore: overridden_fields + final String ext = "mypayment/invoices/"; + + Future getInvoicePdf(String invoiceId) async { + return await getPdf(invoiceId); + } +} + +final invoicePdfRepositoryProvider = Provider((ref) { + final token = ref.watch(tokenProvider); + return InvoicePdfRepository()..setToken(token); +}); diff --git a/lib/paiement/repositories/invoices_repository.dart b/lib/paiement/repositories/invoices_repository.dart new file mode 100644 index 0000000000..4197cbed2a --- /dev/null +++ b/lib/paiement/repositories/invoices_repository.dart @@ -0,0 +1,87 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/auth/providers/openid_provider.dart'; +import 'package:titan/paiement/class/invoice.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/repository/repository.dart'; + +String formatQuery( + int? page, + int? pageSize, + List? structuresIds, + DateTime? startDate, + DateTime? endDate, +) { + final queryParams = []; + if (page != null) queryParams.add('page=$page'); + if (pageSize != null) queryParams.add('page_size=$pageSize'); + if (structuresIds != null) { + for (final id in structuresIds) { + queryParams.add('structures_ids=$id'); + } + } + if (startDate != null) { + queryParams.add('start_date=${processDateToAPI(startDate)}'); + } + if (endDate != null) { + queryParams.add('end_date=${processDateToAPI(endDate)}'); + } + return queryParams.isNotEmpty ? '?${queryParams.join('&')}' : ''; +} + +class InvoiceRepository extends Repository { + @override + // ignore: overridden_fields + final ext = 'mypayment/invoices'; + + Future> getInvoices( + int page, + int pageSize, + List? structuresIds, + DateTime? startDate, + DateTime? endDate, + ) async { + return List.from( + (await getList( + suffix: formatQuery(page, pageSize, structuresIds, startDate, endDate), + )).map((e) => Invoice.fromJson(e)), + ); + } + + Future> getStructureInvoices( + String structureId, + int page, + int pageSize, + DateTime? startDate, + DateTime? endDate, + ) async { + return List.from( + (await getList( + suffix: + "/structures/$structureId${formatQuery(page, pageSize, null, startDate, endDate)}", + )).map((e) => Invoice.fromJson(e)), + ); + } + + Future createInvoice(String structureId) async { + return Invoice.fromJson( + await create(null, suffix: "/structures/$structureId"), + ); + } + + Future updateInvoicePaidStatus(String invoiceId, bool paid) async { + return await update(null, "/$invoiceId/paid?paid=$paid"); + } + + Future updateInvoiceReceivedStatus(String invoiceId) async { + return await update(null, "/$invoiceId/received"); + } + + Future deleteInvoice(String invoiceId) async { + return await delete("/$invoiceId"); + } +} + +final invoiceRepositoryProvider = Provider((ref) { + final token = ref.watch(tokenProvider); + return InvoiceRepository()..setToken(token); +}); diff --git a/lib/paiement/repositories/requests_repository.dart b/lib/paiement/repositories/requests_repository.dart new file mode 100644 index 0000000000..812cbcade0 --- /dev/null +++ b/lib/paiement/repositories/requests_repository.dart @@ -0,0 +1,37 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/auth/providers/openid_provider.dart'; +import 'package:titan/paiement/class/payment_request.dart'; +import 'package:titan/paiement/class/request_validation.dart'; +import 'package:titan/tools/repository/repository.dart'; + +class RequestsRepository extends Repository { + @override + // ignore: overridden_fields + final ext = 'mypayment/'; + + Future> getRequests() async { + return List.from( + (await getList( + suffix: 'requests', + )).map((e) => PaymentRequest.fromJson(e)), + ); + } + + Future acceptRequest( + String requestId, + RequestValidation validation, + ) async { + await create(validation.toJson(), suffix: 'requests/$requestId/accept'); + return true; + } + + Future refuseRequest(String requestId) async { + await create({}, suffix: 'requests/$requestId/refuse'); + return true; + } +} + +final requestsRepositoryProvider = Provider((ref) { + final token = ref.watch(tokenProvider); + return RequestsRepository()..setToken(token); +}); diff --git a/lib/paiement/repositories/store_sellers_repository.dart b/lib/paiement/repositories/store_sellers_repository.dart index 0e8ba4bf44..810cf139e5 100644 --- a/lib/paiement/repositories/store_sellers_repository.dart +++ b/lib/paiement/repositories/store_sellers_repository.dart @@ -6,7 +6,7 @@ import 'package:titan/tools/repository/repository.dart'; class SellerStoreRepository extends Repository { @override // ignore: overridden_fields - final ext = 'myeclpay/stores'; + final ext = 'mypayment/stores'; Future createSeller(String storeId, Seller seller) async { return Seller.fromJson( diff --git a/lib/paiement/repositories/stores_repository.dart b/lib/paiement/repositories/stores_repository.dart index ec1a48b57f..7fa3f50b58 100644 --- a/lib/paiement/repositories/stores_repository.dart +++ b/lib/paiement/repositories/stores_repository.dart @@ -10,7 +10,7 @@ import 'package:titan/tools/repository/repository.dart'; class StoresRepository extends Repository { @override // ignore: overridden_fields - final ext = 'myeclpay/stores'; + final ext = 'mypayment/stores'; Future updateStore(Store store) async { return await update(store.toJson(), "/${store.id}"); diff --git a/lib/paiement/repositories/structures_repository.dart b/lib/paiement/repositories/structures_repository.dart index 4fec46b3e0..45ff5fc36c 100644 --- a/lib/paiement/repositories/structures_repository.dart +++ b/lib/paiement/repositories/structures_repository.dart @@ -7,7 +7,7 @@ import 'package:titan/tools/repository/repository.dart'; class StructuresRepository extends Repository { @override // ignore: overridden_fields - final ext = 'myeclpay/structures'; + final ext = 'mypayment/structures'; Future createStructure(Structure structure) async { return Structure.fromJson(await create(structure.toJson())); diff --git a/lib/paiement/repositories/tos_repository.dart b/lib/paiement/repositories/tos_repository.dart index b90379c14c..64f3185d07 100644 --- a/lib/paiement/repositories/tos_repository.dart +++ b/lib/paiement/repositories/tos_repository.dart @@ -6,7 +6,7 @@ import 'package:titan/tools/repository/repository.dart'; class TosRepository extends Repository { @override // ignore: overridden_fields - final ext = 'myeclpay/users/me/'; + final ext = 'mypayment/users/me/'; Future getTOS() async { return TOS.fromJson(await getOne("tos")); diff --git a/lib/paiement/repositories/transaction_repository.dart b/lib/paiement/repositories/transaction_repository.dart index b1c8dd82c0..f6d96906fd 100644 --- a/lib/paiement/repositories/transaction_repository.dart +++ b/lib/paiement/repositories/transaction_repository.dart @@ -6,7 +6,7 @@ import 'package:titan/tools/repository/repository.dart'; class TransactionsRepository extends Repository { @override // ignore: overridden_fields - final ext = 'myeclpay/transactions'; + final ext = 'mypayment/transactions'; Future refundTransaction(String transactionId, Refund refund) async { return await create(refund.toJson(), suffix: '/$transactionId/refund'); diff --git a/lib/paiement/repositories/users_me_repository.dart b/lib/paiement/repositories/users_me_repository.dart index 9c6d82e95b..e33b64b005 100644 --- a/lib/paiement/repositories/users_me_repository.dart +++ b/lib/paiement/repositories/users_me_repository.dart @@ -8,7 +8,7 @@ import 'package:titan/tools/repository/repository.dart'; class UsersMeRepository extends Repository { @override // ignore: overridden_fields - final ext = 'myeclpay/users/me/'; + final ext = 'mypayment/users/me/'; Future register() async { return await create({}, suffix: 'register'); diff --git a/lib/paiement/router.dart b/lib/paiement/router.dart index c4d106a805..4895966a5a 100644 --- a/lib/paiement/router.dart +++ b/lib/paiement/router.dart @@ -1,12 +1,16 @@ -import 'package:either_dart/either.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:titan/drawer/class/module.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/class/module.dart'; import 'package:titan/paiement/providers/is_payment_admin.dart'; -import 'package:titan/paiement/ui/pages/admin_page/admin_page.dart' - deferred as admin_page; +import 'package:titan/paiement/ui/pages/structure_admin_page/structure_admin_page.dart' + deferred as structure_stores_page; import 'package:titan/paiement/ui/pages/fund_page/web_view_modal.dart' deferred as fund_page; +import 'package:titan/paiement/ui/pages/invoices_admin_page/invoices_admin_page.dart' + deferred as invoices_admin_page; +import 'package:titan/paiement/ui/pages/invoices_structure_page/invoices_structure_page.dart' + deferred as structure_invoices_page; import 'package:titan/paiement/ui/pages/store_pages/add_edit_store.dart' deferred as add_edit_page; import 'package:titan/paiement/ui/pages/store_admin_page/store_admin_page.dart' @@ -21,6 +25,7 @@ import 'package:titan/paiement/ui/pages/store_stats_page/store_stats_page.dart' deferred as store_stats_page; import 'package:titan/paiement/ui/pages/transfer_structure_page/transfer_structure_page.dart' deferred as transfer_structure_page; +import 'package:titan/tools/functions.dart'; import 'package:titan/tools/middlewares/admin_middleware.dart'; import 'package:titan/tools/middlewares/authenticated_middleware.dart'; import 'package:titan/tools/middlewares/deferred_middleware.dart'; @@ -31,17 +36,19 @@ class PaymentRouter { static const String root = '/payment'; static const String stats = '/stats'; static const String devices = '/devices'; - static const String admin = '/admin'; + static const String structureStores = '/structureStores'; + static const String invoicesAdmin = '/invoicesAdmin'; + static const String invoicesStructure = '/invoicesStructure'; static const String fund = '/fund'; static const String addEditStore = '/addEditStore'; static const String transferStructure = '/transferStructure'; static const String storeAdmin = '/storeAdmin'; static const String storeStats = '/storeStats'; static final Module module = Module( - name: "MyECLPay", - icon: const Left(HeroIcons.creditCard), + getName: (context) => getPaymentName(), + getDescription: (context) => + AppLocalizations.of(context)!.modulePaymentDescription, root: PaymentRouter.root, - selected: false, ); PaymentRouter(this.ref); @@ -53,6 +60,10 @@ class PaymentRouter { AuthenticatedMiddleware(ref), DeferredLoadingMiddleware(main_page.loadLibrary), ], + pageType: QCustomPage( + transitionsBuilder: (_, animation, _, child) => + FadeTransition(opacity: animation, child: child), + ), children: [ QRoute( path: PaymentRouter.stats, @@ -70,11 +81,27 @@ class PaymentRouter { middleware: [DeferredLoadingMiddleware(store_admin_page.loadLibrary)], ), QRoute( - path: PaymentRouter.admin, - builder: () => admin_page.AdminPage(), + path: PaymentRouter.invoicesAdmin, + builder: () => invoices_admin_page.InvoicesAdminPage(), middleware: [ - DeferredLoadingMiddleware(admin_page.loadLibrary), - AdminMiddleware(ref, isPaymentAdminProvider), + DeferredLoadingMiddleware(invoices_admin_page.loadLibrary), + AdminMiddleware(ref, isBankAccountHolderProvider), + ], + ), + QRoute( + path: PaymentRouter.invoicesStructure, + builder: () => structure_invoices_page.StructureInvoicesPage(), + middleware: [ + DeferredLoadingMiddleware(structure_invoices_page.loadLibrary), + AdminMiddleware(ref, isStructureAdminProvider), + ], + ), + QRoute( + path: PaymentRouter.structureStores, + builder: () => structure_stores_page.StructureStoresPage(), + middleware: [ + DeferredLoadingMiddleware(structure_stores_page.loadLibrary), + AdminMiddleware(ref, isStructureAdminProvider), ], children: [ QRoute( diff --git a/lib/paiement/tools/constants.dart b/lib/paiement/tools/constants.dart deleted file mode 100644 index 8fa14620d3..0000000000 --- a/lib/paiement/tools/constants.dart +++ /dev/null @@ -1,3 +0,0 @@ -class PaiementTextConstants { - static const String paiement = "Paiement"; -} diff --git a/lib/paiement/tools/functions.dart b/lib/paiement/tools/functions.dart index 652db1fb66..d5dc5925bf 100644 --- a/lib/paiement/tools/functions.dart +++ b/lib/paiement/tools/functions.dart @@ -10,24 +10,6 @@ import 'package:titan/paiement/tools/key_service.dart'; enum TransferType { helloAsso, check, cash, bankTransfer } -String getMonth(int m) { - final months = [ - "Décembre", - "Janvier", - "Février", - "Mars", - "Avril", - "Mai", - "Juin", - "Juillet", - "Août", - "Septembre", - "Octobre", - "Novembre", - ]; - return months[m]; -} - Widget getStatusTag(WalletDeviceStatus status) { switch (status) { case WalletDeviceStatus.active: diff --git a/lib/paiement/tools/key_service.dart b/lib/paiement/tools/key_service.dart index 2f07d4bd4d..11a247dbfd 100644 --- a/lib/paiement/tools/key_service.dart +++ b/lib/paiement/tools/key_service.dart @@ -1,12 +1,14 @@ +import 'dart:convert'; import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:titan/tools/functions.dart'; class KeyService { - final FlutterSecureStorage _secureStorage = const FlutterSecureStorage( - aOptions: AndroidOptions(encryptedSharedPreferences: true), + final FlutterSecureStorage _secureStorage = FlutterSecureStorage( + aOptions: AndroidOptions(), iOptions: IOSOptions( // A service name is required for iOS KeyChain - accountName: 'fr.titan.myecl', + accountName: getTitanPackageName(), ), ); final algorithm = Ed25519(); @@ -20,12 +22,13 @@ class KeyService { final publicKey = await keyPair.extractPublicKey(); await _secureStorage.write( key: 'privateKey', - value: String.fromCharCodes(privateKey), + value: base64.encode(privateKey), ); await _secureStorage.write( key: 'publicKey', - value: String.fromCharCodes(publicKey.bytes), + value: base64.encode(publicKey.bytes), ); + await _secureStorage.write(key: 'migrated', value: 'true'); } Future saveKeyId(String keyId) async { @@ -38,8 +41,28 @@ class KeyService { if (privateKeyString == null || publicKeyString == null) { return null; } - final privateKey = privateKeyString.codeUnits; - final publicKey = publicKeyString.codeUnits; + final migrated = await _secureStorage.read(key: 'migrated'); + if (migrated == null) { + final privateKey = privateKeyString.codeUnits; + final publicKey = publicKeyString.codeUnits; + + await _secureStorage.write( + key: 'privateKey', + value: base64.encode(privateKey), + ); + await _secureStorage.write( + key: 'publicKey', + value: base64.encode(publicKey), + ); + await _secureStorage.write(key: 'migrated', value: 'true'); + return SimpleKeyPairData( + privateKey, + publicKey: SimplePublicKey(publicKey, type: KeyPairType.ed25519), + type: KeyPairType.ed25519, + ); + } + final privateKey = base64.decode(privateKeyString); + final publicKey = base64.decode(publicKeyString); return SimpleKeyPairData( privateKey, publicKey: SimplePublicKey(publicKey, type: KeyPairType.ed25519), diff --git a/lib/paiement/ui/components/paiment_delegate/confirm_button.dart b/lib/paiement/ui/components/paiment_delegate/confirm_button.dart new file mode 100644 index 0000000000..335e5985db --- /dev/null +++ b/lib/paiement/ui/components/paiment_delegate/confirm_button.dart @@ -0,0 +1,143 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; + +const _teal = Color(0xff017f80); +const _tealDark = Color.fromARGB(255, 4, 84, 84); + +class ConfirmButton extends HookWidget { + final VoidCallback onConfirm; + final VoidCallback onCancel; + final int? totalSeconds; + + const ConfirmButton({ + super.key, + required this.onConfirm, + required this.onCancel, + required this.totalSeconds, + }); + + @override + Widget build(BuildContext context) { + final localizeWithContext = AppLocalizations.of(context)!; + final isExpired = useState(false); + + // Expiration timer: runs once over totalSeconds + final expirationController = useAnimationController( + duration: Duration(seconds: totalSeconds ?? 0), + ); + + // Shimmer: repeating fast sweep (1.5s per cycle) + final shimmerController = useAnimationController( + duration: const Duration(milliseconds: 1500), + ); + + useEffect(() { + if (totalSeconds != null && totalSeconds! > 0) { + expirationController.forward(); + } + void listener(AnimationStatus status) { + if (status == AnimationStatus.completed) { + isExpired.value = true; + shimmerController.stop(); + } + } + + expirationController.addStatusListener(listener); + shimmerController.repeat(); + return () { + expirationController.removeStatusListener(listener); + }; + }, [expirationController, shimmerController]); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: isExpired.value ? null : onConfirm, + child: AnimatedBuilder( + animation: shimmerController, + builder: (context, child) { + return Stack( + children: [ + Center( + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 14, + ), + decoration: isExpired.value + ? BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.circular(8), + ) + : BoxDecoration( + gradient: const LinearGradient( + colors: [_teal, _tealDark], + ), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + localizeWithContext.paiementConfirmPayment, + textAlign: TextAlign.center, + style: TextStyle( + color: ColorConstants.background, + fontSize: 18, + fontWeight: FontWeight.w900, + ), + ), + ), + ), + if (!isExpired.value) + Positioned.fill( + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: LayoutBuilder( + builder: (context, constraints) { + final shimmerWidth = constraints.maxWidth * 0.9; + final startPosition = -shimmerWidth * 1.5; + final endPosition = constraints.maxWidth; + final totalDistance = endPosition - startPosition; + + return Transform.translate( + offset: Offset( + startPosition + + (shimmerController.value * totalDistance), + 0, + ), + child: Container( + width: shimmerWidth, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.white.withOpacity(0.0), + Colors.white.withOpacity(0.2), + Colors.white.withOpacity(0.0), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + ), + ); + }, + ), + ), + ), + ], + ); + }, + ), + ), + const SizedBox(height: 10), + Button.secondary( + text: localizeWithContext.paiementCancel, + onPressed: onCancel, + ), + ], + ); + } +} diff --git a/lib/paiement/ui/components/paiment_delegate/countdown_timer.dart b/lib/paiement/ui/components/paiment_delegate/countdown_timer.dart new file mode 100644 index 0000000000..c63ef9f46f --- /dev/null +++ b/lib/paiement/ui/components/paiment_delegate/countdown_timer.dart @@ -0,0 +1,125 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:titan/l10n/app_localizations.dart'; + +class CountdownTimer extends HookWidget { + final int totalSeconds; + final VoidCallback? onFinished; + + const CountdownTimer({ + super.key, + required this.totalSeconds, + this.onFinished, + }); + + @override + Widget build(BuildContext context) { + final localizeWithContext = AppLocalizations.of(context)!; + final controller = useAnimationController( + duration: Duration(seconds: totalSeconds), + ); + + useEffect(() { + controller.forward(); + void listener(AnimationStatus status) { + if (status == AnimationStatus.completed) { + onFinished?.call(); + } + } + + controller.addStatusListener(listener); + return () => controller.removeStatusListener(listener); + }, [controller]); + + final progress = useAnimation(controller); + final remainingSeconds = (totalSeconds * (1 - progress)).round(); + final minutes = (remainingSeconds ~/ 60).toString().padLeft(2, '0'); + final seconds = (remainingSeconds % 60).toString().padLeft(2, '0'); + + final colorScheme = Theme.of(context).colorScheme; + final urgencyRatio = progress; + final progressColor = Color.lerp( + const Color(0xff017f80), + colorScheme.error, + urgencyRatio, + )!; + final backgroundColor = progressColor.withOpacity(0.1); + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: progressColor.withOpacity(0.3), width: 2), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Stack( + alignment: Alignment.center, + children: [ + SizedBox( + width: 80, + height: 80, + child: CircularProgressIndicator( + value: 1 - progress, + strokeWidth: 6, + backgroundColor: progressColor.withOpacity(0.2), + valueColor: AlwaysStoppedAnimation(progressColor), + strokeCap: StrokeCap.round, + ), + ), + AnimatedScale( + scale: remainingSeconds <= 10 ? 1.1 : 1.0, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '$minutes:$seconds', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: progressColor, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ], + ), + ), + ], + ), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + localizeWithContext.paiementTimeRemaining, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: progressColor.withOpacity(0.7), + ), + ), + const SizedBox(height: 4), + Text( + remainingSeconds <= 30 + ? localizeWithContext.paiementHurryUp + : localizeWithContext.paiementCompletePayment, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: progressColor, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/paiement/ui/components/paiment_delegate/feedback_overlay.dart b/lib/paiement/ui/components/paiment_delegate/feedback_overlay.dart new file mode 100644 index 0000000000..6e740cf456 --- /dev/null +++ b/lib/paiement/ui/components/paiment_delegate/feedback_overlay.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/constants.dart'; + +const _teal = Color(0xff017f80); + +class FeedbackOverlay extends HookWidget { + final bool isSuccess; + const FeedbackOverlay({super.key, required this.isSuccess}); + + @override + Widget build(BuildContext context) { + final controller = useAnimationController( + duration: const Duration(milliseconds: 700), + ); + + useEffect(() { + controller.forward(); + return null; + }, [controller]); + + final scaleAnim = CurvedAnimation( + parent: controller, + curve: Curves.elasticOut, + ); + final fadeAnim = CurvedAnimation( + parent: controller, + curve: const Interval(0.0, 0.4, curve: Curves.easeOut), + ); + + final color = isSuccess ? _teal : ColorConstants.main; + + return Center( + child: FadeTransition( + opacity: fadeAnim, + child: ScaleTransition( + scale: scaleAnim, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color, + boxShadow: [ + BoxShadow( + color: color.withAlpha(77), + blurRadius: 20, + spreadRadius: 4, + ), + ], + ), + child: Icon( + isSuccess ? Icons.check_rounded : Icons.close_rounded, + color: Colors.white, + size: 44, + ), + ), + const SizedBox(height: 20), + Text( + isSuccess + ? AppLocalizations.of(context)!.paiementPaymentSuccessful + : AppLocalizations.of(context)!.paiementPaymentCanceled, + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w700, + color: color, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/paiement/ui/components/paiment_delegate/paiment_delegate_modal.dart b/lib/paiement/ui/components/paiment_delegate/paiment_delegate_modal.dart new file mode 100644 index 0000000000..c6427d9103 --- /dev/null +++ b/lib/paiement/ui/components/paiment_delegate/paiment_delegate_modal.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/paiement/ui/components/paiment_delegate/confirm_button.dart'; +import 'package:titan/paiement/ui/components/paiment_delegate/countdown_timer.dart'; +import 'package:titan/paiement/ui/components/paiment_delegate/feedback_overlay.dart'; +import 'package:titan/paiement/ui/components/paiment_delegate/product_card.dart'; +import 'package:titan/paiement/ui/components/paiment_delegate/wallet_balance_card.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; + +enum _ModalState { idle, loading, success, canceled } + +class PaimentDelegateModal extends HookWidget { + final String itemTitle; + final String itemDescription; + final int itemPrice; + final DateTime? itemExpirationDate; + final VoidCallback onConfirm; + final VoidCallback? onRefuse; + const PaimentDelegateModal({ + super.key, + required this.itemTitle, + required this.itemDescription, + required this.itemPrice, + this.itemExpirationDate, + required this.onConfirm, + this.onRefuse, + }); + + @override + Widget build(BuildContext context) { + final state = useState(_ModalState.idle); + final isExpired = useState(false); + final idleHeight = useState(null); + final idleKey = useMemoized(() => GlobalKey()); + final expirationDate = useMemoized( + () => DateTime.now().add(const Duration(minutes: 2)), + ); + final secondsLeft = useMemoized( + () => expirationDate.difference(DateTime.now()).inSeconds, + ); + + // Capture the idle content height after first layout + useEffect(() { + WidgetsBinding.instance.addPostFrameCallback((_) { + final renderBox = + idleKey.currentContext?.findRenderObject() as RenderBox?; + if (renderBox != null && idleHeight.value == null) { + idleHeight.value = renderBox.size.height; + } + }); + return null; + }, []); + + Future onValidate() async { + if (isExpired.value || state.value != _ModalState.idle) return; + state.value = _ModalState.loading; + onConfirm(); + await Future.delayed(const Duration(milliseconds: 600)); + if (!context.mounted) return; + state.value = _ModalState.success; + await Future.delayed(const Duration(milliseconds: 1500)); + if (context.mounted) Navigator.of(context).pop(); + } + + Future onCancel() async { + if (state.value != _ModalState.idle) return; + if (onRefuse != null) { + state.value = _ModalState.loading; + onRefuse!(); + return; + } + state.value = _ModalState.canceled; + await Future.delayed(const Duration(milliseconds: 1500)); + if (context.mounted) Navigator.of(context).pop(); + } + + final showIdle = + state.value == _ModalState.idle || state.value == _ModalState.loading; + + return BottomModalTemplate( + title: AppLocalizations.of(context)!.paiementConfirmYourPurchase, + child: AnimatedSize( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + alignment: Alignment.topCenter, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeIn, + child: showIdle + ? SingleChildScrollView( + key: const ValueKey('idle'), + child: Column( + key: idleKey, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ProductCard( + title: itemTitle, + description: itemDescription, + priceInCents: itemPrice, + ), + if (secondsLeft > 0) ...[ + const SizedBox(height: 20), + CountdownTimer( + totalSeconds: secondsLeft, + onFinished: () => isExpired.value = true, + ), + ], + const SizedBox(height: 20), + const WalletBalanceCard(), + const SizedBox(height: 24), + ConfirmButton( + totalSeconds: secondsLeft, + onConfirm: onValidate, + onCancel: onCancel, + ), + ], + ), + ) + : SizedBox( + height: idleHeight.value, + child: FeedbackOverlay( + key: ValueKey(state.value), + isSuccess: state.value == _ModalState.success, + ), + ), + ), + ), + ); + } +} diff --git a/lib/paiement/ui/components/paiment_delegate/product_card.dart b/lib/paiement/ui/components/paiment_delegate/product_card.dart new file mode 100644 index 0000000000..f627a2d887 --- /dev/null +++ b/lib/paiement/ui/components/paiment_delegate/product_card.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:titan/tools/constants.dart'; + +const _teal = Color(0xff017f80); +const _tealDark = Color.fromARGB(255, 4, 84, 84); + +class ProductCard extends StatelessWidget { + final String title; + final String description; + final int priceInCents; + + const ProductCard({ + super.key, + required this.title, + required this.description, + required this.priceInCents, + }); + + @override + Widget build(BuildContext context) { + final priceFormatter = NumberFormat.currency( + locale: 'fr_FR', + symbol: '€', + decimalDigits: 2, + ); + + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + children: [ + Text( + title, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: ColorConstants.tertiary, + ), + ), + const SizedBox(height: 6), + Text( + description, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: ColorConstants.tertiary.withAlpha(153), + height: 1.4, + ), + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10), + decoration: BoxDecoration( + gradient: const LinearGradient(colors: [_teal, _tealDark]), + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + color: _teal.withAlpha(77), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Text( + priceFormatter.format(priceInCents / 100), + style: const TextStyle( + color: Colors.white, + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/paiement/ui/components/paiment_delegate/wallet_balance_card.dart b/lib/paiement/ui/components/paiment_delegate/wallet_balance_card.dart new file mode 100644 index 0000000000..9aca7709fc --- /dev/null +++ b/lib/paiement/ui/components/paiment_delegate/wallet_balance_card.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/paiement/providers/my_wallet_provider.dart'; +import 'package:titan/tools/ui/styleguide/list_item_template.dart'; + +const _teal = Color(0xff017f80); + +class WalletBalanceCard extends ConsumerWidget { + const WalletBalanceCard({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final wallet = ref.watch(myWalletProvider); + final priceFormatter = NumberFormat.currency( + locale: 'fr_FR', + symbol: '€', + decimalDigits: 2, + ); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + ), + child: ListItemTemplate( + title: AppLocalizations.of(context)!.paiementYourBalance, + icon: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: _teal.withAlpha(26), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.account_balance_wallet_rounded, + color: _teal, + size: 20, + ), + ), + trailing: wallet.when( + data: (w) => Text( + priceFormatter.format(w.balance / 100), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: _teal, + ), + ), + loading: () => const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), + error: (_, __) => const Text( + '—', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: _teal, + ), + ), + ), + ), + ); + } +} diff --git a/lib/paiement/ui/components/transaction_card.dart b/lib/paiement/ui/components/transaction_card.dart index d3f2321f47..5c033d2cbc 100644 --- a/lib/paiement/ui/components/transaction_card.dart +++ b/lib/paiement/ui/components/transaction_card.dart @@ -1,11 +1,14 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:heroicons/heroicons.dart'; import 'package:intl/intl.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/paiement/class/history.dart'; import 'package:titan/paiement/tools/functions.dart'; +import 'package:titan/tools/providers/locale_notifier.dart'; -class TransactionCard extends StatelessWidget { +class TransactionCard extends ConsumerWidget { final History transaction; final Function()? onTap; final bool storeView; @@ -17,8 +20,12 @@ class TransactionCard extends StatelessWidget { }); @override - Widget build(BuildContext context) { - final formatter = NumberFormat("#,##0.00", "fr_FR"); + Widget build(BuildContext context, WidgetRef ref) { + final locale = ref.watch(localeProvider); + final formatter = NumberFormat.currency( + locale: locale.toString(), + symbol: "€", + ); final HeroIcons icon; switch (transaction.type) { @@ -41,7 +48,7 @@ class TransactionCard extends StatelessWidget { final transactionName = transaction.type != HistoryType.transfer ? transaction.otherWalletName - : "Recharge"; + : AppLocalizations.of(context)!.paiementTopUp; final colors = getTransactionColors(transaction); @@ -79,7 +86,7 @@ class TransactionCard extends StatelessWidget { child: AutoSizeText( storeView ? transactionName - : "${transaction.type == HistoryType.refundCredited || transaction.type == HistoryType.refundDebited ? "Remboursement - " : ""}$transactionName", + : "${transaction.type == HistoryType.refundCredited || transaction.type == HistoryType.refundDebited ? "${AppLocalizations.of(context)!.paiementRefund} - " : ""}$transactionName", maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( @@ -106,7 +113,7 @@ class TransactionCard extends StatelessWidget { borderRadius: BorderRadius.circular(5), ), child: Text( - "Annulé", + AppLocalizations.of(context)!.paiementCancelled, style: TextStyle( color: const Color.fromARGB(255, 204, 70, 25), fontSize: 12, @@ -118,7 +125,7 @@ class TransactionCard extends StatelessWidget { ), if (transaction.refund == null) const SizedBox(height: 5), Text( - "Le ${DateFormat("EEE dd MMMM yyyy à HH:mm", "fr_FR").format(transaction.creation)}", + "${AppLocalizations.of(context)!.paiementThe} ${DateFormat.yMMMMEEEEd(locale.toString()).format(transaction.creation)} + ${AppLocalizations.of(context)!.paiementAt} ${DateFormat.Hm(locale.toString()).format(transaction.creation)}", style: const TextStyle( color: Color(0xff204550), fontSize: 12, @@ -126,7 +133,7 @@ class TransactionCard extends StatelessWidget { ), if (transaction.refund != null) Text( - "Remboursé le ${DateFormat("EEE dd MMMM yyyy à HH:mm", "fr_FR").format(transaction.refund!.creation)} de ${formatter.format(transaction.refund!.total / 100)} €", + "${AppLocalizations.of(context)!.paiementRefundedThe} ${DateFormat.yMMMMEEEEd(locale.toString()).format(transaction.refund!.creation)} + ${AppLocalizations.of(context)!.paiementAt} ${DateFormat.Hm(locale.toString()).format(transaction.refund!.creation)} ${AppLocalizations.of(context)!.paiementOf} ${formatter.format(transaction.refund!.total / 100)}", style: const TextStyle( color: Color.fromARGB(255, 16, 46, 55), fontSize: 9, @@ -137,7 +144,7 @@ class TransactionCard extends StatelessWidget { ), const SizedBox(width: 10), Text( - "${transaction.type == HistoryType.given || transaction.type == HistoryType.refundDebited ? " -" : " +"} ${formatter.format(transaction.total / 100)} €", + "${transaction.type == HistoryType.given || transaction.type == HistoryType.refundDebited ? " -" : " +"} ${formatter.format(transaction.total / 100)}", style: TextStyle( color: const Color(0xff204550), fontSize: 18, diff --git a/lib/paiement/ui/pages/devices_page/add_device_button.dart b/lib/paiement/ui/pages/devices_page/add_device_button.dart index 193b0a7ea9..d462e30c40 100644 --- a/lib/paiement/ui/pages/devices_page/add_device_button.dart +++ b/lib/paiement/ui/pages/devices_page/add_device_button.dart @@ -2,6 +2,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; class AddDeviceButton extends StatelessWidget { @@ -60,7 +61,7 @@ class AddDeviceButton extends StatelessWidget { ), Spacer(), Text( - "Ajouter cet appareil", + AppLocalizations.of(context)!.paiementAddThisDevice, style: TextStyle( color: Color(0xff204550), fontSize: 20, diff --git a/lib/paiement/ui/pages/devices_page/device_item.dart b/lib/paiement/ui/pages/devices_page/device_item.dart index 1a07051b74..8d844aa0ee 100644 --- a/lib/paiement/ui/pages/devices_page/device_item.dart +++ b/lib/paiement/ui/pages/devices_page/device_item.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:heroicons/heroicons.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/paiement/class/wallet_device.dart'; import 'package:titan/paiement/tools/functions.dart'; @@ -44,8 +45,8 @@ class DeviceItem extends ConsumerWidget { ), ), if (isActual) - const Text( - '(cet appareil)', + Text( + AppLocalizations.of(context)!.paiementThisDevice, style: TextStyle( color: Color(0xff204550), fontSize: 15, diff --git a/lib/paiement/ui/pages/devices_page/devices_page.dart b/lib/paiement/ui/pages/devices_page/devices_page.dart index 8d282bab8d..c34c6e1b07 100644 --- a/lib/paiement/ui/pages/devices_page/devices_page.dart +++ b/lib/paiement/ui/pages/devices_page/devices_page.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/paiement/class/create_device.dart'; import 'package:titan/paiement/class/wallet_device.dart'; import 'package:titan/paiement/providers/device_list_provider.dart'; @@ -46,12 +47,15 @@ class DevicesPage extends HookConsumerWidget { } else if (Theme.of(context).platform == TargetPlatform.iOS) { return deviceInfo.iosInfo.then((info) => info.utsname.machine); } else { - return Future.value("Unknown Device"); + return Future.value( + AppLocalizations.of(context)!.paiementUnknownDevice, + ); } } return PaymentTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await devicesNotifier.getDeviceList(); }, @@ -97,7 +101,9 @@ class DevicesPage extends HookConsumerWidget { if (!hasAcceptedToS) { displayToastWithContext( TypeMsg.error, - "Veuillez accepter les Conditions Générales d'Utilisation.", + AppLocalizations.of( + context, + )!.paiementPleaseAcceptTOS, ); return; } @@ -123,10 +129,12 @@ class DevicesPage extends HookConsumerWidget { context: context, builder: (context) { return DeviceDialogBox( - title: - 'Demande d\'activation de l\'appareil', - descriptions: - "La demande d'activation est prise en compte, veuilliez consulter votre boite mail pour finaliser la démarche", + title: AppLocalizations.of( + context, + )!.paiementAskDeviceActivation, + descriptions: AppLocalizations.of( + context, + )!.paiementDeviceActivationReceived, buttonText: "Ok", onClick: () { Navigator.of(context).pop(); @@ -146,7 +154,9 @@ class DevicesPage extends HookConsumerWidget { if (!hasAcceptedToS) { displayToastWithContext( TypeMsg.error, - "Veuillez accepter les Conditions Générales d'Utilisation.", + AppLocalizations.of( + context, + )!.paiementPleaseAcceptTOS, ); return; } @@ -154,11 +164,22 @@ class DevicesPage extends HookConsumerWidget { context: context, builder: (context) { return CustomDialogBox( - title: "Révoquer l'appareil ?", - descriptions: - "Vous ne pourrez plus utiliser cet appareil pour les paiements", + title: AppLocalizations.of( + context, + )!.paiementRevokeDevice, + descriptions: AppLocalizations.of( + context, + )!.paiementRevokeDeviceDescription, onYes: () async { tokenExpireWrapper(ref, () async { + final deviceRevokedMsg = + AppLocalizations.of( + context, + )!.paiementDeviceRevoked; + final deviceRevokingErrorMsg = + AppLocalizations.of( + context, + )!.paiementDeviceRevokingError; final value = await devicesNotifier .revokeDevice( device.copyWith( @@ -168,7 +189,7 @@ class DevicesPage extends HookConsumerWidget { if (value) { displayToastWithContext( TypeMsg.msg, - "Appareil révoqué", + deviceRevokedMsg, ); final savedId = await keyService .getKeyId(); @@ -178,7 +199,7 @@ class DevicesPage extends HookConsumerWidget { } else { displayToastWithContext( TypeMsg.error, - "Erreur lors de la révocation de l'appareil", + deviceRevokingErrorMsg, ); } }); @@ -189,6 +210,7 @@ class DevicesPage extends HookConsumerWidget { }, ); }), + SizedBox(height: 80), ], ); }, diff --git a/lib/paiement/ui/pages/fund_page/confirm_button.dart b/lib/paiement/ui/pages/fund_page/confirm_button.dart index b8800a7b02..06e19bcd4b 100644 --- a/lib/paiement/ui/pages/fund_page/confirm_button.dart +++ b/lib/paiement/ui/pages/fund_page/confirm_button.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/paiement/class/init_info.dart'; import 'package:titan/paiement/providers/fund_amount_provider.dart'; import 'package:titan/paiement/providers/funding_url_provider.dart'; @@ -37,7 +38,7 @@ class ConfirmFundButton extends ConsumerWidget { ); final redirectUrl = kIsWeb - ? "${getTitanURL()}/static.html" // ? + ? "${getTitanURL()}/payment" : "${getTitanURLScheme()}://payment"; final amountToAdd = double.tryParse(fundAmount.replaceAll(",", ".")) ?? 0; @@ -50,12 +51,14 @@ class ConfirmFundButton extends ConsumerWidget { displayToast(context, type, message); } + final localizeWithContext = AppLocalizations.of(context)!; + Future tryLaunchUrl(url) async { if (!await launchUrl( Uri.parse(url), mode: LaunchMode.externalApplication, )) { - throw Exception('Could not launch google'); + throw Exception(localizeWithContext.paiementCantLaunchURL); } } @@ -69,7 +72,10 @@ class ConfirmFundButton extends ConsumerWidget { as html.WindowBase?; if (popupWin == null) { - displayToastWithContext(TypeMsg.error, "Veuillez autoriser les popups"); + displayToastWithContext( + TypeMsg.error, + localizeWithContext.paiementPleaseAcceptPopup, + ); return; } @@ -89,11 +95,17 @@ class ConfirmFundButton extends ConsumerWidget { final receivedUri = Uri.parse(data); final code = receivedUri.queryParameters["code"]; if (code == "succeeded") { - displayToastWithContext(TypeMsg.msg, "Paiement effectué avec succès"); + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.paiementProceedSuccessfully, + ); myWalletNotifier.getMyWallet(); myHistoryNotifier.getHistory(); } else { - displayToastWithContext(TypeMsg.error, "Paiement annulé"); + displayToastWithContext( + TypeMsg.error, + localizeWithContext.paiementCancelledTransaction, + ); } popupWin.close(); Navigator.pop(context, code); @@ -111,14 +123,14 @@ class ConfirmFundButton extends ConsumerWidget { if (!minValidFundAmount) { displayToastWithContext( TypeMsg.error, - "Veuillez entrer un montant supérieur à 1€", + localizeWithContext.paiementPleaseEnterMinAmount, ); return; } if (!maxValidFundAmount) { displayToastWithContext( TypeMsg.error, - "Le montant maximum de votre portefeuille est de ${maxBalanceAmount.toStringAsFixed(2)}€", + "${localizeWithContext.paiementMaxAmount} ${maxBalanceAmount.toStringAsFixed(2)}€", ); return; } @@ -181,7 +193,7 @@ class ConfirmFundButton extends ConsumerWidget { ), ), Text( - "Payer avec HelloAsso", + localizeWithContext.paiementPayWithHA, style: TextStyle( color: (minValidFundAmount && maxValidFundAmount) ? const Color(0xff2e2f5e) diff --git a/lib/paiement/ui/pages/fund_page/fund_page.dart b/lib/paiement/ui/pages/fund_page/fund_page.dart index 93a04f4a14..47c4836662 100644 --- a/lib/paiement/ui/pages/fund_page/fund_page.dart +++ b/lib/paiement/ui/pages/fund_page/fund_page.dart @@ -1,18 +1,21 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:intl/intl.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/paiement/providers/fund_amount_provider.dart'; import 'package:titan/paiement/providers/my_wallet_provider.dart'; import 'package:titan/paiement/providers/tos_provider.dart'; import 'package:titan/paiement/ui/components/digit_fade_in_animation.dart'; import 'package:titan/paiement/ui/components/keyboard.dart'; import 'package:titan/paiement/ui/pages/fund_page/confirm_button.dart'; +import 'package:titan/tools/providers/locale_notifier.dart'; class FundPage extends ConsumerWidget { const FundPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final locale = ref.watch(localeProvider); final fundAmount = ref.watch(fundAmountProvider); final fundAmountNotifier = ref.watch(fundAmountProvider.notifier); final myWallet = ref.watch(myWalletProvider); @@ -25,7 +28,10 @@ class FundPage extends ConsumerWidget { orElse: () => 0, data: (wallet) => wallet.balance / 100, ); - final formatter = NumberFormat("#,##0.00", "fr_FR"); + final formatter = NumberFormat.currency( + locale: locale.toString(), + symbol: "€", + ); final amountToAdd = double.tryParse(fundAmount.replaceAll(",", ".")) ?? 0; @@ -44,11 +50,12 @@ class FundPage extends ConsumerWidget { end: Alignment.bottomRight, ), ), + height: MediaQuery.of(context).size.height * 0.8, child: Column( children: [ const SizedBox(height: 20), Text( - 'Recharge', + AppLocalizations.of(context)!.paiementTopUp, style: const TextStyle( color: Colors.white, fontSize: 20, @@ -57,7 +64,7 @@ class FundPage extends ConsumerWidget { ), const SizedBox(height: 5), Text( - 'Solde après recharge : ${formatter.format(amountToAdd + currentAmount)} € (max: ${formatter.format(maxBalanceAmount)} €)', + '${AppLocalizations.of(context)!.paiementBalanceAfterTopUp} ${formatter.format(amountToAdd + currentAmount)} (max: ${formatter.format(maxBalanceAmount)})', style: const TextStyle(color: Colors.white, fontSize: 15), ), Expanded( @@ -123,6 +130,7 @@ class FundPage extends ConsumerWidget { }, ), const Expanded(child: Center(child: ConfirmFundButton())), + const SizedBox(height: 10), ], ), ), diff --git a/lib/paiement/ui/pages/invoices_admin_page/invoice_card.dart b/lib/paiement/ui/pages/invoices_admin_page/invoice_card.dart new file mode 100644 index 0000000000..cc7bc7e989 --- /dev/null +++ b/lib/paiement/ui/pages/invoices_admin_page/invoice_card.dart @@ -0,0 +1,224 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:file_saver/file_saver.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/paiement/class/invoice.dart'; +import 'package:titan/paiement/providers/invoice_list_provider.dart'; +import 'package:titan/paiement/providers/invoice_pdf_provider.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; +import 'package:titan/tools/ui/styleguide/confirm_modal.dart'; +import 'package:titan/tools/ui/styleguide/list_item_template.dart'; + +class InvoiceCard extends HookConsumerWidget { + final Invoice invoice; + final bool isAdmin; + + const InvoiceCard({super.key, required this.invoice, required this.isAdmin}) + : super(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final localizeWithContext = AppLocalizations.of(context)!; + final invoicesNotifier = ref.read(invoiceListProvider.notifier); + final invoicePdf = ref.watch(invoicePdfProvider(invoice.id).future); + + void displayToastWithContext(TypeMsg type, String msg) { + displayToast(context, type, msg); + } + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: ListItemTemplate( + title: invoice.reference, + subtitle: invoice.structure.name, + onTap: () => showCustomBottomModal( + context: context, + modal: BottomModalTemplate( + title: invoice.reference, + child: Column( + children: [ + Button( + text: localizeWithContext.paiementDownload, + onPressed: () async { + late final Uint8List pdfBytes; + try { + pdfBytes = await invoicePdf; + } catch (e) { + displayToastWithContext(TypeMsg.error, e.toString()); + return; + } + final path = kIsWeb + ? await FileSaver.instance.saveFile( + name: invoice.reference, + bytes: pdfBytes, + ext: "pdf", + mimeType: MimeType.pdf, + ) + : await FileSaver.instance.saveAs( + name: invoice.reference, + bytes: pdfBytes, + ext: "pdf", + mimeType: MimeType.pdf, + ); + if (path != null) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.phSuccesDowloading, + ); + } + }, + ), + if (!invoice.received && isAdmin) ...[ + const SizedBox(height: 10), + Button( + text: invoice.paid + ? localizeWithContext.paiementMarkUnpaid + : localizeWithContext.paiementMarkPaid, + onPressed: () async { + final value = await invoicesNotifier + .updateInvoicePaidStatus(invoice, !invoice.paid); + if (value) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.paiementModifySuccessfully, + ); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.paiementErrorUpdatingStatus, + ); + } + }, + ), + ], + if (!isAdmin && invoice.paid && !invoice.received) ...[ + const SizedBox(height: 10), + Button( + text: localizeWithContext.paiementMarkReceived, + onPressed: () async { + Navigator.of(context).pop(); + showCustomBottomModal( + context: context, + ref: ref, + modal: ConfirmModal.danger( + title: localizeWithContext.paiementDeleteInvoice, + description: + localizeWithContext.globalIrreversibleAction, + onYes: () async { + final value = await invoicesNotifier + .updateInvoiceReceivedStatus(invoice, true); + if (value) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.paiementModifySuccessfully, + ); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.paiementErrorUpdatingStatus, + ); + } + }, + ), + ); + }, + ), + ], + if (!invoice.paid && isAdmin) ...[ + const SizedBox(height: 10), + Button( + text: localizeWithContext.paiementDeleteInvoice, + onPressed: () async { + Navigator.of(context).pop(); + showCustomBottomModal( + context: context, + ref: ref, + modal: ConfirmModal.danger( + title: localizeWithContext.paiementDeleteInvoice, + description: + localizeWithContext.globalIrreversibleAction, + onYes: () async { + final value = await invoicesNotifier.deleteInvoice( + invoice, + ); + if (value) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.paiementDeleteSuccessfully, + ); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.paiementErrorDeleting, + ); + } + }, + ), + ); + }, + ), + ], + ], + ), + ), + ref: ref, + ), + trailing: Expanded( + flex: kIsWeb ? 2 : 1, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (kIsWeb) + Column( + children: [ + Text( + '${(invoice.total / 100).toStringAsFixed(2)} €', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: ColorConstants.tertiary, + ), + ), + AutoSizeText( + localizeWithContext.paiementFromTo( + invoice.startDate, + invoice.endDate, + ), + maxLines: 2, + style: TextStyle(fontSize: 12), + ), + ], + ), + Text( + invoice.received + ? localizeWithContext.paiementReceived + : invoice.paid + ? localizeWithContext.paiementPaid + : localizeWithContext.paiementPending, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: invoice.received + ? Colors.green + : invoice.paid + ? Colors.blue + : Colors.orange, + ), + ), + const HeroIcon( + HeroIcons.chevronRight, + color: ColorConstants.tertiary, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/paiement/ui/pages/invoices_admin_page/invoices_admin_page.dart b/lib/paiement/ui/pages/invoices_admin_page/invoices_admin_page.dart new file mode 100644 index 0000000000..45e2fa5e18 --- /dev/null +++ b/lib/paiement/ui/pages/invoices_admin_page/invoices_admin_page.dart @@ -0,0 +1,210 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/admin/providers/structure_provider.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/paiement/providers/invoice_list_provider.dart'; +import 'package:titan/paiement/providers/structure_list_provider.dart'; +import 'package:titan/paiement/ui/pages/invoices_admin_page/invoice_card.dart'; +import 'package:titan/paiement/ui/paiement.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; +import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/tools/ui/layouts/refresher.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; +import 'package:titan/tools/ui/styleguide/item_chip.dart'; +import 'package:titan/tools/ui/styleguide/list_item_template.dart'; +import 'package:tuple/tuple.dart'; + +class InvoicesAdminPage extends HookConsumerWidget { + const InvoicesAdminPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final controller = useScrollController(); + final page = useState(1); + final pageSize = useState(20); + final invoices = ref.watch(invoiceListProvider); + final structures = ref.watch(structureListProvider); + final structureNotifier = ref.watch(structureProvider.notifier); + final invoicesNotifier = ref.read(invoiceListProvider.notifier); + + final localizeWithContext = AppLocalizations.of(context)!; + + void refreshInvoices() { + tokenExpireWrapper( + ref, + () => invoicesNotifier.getInvoices( + page: page.value, + pageLimit: pageSize.value, + ), + ); + } + + void displayToastWithContext(TypeMsg type, String msg) { + displayToast(context, type, msg); + } + + return PaymentTemplate( + child: Refresher( + onRefresh: () async { + refreshInvoices(); + }, + controller: controller, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Async2Children( + values: Tuple2(invoices, structures), + builder: (context, invoices, structures) { + return Column( + children: [ + Row( + children: [ + IconButton( + icon: HeroIcon(HeroIcons.arrowLeft), + onPressed: page.value <= 1 + ? null + : () { + page.value--; + refreshInvoices(); + }, + color: ColorConstants.onTertiary, + disabledColor: ColorConstants.background, + ), + DropdownButton( + items: [10, 20, 50, 100] + .map( + (size) => DropdownMenuItem( + value: size, + child: Text( + localizeWithContext.paiementInvoicesPerPage( + size, + ), + ), + ), + ) + .toList(), + onChanged: (value) { + if (value != null) { + pageSize.value = value; + refreshInvoices(); + } + }, + value: pageSize.value, + ), + IconButton( + icon: HeroIcon(HeroIcons.arrowRight), + onPressed: invoices.length < pageSize.value + ? null + : () { + page.value++; + refreshInvoices(); + }, + color: ColorConstants.onTertiary, + disabledColor: ColorConstants.background, + ), + ], + ), + const SizedBox(height: 10), + ListItemTemplate( + title: localizeWithContext.paiementCreateInvoice, + onTap: () => showCustomBottomModal( + context: context, + modal: Consumer( + builder: (context, ref, _) { + final structure = ref.watch(structureProvider); + return BottomModalTemplate( + title: localizeWithContext.paiementCreateInvoice, + child: Column( + children: [ + Text( + localizeWithContext.paiementSelectStructure, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + SizedBox( + height: min( + structures.length * 50, + MediaQuery.of(context).size.height * 0.8, + ), + child: SingleChildScrollView( + child: Column( + children: structures + .map( + (e) => ItemChip( + scrollDirection: Axis.vertical, + onTap: () => structureNotifier + .setStructure(e), + selected: structure.id == e.id, + child: Text( + e.name, + style: TextStyle( + fontSize: 16, + color: structure.id == e.id + ? Colors.white + : Colors.black, + ), + ), + ), + ) + .toList(), + ), + ), + ), + Button( + text: localizeWithContext.paiementCreate, + onPressed: () async { + if (structure.id == "") return; + Navigator.pop(context); + await tokenExpireWrapper(ref, () async { + final value = await invoicesNotifier + .createInvoice(structure); + if (value) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext + .paiementInvoiceCreatedSuccessfully, + ); + refreshInvoices(); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext + .paiementNoInvoiceToCreate, + ); + } + }); + }, + ), + ], + ), + ); + }, + ), + ref: ref, + ), + trailing: HeroIcon( + HeroIcons.plus, + color: ColorConstants.onTertiary, + ), + ), + const SizedBox(height: 10), + ...invoices.map( + (invoice) => InvoiceCard(invoice: invoice, isAdmin: true), + ), + ], + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/paiement/ui/pages/invoices_structure_page/invoices_structure_page.dart b/lib/paiement/ui/pages/invoices_structure_page/invoices_structure_page.dart new file mode 100644 index 0000000000..1edec34da4 --- /dev/null +++ b/lib/paiement/ui/pages/invoices_structure_page/invoices_structure_page.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/paiement/providers/invoice_list_provider.dart'; +import 'package:titan/paiement/providers/selected_structure_provider.dart'; +import 'package:titan/paiement/ui/pages/invoices_admin_page/invoice_card.dart'; +import 'package:titan/paiement/ui/paiement.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; +import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/tools/ui/layouts/refresher.dart'; + +class StructureInvoicesPage extends HookConsumerWidget { + const StructureInvoicesPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final controller = useScrollController(); + final page = useState(1); + final pageSize = useState(20); + + final selectedStructure = ref.watch(selectedStructureProvider); + final invoices = ref.watch(invoiceListProvider); + final invoicesNotifier = ref.watch(invoiceListProvider.notifier); + + void refreshInvoices() { + tokenExpireWrapper( + ref, + () => invoicesNotifier.getStructureInvoices( + selectedStructure.id, + page: page.value, + pageLimit: pageSize.value, + ), + ); + } + + return PaymentTemplate( + child: Refresher( + controller: controller, + onRefresh: () async { + refreshInvoices(); + }, + child: AsyncChild( + value: invoices, + builder: (context, invoices) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + Row( + children: [ + IconButton( + icon: HeroIcon(HeroIcons.arrowLeft), + onPressed: page.value <= 1 + ? null + : () { + page.value--; + refreshInvoices(); + }, + color: ColorConstants.onTertiary, + disabledColor: ColorConstants.background, + ), + DropdownButton( + items: [10, 20, 50, 100] + .map( + (size) => DropdownMenuItem( + value: size, + child: Text(size.toString()), + ), + ) + .toList(), + onChanged: (value) { + if (value != null) { + pageSize.value = value; + refreshInvoices(); + } + }, + value: pageSize.value, + ), + IconButton( + icon: HeroIcon(HeroIcons.arrowRight), + onPressed: invoices.length < pageSize.value + ? null + : () { + page.value++; + refreshInvoices(); + }, + color: ColorConstants.onTertiary, + disabledColor: ColorConstants.background, + ), + ], + ), + const SizedBox(height: 10), + ...invoices.map( + (invoice) => InvoiceCard(invoice: invoice, isAdmin: false), + ), + ], + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/paiement/ui/pages/main_page/account_card/account_card.dart b/lib/paiement/ui/pages/main_page/account_card/account_card.dart index b15a39d7f0..318baaf465 100644 --- a/lib/paiement/ui/pages/main_page/account_card/account_card.dart +++ b/lib/paiement/ui/pages/main_page/account_card/account_card.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:intl/intl.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/paiement/class/wallet_device.dart'; import 'package:titan/paiement/providers/device_list_provider.dart'; import 'package:titan/paiement/providers/device_provider.dart'; @@ -18,9 +19,11 @@ import 'package:titan/paiement/ui/pages/main_page/main_card_button.dart'; import 'package:titan/paiement/ui/pages/main_page/main_card_template.dart'; import 'package:titan/paiement/ui/pages/pay_page/pay_page.dart'; import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/providers/locale_notifier.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; class AccountCard extends HookConsumerWidget { final Function? toggle; @@ -33,6 +36,7 @@ class AccountCard extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final locale = ref.watch(localeProvider); final myWallet = ref.watch(myWalletProvider); final keyService = ref.read(keyServiceProvider); final payAmountNotifier = ref.watch(payAmountProvider.notifier); @@ -43,35 +47,59 @@ class AccountCard extends HookConsumerWidget { const Color(0xff017f80), const Color.fromARGB(255, 4, 84, 84), ]; - final formatter = NumberFormat("#,##0.00", "fr_FR"); + final formatter = NumberFormat.currency( + locale: locale.toString(), + symbol: "€", + ); + final localizeWithContext = AppLocalizations.of(context)!; void displayToastWithContext(TypeMsg type, String message) { displayToast(context, type, message); } void showPayModal() { - showModalBottomSheet( + showCustomBottomModal( context: context, - backgroundColor: Colors.transparent, - scrollControlDisabledMaxHeightRatio: - (1 - 80 / MediaQuery.of(context).size.height), - builder: (context) => const PayPage(), - ).then((_) { - payAmountNotifier.setPayAmount(""); - }); + // backgroundColor: Colors.transparent, + // scrollControlDisabledMaxHeightRatio: + // (1 - 80 / MediaQuery.of(context).size.height), + // builder: (context) => const PayPage(), + modal: PayPage(), + ref: ref, + onCloseCallback: () => payAmountNotifier.setPayAmount(""), + ); } void showFundModal() async { resetHandledKeys(); - await showModalBottomSheet( + await showCustomBottomModal( context: context, - backgroundColor: Colors.transparent, - scrollControlDisabledMaxHeightRatio: - (1 - 80 / MediaQuery.of(context).size.height), - builder: (context) => const FundPage(), - ).then((code) { - fundAmountNotifier.setFundAmount(""); - }); + modal: FundPage(), + ref: ref, + onCloseCallback: () => fundAmountNotifier.setFundAmount(""), + + // backgroundColor: Colors.transparent, + // scrollControlDisabledMaxHeightRatio: + // (1 - 80 / MediaQuery.of(context).size.height), + // builder: (context) => const FundPage(), + ); + } + + void showNotRegisteredDeviceDialog() async { + await showDialog( + context: context, + builder: (context) { + return DeviceDialogBox( + title: localizeWithContext.paiementDeviceNotRegistered, + descriptions: + localizeWithContext.paiementDeviceNotRegisteredDescription, + buttonText: localizeWithContext.paiementAccessPage, + onClick: () { + QR.to(PaymentRouter.root + PaymentRouter.devices); + }, + ); + }, + ); } return MainCardTemplate( @@ -80,13 +108,13 @@ class AccountCard extends HookConsumerWidget { Color(0xff017f80), Color.fromARGB(255, 4, 84, 84), ], - title: 'Solde personnel', + title: localizeWithContext.paiementPersonalBalance, toggle: toggle, actionButtons: [ MainCardButton( colors: buttonGradient, icon: HeroIcons.devicePhoneMobile, - title: "Appareils", + title: localizeWithContext.paiementDevices, onPressed: () async { ref.invalidate(deviceListProvider); QR.to(PaymentRouter.root + PaymentRouter.devices); @@ -96,32 +124,19 @@ class AccountCard extends HookConsumerWidget { MainCardButton( colors: buttonGradient, icon: HeroIcons.qrCode, - title: "Payer", + title: localizeWithContext.paiementPay, onPressed: () async { await tokenExpireWrapper(ref, () async { if (!hasAcceptedToS) { displayToastWithContext( TypeMsg.error, - "Veuillez accepter les Conditions Générales d'Utilisation.", + localizeWithContext.paiementPleaseAcceptTOS, ); return; } String? keyId = await keyService.getKeyId(); if (keyId == null) { - await showDialog( - context: context, - builder: (context) { - return DeviceDialogBox( - title: 'Appareil non enregistré', - descriptions: - 'Votre appareil n\'est pas encore enregistré. \nPour l\'enregistrer, veuillez vous rendre sur la page des appareils.', - buttonText: 'Accéder à la page', - onClick: () { - QR.to(PaymentRouter.root + PaymentRouter.devices); - }, - ); - }, - ); + showNotRegisteredDeviceDialog(); return; } final device = await deviceNotifier.getDevice(keyId); @@ -134,10 +149,11 @@ class AccountCard extends HookConsumerWidget { context: context, builder: (context) { return DeviceDialogBox( - title: 'Appareil non activé', - descriptions: - 'Votre appareil n\'est pas encore activé. \nPour l\'activer, veuillez vous rendre sur la page des appareils.', - buttonText: 'Accéder à la page', + title: + localizeWithContext.paiementDeviceNotActivated, + descriptions: localizeWithContext + .paiementDeviceNotActivatedDescription, + buttonText: localizeWithContext.paiementAccessPage, onClick: () { QR.to(PaymentRouter.root + PaymentRouter.devices); }, @@ -149,10 +165,10 @@ class AccountCard extends HookConsumerWidget { context: context, builder: (context) { return DeviceDialogBox( - title: 'Appareil révoqué', - descriptions: - 'Votre appareil a été révoqué. \nPour le réactiver, veuillez vous rendre sur la page des appareils.', - buttonText: 'Accéder à la page', + title: localizeWithContext.paiementDeviceRevoked, + descriptions: localizeWithContext + .paiementReactivateRevokedDeviceDescription, + buttonText: localizeWithContext.paiementAccessPage, onClick: () { QR.to(PaymentRouter.root + PaymentRouter.devices); }, @@ -164,7 +180,7 @@ class AccountCard extends HookConsumerWidget { error: (e, s) { displayToastWithContext( TypeMsg.error, - "Erreur lors de la récupération de l'appareil", + localizeWithContext.paiementDeviceRecoveryError, ); }, loading: () {}, @@ -175,7 +191,7 @@ class AccountCard extends HookConsumerWidget { MainCardButton( colors: buttonGradient, icon: HeroIcons.chartPie, - title: "Stats", + title: localizeWithContext.paiementStats, onPressed: () async { QR.to(PaymentRouter.root + PaymentRouter.stats); }, @@ -183,12 +199,12 @@ class AccountCard extends HookConsumerWidget { MainCardButton( colors: buttonGradient, icon: HeroIcons.creditCard, - title: "Recharger", + title: localizeWithContext.paiementTopUpAction, onPressed: () async { if (!hasAcceptedToS) { displayToastWithContext( TypeMsg.error, - "Veuillez accepter les Conditions Générales d'Utilisation.", + localizeWithContext.paiementPleaseAcceptTOS, ); return; } @@ -199,11 +215,11 @@ class AccountCard extends HookConsumerWidget { child: AsyncChild( value: myWallet, builder: (context, wallet) => Text( - '${formatter.format(wallet.balance / 100)} €', + formatter.format(wallet.balance / 100), style: const TextStyle(color: Colors.white, fontSize: 50), ), errorBuilder: (error, stackTrace) => Text( - 'Erreur lors de la récupération du solde : $error', + localizeWithContext.paiementGetBalanceError, style: const TextStyle(color: Colors.white, fontSize: 50), ), ), diff --git a/lib/paiement/ui/pages/main_page/account_card/last_transactions.dart b/lib/paiement/ui/pages/main_page/account_card/last_transactions.dart index da37b65679..c361dd963b 100644 --- a/lib/paiement/ui/pages/main_page/account_card/last_transactions.dart +++ b/lib/paiement/ui/pages/main_page/account_card/last_transactions.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/paiement/class/history.dart'; import 'package:titan/paiement/providers/my_history_provider.dart'; import 'package:titan/paiement/ui/pages/main_page/account_card/day_divider.dart'; @@ -23,9 +24,9 @@ class LastTransactions extends ConsumerWidget { Container( padding: const EdgeInsets.symmetric(horizontal: 30), alignment: Alignment.centerLeft, - child: const Text( - "Dernières transactions", - style: TextStyle( + child: Text( + AppLocalizations.of(context)!.paiementLastTransactions, + style: const TextStyle( color: Color(0xff204550), fontSize: 20, fontWeight: FontWeight.bold, @@ -79,7 +80,7 @@ class LastTransactions extends ConsumerWidget { }, errorBuilder: (error, stack) => Center( child: Text( - "Erreur lors de la récupération des transactions : $error", + "${AppLocalizations.of(context)!.paiementGetTransactionsError} : $error", style: TextStyle(color: Colors.red), ), ), diff --git a/lib/paiement/ui/pages/main_page/main_page.dart b/lib/paiement/ui/pages/main_page/main_page.dart index 82fa2559e3..569b07504c 100644 --- a/lib/paiement/ui/pages/main_page/main_page.dart +++ b/lib/paiement/ui/pages/main_page/main_page.dart @@ -1,14 +1,23 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/ui/scroll_to_hide_navbar.dart'; +import 'package:titan/paiement/class/payment_request.dart'; +import 'package:titan/paiement/class/request_validation.dart'; import 'package:titan/paiement/providers/has_accepted_tos_provider.dart'; import 'package:titan/paiement/providers/my_wallet_provider.dart'; +import 'package:titan/paiement/providers/payment_requests_provider.dart'; import 'package:titan/paiement/providers/tos_provider.dart'; import 'package:titan/paiement/providers/is_payment_admin.dart'; import 'package:titan/paiement/providers/my_history_provider.dart'; import 'package:titan/paiement/providers/my_stores_provider.dart'; import 'package:titan/paiement/providers/register_provider.dart'; import 'package:titan/paiement/providers/should_display_tos_dialog.dart'; +import 'package:titan/paiement/tools/key_service.dart'; +import 'package:titan/paiement/ui/components/paiment_delegate/paiment_delegate_modal.dart'; import 'package:titan/paiement/ui/pages/main_page/account_card/account_card.dart'; import 'package:titan/paiement/ui/pages/main_page/tos_dialog.dart'; import 'package:titan/paiement/ui/pages/main_page/account_card/last_transactions.dart'; @@ -20,6 +29,7 @@ import 'package:titan/tools/functions.dart'; import 'package:titan/tools/providers/path_forwarding_provider.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/layouts/refresher.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; class PaymentMainPage extends HookConsumerWidget { const PaymentMainPage({super.key}); @@ -44,8 +54,11 @@ class PaymentMainPage extends HookConsumerWidget { final mySellersNotifier = ref.read(myStoresProvider.notifier); final myHistoryNotifier = ref.read(myHistoryProvider.notifier); final myWalletNotifier = ref.read(myWalletProvider.notifier); - final isAdmin = ref.watch(isPaymentAdminProvider); + final isAdmin = ref.watch(isStructureAdminProvider); final flipped = useState(true); + final paymentRequests = ref.watch(paymentRequestsProvider); + final paymentRequestsNotifier = ref.read(paymentRequestsProvider.notifier); + final hasShownRequestModal = useState(false); ref.listen(pathForwardingProvider, (previous, next) async { final params = next.queryParameters; @@ -54,7 +67,11 @@ class PaymentMainPage extends HookConsumerWidget { !_handledKeys.contains("code")) { _handledKeys.add("code"); WidgetsBinding.instance.addPostFrameCallback((_) { - displayToast(context, TypeMsg.msg, "Paiement réussi"); + displayToast( + context, + TypeMsg.msg, + AppLocalizations.of(context)!.paiementSuccededTransaction, + ); }); } await mySellersNotifier.getMyStores(); @@ -78,6 +95,105 @@ class PaymentMainPage extends HookConsumerWidget { } } + Future showRequestModal(PaymentRequest request) async { + final keyService = KeyService(); + await showCustomBottomModal( + context: context, + ref: ref, + modal: PaimentDelegateModal( + itemTitle: request.name, + itemDescription: request.storeNote ?? '', + itemPrice: request.total, + onConfirm: () async { + final keyId = await keyService.getKeyId(); + final keyPair = await keyService.getKeyPair(); + if (keyId == null || keyPair == null) { + if (context.mounted) { + Navigator.of(context).pop(); + displayToast( + context, + TypeMsg.error, + AppLocalizations.of(context)!.paiementPaymentRequestError, + ); + } + return; + } + final now = DateTime.now(); + final validationData = RequestValidationData( + requestId: request.id, + key: keyId, + iat: now, + tot: request.total, + ); + final dataToSign = jsonEncode(validationData.toJson()); + final signature = await keyService.signMessage( + keyPair, + dataToSign.codeUnits, + ); + final validation = RequestValidation( + requestId: request.id, + key: keyId, + iat: now, + tot: request.total, + signature: base64Encode(signature.bytes), + ); + final success = await paymentRequestsNotifier.acceptRequest( + request, + validation, + ); + if (context.mounted) { + Navigator.of(context).pop(); + displayToast( + context, + success ? TypeMsg.msg : TypeMsg.error, + success + ? AppLocalizations.of( + context, + )!.paiementPaymentRequestAccepted + : AppLocalizations.of(context)!.paiementPaymentRequestError, + ); + if (success) { + await myHistoryNotifier.getHistory(); + await myWalletNotifier.getMyWallet(); + } + } + }, + onRefuse: () async { + final success = await paymentRequestsNotifier.refuseRequest( + request, + ); + if (context.mounted) { + Navigator.of(context).pop(); + displayToast( + context, + success ? TypeMsg.msg : TypeMsg.error, + success + ? AppLocalizations.of( + context, + )!.paiementPaymentRequestRefused + : AppLocalizations.of(context)!.paiementPaymentRequestError, + ); + } + }, + ), + ); + } + + useEffect(() { + paymentRequests.whenData((requests) { + final pendingRequests = requests + .where((r) => r.status == RequestStatus.proposed) + .toList(); + if (pendingRequests.isNotEmpty && !hasShownRequestModal.value) { + hasShownRequestModal.value = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + showRequestModal(pendingRequests.first); + }); + } + }); + return null; + }, [paymentRequests]); + tos.maybeWhen( orElse: () {}, error: (e, s) async { @@ -95,43 +211,52 @@ class PaymentMainPage extends HookConsumerWidget { return PaymentTemplate( child: shouldDisplayTosDialog - ? SingleChildScrollView( - child: TOSDialogBox( - descriptions: tos.maybeWhen( - orElse: () => '', - data: (tos) => tos.tosContent, + ? ScrollToHideNavbar( + controller: ScrollController(), + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: TOSDialogBox( + descriptions: tos.maybeWhen( + orElse: () => '', + data: (tos) => tos.tosContent, + ), + title: AppLocalizations.of(context)!.paiementNewCGU, + onYes: () { + tos.maybeWhen( + orElse: () {}, + data: (tos) async { + final value = await tosNotifier.signTOS( + tos.copyWith( + acceptedTosVersion: tos.latestTosVersion, + ), + ); + if (value) { + await mySellersNotifier.getMyStores(); + await myHistoryNotifier.getHistory(); + await myWalletNotifier.getMyWallet(); + shouldDisplayTosDialogNotifier.update(false); + hasAcceptedToSNotifier.update(true); + } + }, + ); + }, + onNo: () { + shouldDisplayTosDialogNotifier.update(false); + }, ), - title: "Nouvelles Conditions Générales d'Utilisation", - onYes: () { - tos.maybeWhen( - orElse: () {}, - data: (tos) async { - final value = await tosNotifier.signTOS( - tos.copyWith(acceptedTosVersion: tos.latestTosVersion), - ); - if (value) { - await mySellersNotifier.getMyStores(); - await myHistoryNotifier.getHistory(); - await myWalletNotifier.getMyWallet(); - shouldDisplayTosDialogNotifier.update(false); - hasAcceptedToSNotifier.update(true); - } - }, - ); - }, - onNo: () { - shouldDisplayTosDialogNotifier.update(false); - }, ), ) : LayoutBuilder( builder: (context, constraints) { return Refresher( + controller: ScrollController(), onRefresh: () async { await mySellersNotifier.getMyStores(); await myHistoryNotifier.getHistory(); await myWalletNotifier.getMyWallet(); await tosNotifier.getTOS(); + hasShownRequestModal.value = false; + await paymentRequestsNotifier.getRequests(); }, child: Column( children: [ diff --git a/lib/paiement/ui/pages/main_page/seller_card/admin_invoice_card.dart b/lib/paiement/ui/pages/main_page/seller_card/admin_invoice_card.dart new file mode 100644 index 0000000000..a6bde2e160 --- /dev/null +++ b/lib/paiement/ui/pages/main_page/seller_card/admin_invoice_card.dart @@ -0,0 +1,60 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/paiement/providers/invoice_list_provider.dart'; +import 'package:titan/paiement/router.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; + +class InvoiceAdminCard extends ConsumerWidget { + const InvoiceAdminCard({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final invoicesNotifier = ref.watch(invoiceListProvider.notifier); + return GestureDetector( + behavior: HitTestBehavior.opaque, + child: Container( + height: 70, + padding: const EdgeInsets.symmetric(horizontal: 20), + width: MediaQuery.of(context).size.width, + child: Row( + children: [ + CircleAvatar( + radius: 27, + backgroundColor: Color.fromARGB(255, 6, 75, 75), + child: HeroIcon( + HeroIcons.documentCurrencyEuro, + color: ColorConstants.background, + ), + ), + SizedBox(width: 15), + Expanded( + child: AutoSizeText( + AppLocalizations.of(context)!.paiementBillingSpace, + maxLines: 1, + style: TextStyle( + color: Color.fromARGB(255, 0, 29, 29), + fontSize: 14, + ), + ), + ), + SizedBox(width: 10), + HeroIcon( + HeroIcons.arrowRight, + color: Color.fromARGB(255, 0, 29, 29), + size: 25, + ), + ], + ), + ), + onTap: () { + tokenExpireWrapper(ref, () => invoicesNotifier.getInvoices()); + QR.to(PaymentRouter.root + PaymentRouter.invoicesAdmin); + }, + ); + } +} diff --git a/lib/paiement/ui/pages/main_page/seller_card/store_admin_card.dart b/lib/paiement/ui/pages/main_page/seller_card/store_admin_card.dart deleted file mode 100644 index b0b862587b..0000000000 --- a/lib/paiement/ui/pages/main_page/seller_card/store_admin_card.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:auto_size_text/auto_size_text.dart'; -import 'package:flutter/material.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/paiement/providers/my_structures_provider.dart'; -import 'package:titan/paiement/providers/selected_structure_provider.dart'; -import 'package:titan/paiement/router.dart'; -import 'package:qlevar_router/qlevar_router.dart'; - -class StoreAdminCard extends ConsumerWidget { - const StoreAdminCard({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final myStructures = ref.watch(myStructuresProvider); - final selectedStructureNotifier = ref.read( - selectedStructureProvider.notifier, - ); - return Column( - children: myStructures.map((structure) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - child: Container( - height: 70, - padding: const EdgeInsets.symmetric(horizontal: 20), - width: MediaQuery.of(context).size.width, - child: Row( - children: [ - CircleAvatar( - radius: 27, - backgroundColor: Color.fromARGB(255, 6, 75, 75), - ), - SizedBox(width: 15), - Expanded( - child: AutoSizeText( - "Gestion des assocations ${structure.name}", - maxLines: 2, - style: TextStyle( - color: Color.fromARGB(255, 0, 29, 29), - fontSize: 14, - ), - ), - ), - SizedBox(width: 10), - HeroIcon( - HeroIcons.arrowRight, - color: Color.fromARGB(255, 0, 29, 29), - size: 25, - ), - ], - ), - ), - onTap: () { - selectedStructureNotifier.setStructure(structure); - QR.to(PaymentRouter.root + PaymentRouter.admin); - }, - ); - }).toList(), - ); - } -} diff --git a/lib/paiement/ui/pages/main_page/seller_card/store_card.dart b/lib/paiement/ui/pages/main_page/seller_card/store_card.dart index 8d3a7b7e8e..ee7c0faf09 100644 --- a/lib/paiement/ui/pages/main_page/seller_card/store_card.dart +++ b/lib/paiement/ui/pages/main_page/seller_card/store_card.dart @@ -2,6 +2,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/paiement/providers/barcode_provider.dart'; import 'package:titan/paiement/providers/ongoing_transaction.dart'; import 'package:titan/paiement/providers/selected_store_provider.dart'; @@ -9,6 +10,7 @@ import 'package:titan/paiement/router.dart'; import 'package:titan/paiement/ui/pages/main_page/main_card_button.dart'; import 'package:titan/paiement/ui/pages/main_page/main_card_template.dart'; import 'package:titan/paiement/ui/pages/scan_page/scan_page.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; import 'package:titan/user/providers/user_provider.dart'; import 'package:qlevar_router/qlevar_router.dart'; @@ -36,25 +38,28 @@ class StoreCard extends HookConsumerWidget { Color.fromARGB(255, 0, 68, 68), Color.fromARGB(255, 0, 29, 29), ], - title: 'Solde associatif', + title: AppLocalizations.of(context)!.paiementStoreBalance, actionButtons: [ if (store.canBank) MainCardButton( colors: buttonGradient, icon: HeroIcons.viewfinderCircle, - title: "Scanner", + title: AppLocalizations.of(context)!.paiementScan, onPressed: () async { - showModalBottomSheet( + showCustomBottomModal( context: context, - enableDrag: false, - backgroundColor: Colors.transparent, - scrollControlDisabledMaxHeightRatio: - (1 - 80 / MediaQuery.of(context).size.height), - builder: (context) => ScanPage(), - ).then((_) { - ongoingTransactionNotifier.clearOngoingTransaction(); - barcodeNotifier.clearBarcode(); - }); + modal: ScanPage(), + ref: ref, + onCloseCallback: () { + ongoingTransactionNotifier.clearOngoingTransaction(); + barcodeNotifier.clearBarcode(); + }, + // enableDrag: false, + // backgroundColor: Colors.transparent, + // scrollControlDisabledMaxHeightRatio: + // (1 - 80 / MediaQuery.of(context).size.height), + // builder: (context) => ScanPage(), + ); }, ), if (store.canManageSellers) @@ -65,7 +70,7 @@ class StoreCard extends HookConsumerWidget { // storeAdminListNotifier.getStoreAdminList(store.id); QR.to(PaymentRouter.root + PaymentRouter.storeAdmin); }, - title: 'Gestion', + title: AppLocalizations.of(context)!.paiementManagement, ), if (store.canSeeHistory) MainCardButton( @@ -74,7 +79,7 @@ class StoreCard extends HookConsumerWidget { onPressed: () async { QR.to(PaymentRouter.root + PaymentRouter.storeStats); }, - title: 'Historique', + title: AppLocalizations.of(context)!.paiementHistory, ), if (store.structure.managerUser.id == me.id) MainCardButton( @@ -83,7 +88,7 @@ class StoreCard extends HookConsumerWidget { onPressed: () async { QR.to(PaymentRouter.root + PaymentRouter.transferStructure); }, - title: 'Passation', + title: AppLocalizations.of(context)!.paiementHandOver, ), ], child: SizedBox.expand( diff --git a/lib/paiement/ui/pages/main_page/seller_card/store_list.dart b/lib/paiement/ui/pages/main_page/seller_card/store_list.dart index 712d1758d1..7208083b63 100644 --- a/lib/paiement/ui/pages/main_page/seller_card/store_list.dart +++ b/lib/paiement/ui/pages/main_page/seller_card/store_list.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/paiement/class/user_store.dart'; import 'package:titan/paiement/providers/is_payment_admin.dart'; import 'package:titan/paiement/providers/my_stores_provider.dart'; -import 'package:titan/paiement/ui/pages/main_page/seller_card/store_admin_card.dart'; +import 'package:titan/paiement/ui/pages/main_page/seller_card/admin_invoice_card.dart'; +import 'package:titan/paiement/ui/pages/main_page/seller_card/structure_admin_card.dart'; import 'package:titan/paiement/ui/pages/main_page/seller_card/store_divider.dart'; import 'package:titan/paiement/ui/pages/main_page/seller_card/store_seller_card.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; @@ -15,7 +17,8 @@ class StoreList extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final stores = ref.watch(myStoresProvider); - final isAdmin = ref.watch(isPaymentAdminProvider); + final isStructureAdmin = ref.watch(isStructureAdminProvider); + final isBankAccountHolder = ref.watch(isBankAccountHolderProvider); return SizedBox( height: maxHeight, child: SingleChildScrollView( @@ -25,8 +28,8 @@ class StoreList extends ConsumerWidget { Container( padding: const EdgeInsets.symmetric(horizontal: 30), alignment: Alignment.centerLeft, - child: const Text( - "Associations", + child: Text( + AppLocalizations.of(context)!.paiementStores, style: TextStyle( color: Color.fromARGB(255, 0, 29, 29), fontSize: 20, @@ -48,9 +51,12 @@ class StoreList extends ConsumerWidget { } return Column( children: [ - if (isAdmin) ...[ - const StoreDivider(name: "Administrateur"), - const StoreAdminCard(), + if (isStructureAdmin) ...[ + StoreDivider( + name: AppLocalizations.of(context)!.paiementAdmin, + ), + if (isBankAccountHolder) const InvoiceAdminCard(), + const StructureAdminCard(), ], ...sortedByMembership.map((membership, stores) { final List alphabeticallyOrderedStores = stores @@ -70,7 +76,7 @@ class StoreList extends ConsumerWidget { ); }, ), - const SizedBox(height: 15), + const SizedBox(height: 80), ], ), ), diff --git a/lib/paiement/ui/pages/main_page/seller_card/structure_admin_card.dart b/lib/paiement/ui/pages/main_page/seller_card/structure_admin_card.dart new file mode 100644 index 0000000000..c89fcb8964 --- /dev/null +++ b/lib/paiement/ui/pages/main_page/seller_card/structure_admin_card.dart @@ -0,0 +1,107 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/paiement/providers/invoice_list_provider.dart'; +import 'package:titan/paiement/providers/my_structures_provider.dart'; +import 'package:titan/paiement/providers/selected_structure_provider.dart'; +import 'package:titan/paiement/router.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; + +class StructureAdminCard extends ConsumerWidget { + const StructureAdminCard({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final myStructures = ref.watch(myStructuresProvider); + final selectedStructureNotifier = ref.read( + selectedStructureProvider.notifier, + ); + final invoicesNotifier = ref.watch(invoiceListProvider.notifier); + + final localizeWithContext = AppLocalizations.of(context)!; + + return Column( + children: myStructures.map((structure) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + child: Container( + height: 70, + padding: const EdgeInsets.symmetric(horizontal: 20), + width: MediaQuery.of(context).size.width, + child: Row( + children: [ + CircleAvatar( + radius: 27, + backgroundColor: Color.fromARGB(255, 6, 75, 75), + ), + SizedBox(width: 15), + Expanded( + child: AutoSizeText( + localizeWithContext.paiementStructureManagement( + structure.name, + ), + maxLines: 2, + style: TextStyle( + color: Color.fromARGB(255, 0, 29, 29), + fontSize: 14, + ), + ), + ), + SizedBox(width: 10), + HeroIcon( + HeroIcons.arrowRight, + color: Color.fromARGB(255, 0, 29, 29), + size: 25, + ), + ], + ), + ), + onTap: () { + showCustomBottomModal( + context: context, + modal: BottomModalTemplate( + title: structure.name, + child: Column( + children: [ + Button( + text: localizeWithContext.paiementStores, + onPressed: () { + Navigator.of(context).pop(); + selectedStructureNotifier.setStructure(structure); + QR.to( + PaymentRouter.root + PaymentRouter.structureStores, + ); + }, + ), + const SizedBox(height: 10), + Button( + text: localizeWithContext.paiementInvoices, + onPressed: () { + Navigator.of(context).pop(); + tokenExpireWrapper( + ref, + () => invoicesNotifier.getStructureInvoices( + structure.id, + ), + ); + QR.to( + PaymentRouter.root + PaymentRouter.invoicesStructure, + ); + }, + ), + ], + ), + ), + ref: ref, + ); + }, + ); + }).toList(), + ); + } +} diff --git a/lib/paiement/ui/pages/main_page/tos_dialog.dart b/lib/paiement/ui/pages/main_page/tos_dialog.dart index 53a2150afb..12ffb1afe7 100644 --- a/lib/paiement/ui/pages/main_page/tos_dialog.dart +++ b/lib/paiement/ui/pages/main_page/tos_dialog.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/tools/constants.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; class TOSDialogBox extends StatelessWidget { final String title, descriptions; - static const Color titleColor = Color.fromARGB(255, 4, 84, 84); + static const Color titleColor = ColorConstants.onMain; static const Color descriptionColor = Colors.black; static const Color yesColor = Color.fromARGB(255, 9, 103, 103); static const Color noColor = ColorConstants.background2; @@ -14,7 +15,6 @@ class TOSDialogBox extends StatelessWidget { final Function()? onNo; static const double _padding = 20; - static const double _avatarRadius = 45; static const Color background = Color(0xfffafafa); const TOSDialogBox({ @@ -27,93 +27,70 @@ class TOSDialogBox extends StatelessWidget { @override Widget build(BuildContext context) { - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(TOSDialogBox._padding), - ), - elevation: 0, - backgroundColor: Colors.transparent, - child: Stack( + return Container( + padding: const EdgeInsets.all(TOSDialogBox._padding), + color: ColorConstants.background, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Container( - padding: const EdgeInsets.all(TOSDialogBox._padding), - margin: const EdgeInsets.only(top: TOSDialogBox._avatarRadius), - decoration: BoxDecoration( - shape: BoxShape.rectangle, - color: TOSDialogBox.background, - borderRadius: BorderRadius.circular(TOSDialogBox._padding), - boxShadow: [ - BoxShadow( - color: Colors.grey.shade400, - offset: const Offset(0, 5), - blurRadius: 5, - ), - ], + Text( + title, + style: const TextStyle( + fontSize: 25, + fontWeight: FontWeight.w800, + color: titleColor, ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - title, - style: const TextStyle( - fontSize: 25, - fontWeight: FontWeight.w800, - color: titleColor, - ), - ), - const SizedBox(height: 15), - MarkdownBody( - data: descriptions, - selectable: true, - styleSheet: MarkdownStyleSheet( - h2Padding: const EdgeInsets.only(top: 20.0), - textAlign: WrapAlignment.spaceAround, + ), + const SizedBox(height: 15), + MarkdownBody( + data: descriptions, + selectable: true, + styleSheet: MarkdownStyleSheet( + h2Padding: const EdgeInsets.only(top: 20.0), + textAlign: WrapAlignment.spaceAround, + ), + ), + const SizedBox(height: 22), + Align( + alignment: Alignment.bottomCenter, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + TextButton( + onPressed: () { + onNo == null ? Navigator.of(context).pop() : onNo?.call(); + }, + child: Text( + AppLocalizations.of(context)!.paiementDecline, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: noColor, + ), ), ), - const SizedBox(height: 22), - Align( - alignment: Alignment.bottomCenter, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - TextButton( - onPressed: () { - onNo == null - ? Navigator.of(context).pop() - : onNo?.call(); - }, - child: const Text( - "Refuser", - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: noColor, - ), - ), - ), - WaitingButton( - onTap: () async { - if (onNo == null) { - Navigator.of(context).pop(); - } - await onYes(); - }, - builder: (child) => child, - child: const Text( - "Accepter", - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: yesColor, - ), - ), - ), - ], + WaitingButton( + onTap: () async { + if (onNo == null) { + Navigator.of(context).pop(); + } + await onYes(); + }, + builder: (child) => child, + child: Text( + AppLocalizations.of(context)!.paiementAccept, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: yesColor, + ), ), ), + SizedBox(height: 40), ], ), ), + const SizedBox(height: 20), ], ), ); diff --git a/lib/paiement/ui/pages/pay_page/confirm_button.dart b/lib/paiement/ui/pages/pay_page/confirm_button.dart index ea756c5209..b2c36140cb 100644 --- a/lib/paiement/ui/pages/pay_page/confirm_button.dart +++ b/lib/paiement/ui/pages/pay_page/confirm_button.dart @@ -6,6 +6,7 @@ import 'package:local_auth/local_auth.dart'; import 'package:local_auth_android/local_auth_android.dart'; import 'package:local_auth_darwin/local_auth_darwin.dart'; import 'package:titan/event/tools/functions.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/paiement/providers/key_service_provider.dart'; import 'package:titan/paiement/providers/my_history_provider.dart'; import 'package:titan/paiement/providers/my_wallet_provider.dart'; @@ -13,6 +14,7 @@ import 'package:titan/paiement/providers/pay_amount_provider.dart'; import 'package:titan/paiement/ui/pages/pay_page/info_card.dart'; import 'package:titan/paiement/ui/pages/pay_page/qr_code.dart'; import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/providers/locale_notifier.dart'; import 'package:titan/tools/ui/layouts/add_edit_button_layout.dart'; class ConfirmButton extends ConsumerWidget { @@ -20,6 +22,7 @@ class ConfirmButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final locale = ref.watch(localeProvider); final keyService = ref.watch(keyServiceProvider); final payAmount = ref.watch(payAmountProvider); final payAmountNotifier = ref.watch(payAmountProvider.notifier); @@ -43,7 +46,10 @@ class ConfirmButton extends ConsumerWidget { final enabled = amount > 0 && amount * 100 <= myWalletBalance; - final formatter = NumberFormat("#,##0.00", "fr_FR"); + final formatter = NumberFormat.currency( + locale: locale.toString(), + symbol: "€", + ); void displayQRModal() { showModalBottomSheet( @@ -64,17 +70,18 @@ class ConfirmButton extends ConsumerWidget { const SizedBox(height: 30), Row( children: [ - const SizedBox(width: 20), + SizedBox(width: 20), InfoCard( icons: HeroIcons.currencyEuro, - title: "Montant", - value: - '${formatter.format(double.parse(payAmount.replaceAll(',', '.')))} €', + title: AppLocalizations.of(context)!.paiementAmount, + value: formatter.format( + double.parse(payAmount.replaceAll(',', '.')), + ), ), const SizedBox(width: 10), InfoCard( icons: HeroIcons.clock, - title: "Valide jusqu'à", + title: AppLocalizations.of(context)!.paiementValidUntil, value: processDateOnlyHour( DateTime.now().add(const Duration(minutes: 5)), ), @@ -90,8 +97,8 @@ class ConfirmButton extends ConsumerWidget { child: GestureDetector( child: AddEditButtonLayout( color: Colors.grey.shade200.withValues(alpha: 0.5), - child: const Text( - 'Fermer', + child: Text( + AppLocalizations.of(context)!.paiementClose, style: TextStyle( color: Colors.black, fontWeight: FontWeight.bold, @@ -120,32 +127,38 @@ class ConfirmButton extends ConsumerWidget { if (!enabled) { displayToastWithContext( TypeMsg.error, - 'Veuillez entrer un montant valide', + AppLocalizations.of(context)!.paiementPleaseEnterValidAmount, ); return; } + final authentificationFailedMsg = AppLocalizations.of( + context, + )!.paiementAuthentificationFailed; + final pleaseAddDeviceMsg = AppLocalizations.of( + context, + )!.paiementPleaseAddDevice; final bool didAuthenticate = await auth.authenticate( - localizedReason: 'Veuillez vous authentifier pour payer', + localizedReason: AppLocalizations.of( + context, + )!.paiementPleaseAuthenticate, authMessages: [ - const AndroidAuthMessages( - signInTitle: 'L\'authentification est requise pour payer', - cancelButton: 'Non merci', + AndroidAuthMessages( + signInTitle: AppLocalizations.of( + context, + )!.paiementAuthenticationRequired, + cancelButton: AppLocalizations.of(context)!.paiementNoThanks, + ), + IOSAuthMessages( + cancelButton: AppLocalizations.of(context)!.paiementNoThanks, ), - const IOSAuthMessages(cancelButton: 'Non merci'), ], ); if (!didAuthenticate) { - displayToastWithContext( - TypeMsg.error, - 'L\'authentification a échoué', - ); + displayToastWithContext(TypeMsg.error, authentificationFailedMsg); return; } if ((await keyService.getKeyId()) == null) { - displayToastWithContext( - TypeMsg.error, - 'Veuillez ajouter cet appareil pour payer', - ); + displayToastWithContext(TypeMsg.error, pleaseAddDeviceMsg); return; } displayQRModal(); diff --git a/lib/paiement/ui/pages/pay_page/pay_page.dart b/lib/paiement/ui/pages/pay_page/pay_page.dart index d659d25576..208e802698 100644 --- a/lib/paiement/ui/pages/pay_page/pay_page.dart +++ b/lib/paiement/ui/pages/pay_page/pay_page.dart @@ -1,17 +1,20 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:intl/intl.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/paiement/providers/my_wallet_provider.dart'; import 'package:titan/paiement/providers/pay_amount_provider.dart'; import 'package:titan/paiement/ui/pages/pay_page/confirm_button.dart'; import 'package:titan/paiement/ui/components/digit_fade_in_animation.dart'; import 'package:titan/paiement/ui/components/keyboard.dart'; +import 'package:titan/tools/providers/locale_notifier.dart'; class PayPage extends ConsumerWidget { const PayPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final locale = ref.watch(localeProvider); final payAmount = ref.watch(payAmountProvider); final payAmountNotifier = ref.watch(payAmountProvider.notifier); final myWallet = ref.watch(myWalletProvider); @@ -19,7 +22,10 @@ class PayPage extends ConsumerWidget { orElse: () => 0, data: (wallet) => wallet.balance / 100, ); - final formatter = NumberFormat("#,##0.00", "fr_FR"); + final formatter = NumberFormat.currency( + locale: locale.toString(), + symbol: "€", + ); final amountToSub = double.tryParse(payAmount.replaceAll(",", ".")) ?? 0; @@ -37,11 +43,12 @@ class PayPage extends ConsumerWidget { end: Alignment.bottomRight, ), ), + height: MediaQuery.of(context).size.height * 0.8, child: Column( children: [ const SizedBox(height: 20), Text( - 'Paiement', + AppLocalizations.of(context)!.paiementPayment, style: const TextStyle( color: Colors.white, fontSize: 20, @@ -50,7 +57,7 @@ class PayPage extends ConsumerWidget { ), const SizedBox(height: 5), Text( - 'Solde après paiement : ${formatter.format(currentAmount - amountToSub)} €', + '${AppLocalizations.of(context)!.paiementBalanceAfterTransaction} ${formatter.format(currentAmount - amountToSub)}', style: const TextStyle(color: Colors.white, fontSize: 15), ), Expanded( diff --git a/lib/paiement/ui/pages/scan_page/cancel_button.dart b/lib/paiement/ui/pages/scan_page/cancel_button.dart index 5c0ce64b0e..1e56db1289 100644 --- a/lib/paiement/ui/pages/scan_page/cancel_button.dart +++ b/lib/paiement/ui/pages/scan_page/cancel_button.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; class CancelButton extends HookWidget { @@ -90,7 +91,7 @@ class CancelButton extends HookWidget { height: 50, alignment: Alignment.center, child: Text( - 'Annuler (${((1 - disablingAnimationController.value) * 30).toStringAsFixed(0)}s)', + '${AppLocalizations.of(context)!.paiementCancel} (${((1 - disablingAnimationController.value) * 30).toStringAsFixed(0)}s)', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, diff --git a/lib/paiement/ui/pages/scan_page/scan_page.dart b/lib/paiement/ui/pages/scan_page/scan_page.dart index 9f1ee00ba5..39a38ac68f 100644 --- a/lib/paiement/ui/pages/scan_page/scan_page.dart +++ b/lib/paiement/ui/pages/scan_page/scan_page.dart @@ -3,6 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:intl/intl.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/paiement/providers/barcode_provider.dart'; import 'package:titan/paiement/providers/bypass_provider.dart'; import 'package:titan/paiement/providers/ongoing_transaction.dart'; @@ -12,6 +13,7 @@ import 'package:titan/paiement/ui/pages/scan_page/cancel_button.dart'; import 'package:titan/paiement/ui/pages/scan_page/scanner.dart'; import 'package:titan/tools/exception.dart'; import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/providers/locale_notifier.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; @@ -24,12 +26,16 @@ class ScanPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final locale = ref.watch(localeProvider); final bypass = ref.watch(bypassProvider); final store = ref.watch(selectedStoreProvider); final bypassNotifier = ref.watch(bypassProvider.notifier); final barcode = ref.watch(barcodeProvider); final barcodeNotifier = ref.watch(barcodeProvider.notifier); - final formatter = NumberFormat("#,##0.00", "fr_FR"); + final formatter = NumberFormat.currency( + locale: locale.toString(), + symbol: "€", + ); final transactionNotifier = ref.watch(transactionProvider.notifier); final ongoingTransaction = ref.watch(ongoingTransactionProvider); final ongoingTransactionNotifier = ref.watch( @@ -42,339 +48,354 @@ class ScanPage extends HookConsumerWidget { final opacity = useAnimationController(duration: const Duration(seconds: 1)) ..repeat(reverse: true); - return Stack( - children: [ - Scanner(key: scannerKey), - store.structure.associationMembership.id != '' - ? Positioned( - top: 10, - left: 20, - child: SizedBox( - width: MediaQuery.of(context).size.width - 40, - child: Row( - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - bypassNotifier.setBypass(!bypass); - }, - child: Row( - children: [ - Checkbox( - value: !bypass, - checkColor: Colors.black, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), - ), - side: const BorderSide( - color: Colors.white, - width: 1.5, + return SizedBox( + height: MediaQuery.of(context).size.height * 0.9, + child: Stack( + children: [ + Scanner(key: scannerKey), + store.structure.associationMembership.id != '' + ? Positioned( + top: 10, + left: 20, + child: SizedBox( + width: MediaQuery.of(context).size.width - 40, + child: Row( + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + bypassNotifier.setBypass(!bypass); + }, + child: Row( + children: [ + Checkbox( + value: !bypass, + checkColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + side: const BorderSide( + color: Colors.white, + width: 1.5, + ), + activeColor: Colors.white, + onChanged: (value) { + bypassNotifier.setBypass(!bypass); + }, ), - activeColor: Colors.white, - onChanged: (value) { - bypassNotifier.setBypass(!bypass); - }, - ), - const SizedBox(width: 5), - Text( - "Limité à ${store.structure.associationMembership.name}", - style: TextStyle( - color: bypass - ? Colors.white.withValues(alpha: 0.5) - : Colors.white, - fontSize: 15, + const SizedBox(width: 5), + Text( + "${AppLocalizations.of(context)!.paiementLimitedTo} ${store.structure.associationMembership.name}", + style: TextStyle( + color: bypass + ? Colors.white.withValues(alpha: 0.5) + : Colors.white, + fontSize: 15, + ), ), - ), - ], + ], + ), ), - ), - Spacer(), - GestureDetector( - onTap: () { - Navigator.pop(context); - }, - child: const HeroIcon( - HeroIcons.xMark, - size: 20, - color: Colors.white, + Spacer(), + GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: const HeroIcon( + HeroIcons.xMark, + size: 20, + color: Colors.white, + ), ), - ), - ], + ], + ), ), - ), - ) - : Positioned( - top: 20, - right: 20, - child: GestureDetector( - onTap: () { - Navigator.pop(context); - }, - child: const HeroIcon( - HeroIcons.xMark, - size: 20, - color: Colors.white, + ) + : Positioned( + top: 20, + right: 20, + child: GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: const HeroIcon( + HeroIcons.xMark, + size: 20, + color: Colors.white, + ), ), ), - ), - Column( - children: [ - Expanded( - child: Column( - children: [ - const SizedBox(height: 60), - barcode != null - ? Row( - children: [ - const Spacer(), - AsyncChild( - value: ongoingTransaction, - builder: (context, transaction) { - return Container( - padding: const EdgeInsets.symmetric( - vertical: 20, - horizontal: 50, - ), - decoration: BoxDecoration( - gradient: const RadialGradient( - colors: [ - Color(0xff79a400), - Color(0xff387200), - ], - center: Alignment.topLeft, - radius: 2, + Column( + children: [ + Expanded( + child: Column( + children: [ + const SizedBox(height: 60), + barcode != null + ? Row( + children: [ + const Spacer(), + AsyncChild( + value: ongoingTransaction, + builder: (context, transaction) { + return Container( + padding: const EdgeInsets.symmetric( + vertical: 20, + horizontal: 50, ), - borderRadius: BorderRadius.circular(20), - ), - child: Column( - children: [ - Text( - "Montant", - style: TextStyle( - fontSize: 13, - color: Colors.white, - ), + decoration: BoxDecoration( + gradient: const RadialGradient( + colors: [ + Color(0xff79a400), + Color(0xff387200), + ], + center: Alignment.topLeft, + radius: 2, ), - Text( - '${formatter.format(barcode.tot / 100)} €', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 25, - color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + child: Column( + children: [ + Text( + AppLocalizations.of( + context, + )!.paiementAmount, + style: TextStyle( + fontSize: 13, + color: Colors.white, + ), + ), + Text( + formatter.format(barcode.tot / 100), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 25, + color: Colors.white, + ), ), + ], + ), + ); + }, + errorBuilder: (error, stack) { + return Container( + padding: const EdgeInsets.symmetric( + vertical: 20, + horizontal: 50, + ), + decoration: BoxDecoration( + gradient: const RadialGradient( + colors: [ + Color(0xffa40000), + Color(0xff720000), + ], + center: Alignment.topLeft, + radius: 2, ), - ], - ), - ); - }, - errorBuilder: (error, stack) { - return Container( + borderRadius: BorderRadius.circular(20), + ), + child: Text( + (error as AppException).message, + style: TextStyle( + fontSize: 15, + color: Colors.white, + ), + ), + ); + }, + loadingBuilder: (context) => Container( padding: const EdgeInsets.symmetric( vertical: 20, horizontal: 50, ), decoration: BoxDecoration( - gradient: const RadialGradient( + gradient: RadialGradient( colors: [ - Color(0xffa40000), - Color(0xff720000), + Colors.grey.shade200, + Colors.grey.shade300, ], center: Alignment.topLeft, radius: 2, ), borderRadius: BorderRadius.circular(20), ), - child: Text( - (error as AppException).message, - style: TextStyle( - fontSize: 15, - color: Colors.white, - ), - ), - ); - }, - loadingBuilder: (context) => Container( - padding: const EdgeInsets.symmetric( - vertical: 20, - horizontal: 50, - ), - decoration: BoxDecoration( - gradient: RadialGradient( - colors: [ - Colors.grey.shade200, - Colors.grey.shade300, - ], - center: Alignment.topLeft, - radius: 2, - ), - borderRadius: BorderRadius.circular(20), + child: Loader(), ), - child: Loader(), ), - ), - const Spacer(), - ], - ) - : SizedBox( - height: 100, - child: Center( - child: AnimatedBuilder( - animation: opacity, - builder: (context, child) { - return Opacity( - opacity: opacity.value, - child: const Text( - 'Scanner un code', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.white, + const Spacer(), + ], + ) + : SizedBox( + height: 100, + child: Center( + child: AnimatedBuilder( + animation: opacity, + builder: (context, child) { + return Opacity( + opacity: opacity.value, + child: Text( + AppLocalizations.of( + context, + )!.paiementScanCode, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), ), - ), - ); - }, + ); + }, + ), ), ), - ), - ], + ], + ), ), - ), - // Qr code scanning zone - SizedBox(height: MediaQuery.of(context).size.width * 0.8), - Expanded( - child: Column( - children: [ - const Spacer(), - AsyncChild( - value: ongoingTransaction, - errorBuilder: (context, child) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 30), - child: GestureDetector( - child: Container( - width: double.infinity, - height: 50, - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Colors.white.withValues(alpha: 0.8), - ), - child: const Text( - 'Suivant', - style: TextStyle( - color: Colors.black, - fontWeight: FontWeight.bold, - fontSize: 20, + // Qr code scanning zone + SizedBox(height: MediaQuery.of(context).size.width * 0.8), + Expanded( + child: Column( + children: [ + const Spacer(), + AsyncChild( + value: ongoingTransaction, + errorBuilder: (errorContext, child) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 30), + child: GestureDetector( + child: Container( + width: double.infinity, + height: 50, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.white.withValues(alpha: 0.8), + ), + child: Text( + AppLocalizations.of(context)!.paiementNext, + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 20, + ), ), ), + onTap: () { + scannerKey.currentState?.resetScanner(); + barcodeNotifier.clearBarcode(); + ongoingTransactionNotifier + .clearOngoingTransaction(); + }, ), - onTap: () { - scannerKey.currentState?.resetScanner(); - barcodeNotifier.clearBarcode(); - ongoingTransactionNotifier.clearOngoingTransaction(); - }, ), - ), - builder: (context, transaction) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 30), - child: Row( - children: [ - CancelButton( - onCancel: (bool isInTime) async { - if (isInTime) { - await showDialog( - context: context, - builder: (context) { - return CustomDialogBox( - title: "Annuler la transaction", - descriptions: - "Voulez-vous vraiment annuler la transaction de ${formatter.format(transaction.total / 100)} € ?", - onYes: () async { - tokenExpireWrapper(ref, () async { - final value = - await transactionNotifier - .cancelTransaction( - transaction.id, + builder: (context, transaction) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 30), + child: Row( + children: [ + CancelButton( + onCancel: (bool isInTime) async { + if (isInTime) { + await showDialog( + context: context, + builder: (context) { + return CustomDialogBox( + title: AppLocalizations.of( + context, + )!.paiementCancelTransaction, + descriptions: + "${AppLocalizations.of(context)!.paiementTransactionCancelledDescription} ${formatter.format(transaction.total / 100)} ?", + onYes: () async { + tokenExpireWrapper(ref, () async { + final value = + await transactionNotifier + .cancelTransaction( + transaction.id, + ); + value.when( + data: (value) { + if (value) { + displayToastWithContext( + TypeMsg.msg, + AppLocalizations.of( + context, + )!.paiementTransactionCancelled, ); - value.when( - data: (value) { - if (value) { - displayToastWithContext( - TypeMsg.msg, - "Transaction annulée", - ); - ref - .read( - ongoingTransactionProvider - .notifier, - ) + ref + .read( + ongoingTransactionProvider + .notifier, + ) + .clearOngoingTransaction(); + } else { + displayToastWithContext( + TypeMsg.error, + AppLocalizations.of( + context, + )!.paiementTransactionCancelledError, + ); + } + ongoingTransactionNotifier .clearOngoingTransaction(); - } else { + barcodeNotifier.clearBarcode(); + }, + error: (error, stack) { displayToastWithContext( TypeMsg.error, - "Erreur lors de l'annulation", + error.toString(), ); - } - ongoingTransactionNotifier - .clearOngoingTransaction(); - barcodeNotifier.clearBarcode(); - }, - error: (error, stack) { - displayToastWithContext( - TypeMsg.error, - error.toString(), - ); - }, - loading: () {}, - ); - }); - scannerKey.currentState?.resetScanner(); - }, - ); - }, - ); - } - }, - ), - const SizedBox(width: 20), - Expanded( - child: GestureDetector( - child: Container( - width: double.infinity, - height: 50, - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Colors.white.withValues(alpha: 0.8), - ), - child: const Text( - 'Suivant', - style: TextStyle( - color: Colors.black, - fontWeight: FontWeight.bold, - fontSize: 20, + }, + loading: () {}, + ); + }); + scannerKey.currentState + ?.resetScanner(); + }, + ); + }, + ); + } + }, + ), + const SizedBox(width: 20), + Expanded( + child: GestureDetector( + child: Container( + width: double.infinity, + height: 50, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.white.withValues(alpha: 0.8), + ), + child: Text( + AppLocalizations.of(context)!.paiementNext, + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 20, + ), ), ), + onTap: () { + scannerKey.currentState?.resetScanner(); + barcodeNotifier.clearBarcode(); + ongoingTransactionNotifier + .clearOngoingTransaction(); + }, ), - onTap: () { - scannerKey.currentState?.resetScanner(); - barcodeNotifier.clearBarcode(); - ongoingTransactionNotifier - .clearOngoingTransaction(); - }, ), - ), - ], + ], + ), ), + loadingBuilder: (context) => const SizedBox(), ), - loadingBuilder: (context) => const SizedBox(), - ), - const Spacer(), - ], + const Spacer(), + ], + ), ), - ), - ], - ), - ], + ], + ), + ], + ), ); } } diff --git a/lib/paiement/ui/pages/scan_page/scanner.dart b/lib/paiement/ui/pages/scan_page/scanner.dart index 8235f9572b..69223197e7 100644 --- a/lib/paiement/ui/pages/scan_page/scanner.dart +++ b/lib/paiement/ui/pages/scan_page/scanner.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/paiement/providers/barcode_provider.dart'; import 'package:titan/paiement/providers/bypass_provider.dart'; import 'package:titan/paiement/providers/last_time_scanned.dart'; @@ -35,6 +36,7 @@ class ScannerState extends ConsumerState with WidgetsBindingObserver { setState(() { scannedValue = null; }); + controller.stop(); controller.start(); } @@ -43,9 +45,10 @@ class ScannerState extends ConsumerState with WidgetsBindingObserver { context: context, builder: (context) { return CustomDialogBox( - title: "Pas d'adhésion", - descriptions: - "Ce produit n'est pas disponnible pour les non-adhérents. Confirmer l'encaissement ?", + title: AppLocalizations.of(context)!.paiementScanNoMembership, + descriptions: AppLocalizations.of( + context, + )!.paiementScanNoMembershipConfirmation, onYes: () async { tokenExpireWrapper(ref, () async { onYes.call(); @@ -77,6 +80,8 @@ class ScannerState extends ConsumerState with WidgetsBindingObserver { final ongoingTransactionNotifier = ref.read( ongoingTransactionProvider.notifier, ); + + final localizeWithContext = AppLocalizations.of(context)!; if (mounted && barcodes.barcodes.isNotEmpty && barcode == null) { final data = barcodeNotifier.updateBarcode( barcodes.barcodes.firstOrNull!.rawValue!, @@ -87,7 +92,10 @@ class ScannerState extends ConsumerState with WidgetsBindingObserver { showWithoutMembershipDialog(() async { final value = await scanNotifier.scan(store.id, data, bypass: true); if (value == null) { - displayToastWithContext(TypeMsg.error, "QR Code déjà utilisé"); + displayToastWithContext( + TypeMsg.error, + localizeWithContext.paiementScanAlreadyUsedQRCode, + ); barcodeNotifier.clearBarcode(); ongoingTransactionNotifier.clearOngoingTransaction(); return; @@ -99,7 +107,10 @@ class ScannerState extends ConsumerState with WidgetsBindingObserver { } final value = await scanNotifier.scan(store.id, data); if (value == null) { - displayToastWithContext(TypeMsg.error, "QR Code déjà utilisé"); + displayToastWithContext( + TypeMsg.error, + localizeWithContext.paiementScanAlreadyUsedQRCode, + ); barcodeNotifier.clearBarcode(); ongoingTransactionNotifier.clearOngoingTransaction(); return; @@ -112,6 +123,23 @@ class ScannerState extends ConsumerState with WidgetsBindingObserver { } } + void showCameraPermissionDeniedDialog() async { + await showDialog( + context: context, + builder: (context) => CustomDialogBox( + title: AppLocalizations.of(context)!.paiementCameraPermissionRequired, + descriptions: AppLocalizations.of( + context, + )!.paiementCameraPerssionRequiredDescription, + onYes: () async { + Navigator.of(context).pop(); + await openAppSettings(); + }, + yesText: AppLocalizations.of(context)!.paiementSettings, + ), + ); + } + @override void initState() { super.initState(); @@ -122,19 +150,7 @@ class ScannerState extends ConsumerState with WidgetsBindingObserver { unawaited(() async { await controller.start(); if (!controller.value.hasCameraPermission) { - showDialog( - context: context, - builder: (context) => CustomDialogBox( - title: 'Permission caméra requise', - descriptions: - 'Pour scanner des QR codes, l\'application a besoin d\'accéder à votre caméra. Veuillez accorder cette permission dans les paramètres de votre appareil.', - onYes: () async { - Navigator.of(context).pop(); - await openAppSettings(); - }, - yesText: 'Paramètres', - ), - ); + showCameraPermissionDeniedDialog(); } }()); } diff --git a/lib/paiement/ui/pages/stats_page/month_bar.dart b/lib/paiement/ui/pages/stats_page/month_bar.dart index 9b1930a6b4..6c1d0e40c5 100644 --- a/lib/paiement/ui/pages/stats_page/month_bar.dart +++ b/lib/paiement/ui/pages/stats_page/month_bar.dart @@ -3,6 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:titan/paiement/class/history.dart'; import 'package:titan/paiement/providers/my_history_provider.dart'; +import 'package:titan/tools/providers/locale_notifier.dart'; class MonthBar extends HookConsumerWidget { final DateTime currentMonth; @@ -10,7 +11,11 @@ class MonthBar extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final formatter = NumberFormat("#,##0.00", "fr_FR"); + final locale = ref.watch(localeProvider); + final formatter = NumberFormat.currency( + locale: locale.toString(), + symbol: "€", + ); final history = ref.watch(myHistoryProvider); int total = 0; history.maybeWhen( @@ -36,7 +41,7 @@ class MonthBar extends HookConsumerWidget { }, ); return Text( - "${DateFormat("MMMM yyyy", "fr_FR").format(currentMonth)} : ${total > 0 ? "+" : ""}${formatter.format(total / 100)} €", + "${DateFormat.yMMMM(locale.toString()).format(currentMonth)} : ${total > 0 ? "+" : ""}${formatter.format(total / 100)}", style: const TextStyle( fontSize: 20, color: Colors.black, diff --git a/lib/paiement/ui/pages/stats_page/stats_page.dart b/lib/paiement/ui/pages/stats_page/stats_page.dart index f041e21efe..6bf7cb9db7 100644 --- a/lib/paiement/ui/pages/stats_page/stats_page.dart +++ b/lib/paiement/ui/pages/stats_page/stats_page.dart @@ -19,6 +19,7 @@ class StatsPage extends HookConsumerWidget { return PaymentTemplate( child: LayoutBuilder( builder: (context, constraints) => Refresher( + controller: ScrollController(), onRefresh: () async { await myHistoryNotifier.getHistory(); }, diff --git a/lib/paiement/ui/pages/stats_page/sum_up_chart.dart b/lib/paiement/ui/pages/stats_page/sum_up_chart.dart index ac2bd215e8..83f83f80e5 100644 --- a/lib/paiement/ui/pages/stats_page/sum_up_chart.dart +++ b/lib/paiement/ui/pages/stats_page/sum_up_chart.dart @@ -2,11 +2,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:intl/intl.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/paiement/class/history.dart'; import 'package:titan/paiement/providers/my_history_provider.dart'; import 'package:titan/paiement/providers/selected_transactions_provider.dart'; import 'package:titan/paiement/ui/pages/stats_page/month_section_summary.dart'; import 'package:titan/paiement/ui/pages/stats_page/transaction_chart.dart'; +import 'package:titan/tools/providers/locale_notifier.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; class SumUpChart extends HookConsumerWidget { @@ -15,13 +17,17 @@ class SumUpChart extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final locale = ref.watch(localeProvider); final selected = useState(-1); final history = ref.watch(myHistoryProvider); final pageController = usePageController(); final selectedTransactionsNotifier = ref.read( selectedTransactionsProvider(currentMonth).notifier, ); - final formatter = NumberFormat("#,##0.00", "fr_FR"); + final formatter = NumberFormat.currency( + locale: locale.toString(), + symbol: "€", + ); final Map> transactionPerStore = {}; final Map> creditedTransactionPerStore = {}; @@ -40,7 +46,7 @@ class SumUpChart extends HookConsumerWidget { transaction.type == HistoryType.refundCredited) { final transactionName = transaction.type != HistoryType.transfer ? transaction.otherWalletName - : "Recharge"; + : AppLocalizations.of(context)!.paiementTopUp; creditedTransactionPerStore[transactionName] = [ ...?creditedTransactionPerStore[transactionName], transaction, @@ -115,9 +121,10 @@ class SumUpChart extends HookConsumerWidget { ); }, child: MonthSectionSummary( - title: "Reçu", - amount: - '${formatter.format(transferTotal / 100)} €', + title: AppLocalizations.of( + context, + )!.paiementReceived, + amount: formatter.format(transferTotal / 100), color: const Color.fromARGB(255, 255, 119, 7), darkColor: const Color.fromARGB( 255, @@ -147,8 +154,10 @@ class SumUpChart extends HookConsumerWidget { ); }, child: MonthSectionSummary( - title: "Déboursé", - amount: '${formatter.format(total / 100)} €', + title: AppLocalizations.of( + context, + )!.paiementSpent, + amount: formatter.format(total / 100), color: const Color.fromARGB(255, 1, 127, 128), darkColor: const Color.fromARGB( 255, @@ -174,8 +183,10 @@ class SumUpChart extends HookConsumerWidget { : Container( height: 300, alignment: Alignment.center, - child: const Text( - "Aucune transaction pour ce mois", + child: Text( + AppLocalizations.of( + context, + )!.paiementNoTransactionForThisMonth, style: TextStyle(fontSize: 18, color: Colors.grey), ), ); diff --git a/lib/paiement/ui/pages/stats_page/transaction_chart.dart b/lib/paiement/ui/pages/stats_page/transaction_chart.dart index b0138820e9..1dcd4ee712 100644 --- a/lib/paiement/ui/pages/stats_page/transaction_chart.dart +++ b/lib/paiement/ui/pages/stats_page/transaction_chart.dart @@ -3,10 +3,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:intl/intl.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/paiement/class/history.dart'; import 'package:titan/paiement/providers/selected_transactions_provider.dart'; import 'package:titan/paiement/tools/functions.dart'; import 'package:titan/paiement/ui/pages/stats_page/sum_up_card.dart'; +import 'package:titan/tools/providers/locale_notifier.dart'; class TransactionChart extends HookConsumerWidget { final Map> transactionPerStore; @@ -19,6 +21,7 @@ class TransactionChart extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final locale = ref.watch(localeProvider); final selected = useState(-1); final List chartPart = []; @@ -27,7 +30,10 @@ class TransactionChart extends HookConsumerWidget { ); final Map> mappedHistory = {}; final List keys = []; - final formatter = NumberFormat("#,##0.00", "fr_FR"); + final formatter = NumberFormat.currency( + locale: locale.toString(), + symbol: "€", + ); for (final (index, wallet) in transactionPerStore.keys.indexed) { final l = transactionPerStore[wallet]!; @@ -51,7 +57,7 @@ class TransactionChart extends HookConsumerWidget { radius: 40 + (keys.indexOf(wallet) == selected.value ? 10 : 0), badgePositionPercentageOffset: 0.6, badgeWidget: SumUpCard( - amount: '${formatter.format(totalAmount / 100)} €', + amount: formatter.format(totalAmount / 100), color: walletColor[0], darkColor: walletColor[1], shadowColor: walletColor[2], @@ -62,9 +68,9 @@ class TransactionChart extends HookConsumerWidget { } return chartPart.isEmpty - ? const Center( + ? Center( child: Text( - "Aucune transaction", + AppLocalizations.of(context)!.paiementNoTransaction, style: TextStyle(fontSize: 20, color: Colors.black54), ), ) diff --git a/lib/paiement/ui/pages/store_admin_page/search_result.dart b/lib/paiement/ui/pages/store_admin_page/search_result.dart index 0e36f31de5..8ffc540fb2 100644 --- a/lib/paiement/ui/pages/store_admin_page/search_result.dart +++ b/lib/paiement/ui/pages/store_admin_page/search_result.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/paiement/class/seller.dart'; import 'package:titan/paiement/providers/new_admin_provider.dart'; import 'package:titan/paiement/providers/selected_store_provider.dart'; @@ -52,17 +53,32 @@ class SearchResult extends HookConsumerWidget { builder: (context, ref, _) { final sellerRightsList = ref.watch(sellerRightsListProvider); return SellerRightDialog( - title: "Droit du vendeur", + title: AppLocalizations.of(context)!.paiementSellerRigths, child: Column( mainAxisSize: MainAxisSize.min, children: [ - RightCheckBox(title: "Peut encaisser", index: 0), - RightCheckBox(title: "Peut voir l'historique", index: 1), RightCheckBox( - title: "Peut annuler des transactions", + title: AppLocalizations.of(context)!.paiementCanBank, + index: 0, + ), + RightCheckBox( + title: AppLocalizations.of( + context, + )!.paiementCanSeeHistory, + index: 1, + ), + RightCheckBox( + title: AppLocalizations.of( + context, + )!.paiementCanCancelTransaction, index: 2, ), - RightCheckBox(title: "Peut gérer les vendeurs", index: 3), + RightCheckBox( + title: AppLocalizations.of( + context, + )!.paiementCanManageSellers, + index: 3, + ), ], ), onYes: () async { @@ -78,6 +94,12 @@ class SearchResult extends HookConsumerWidget { canCancel: sellerRightsList[2], canManageSellers: sellerRightsList[3], ); + final addedSellerMsg = AppLocalizations.of( + context, + )!.paiementAddedSeller; + final addingSellerErrorMsg = AppLocalizations.of( + context, + )!.paiementAddingSellerError; final value = await sellerStoreNotifier.createStoreSeller( seller, ); @@ -86,14 +108,14 @@ class SearchResult extends HookConsumerWidget { usersNotifier.clear(); sellerRightsListNotifier.clearRights(); newAdminNotifier.resetNewAdmin(); - displayToastWithContext(TypeMsg.msg, "Vendeur ajouté"); + displayToastWithContext(TypeMsg.msg, addedSellerMsg); if (context.mounted) { Navigator.of(context).pop(); } } else { displayToastWithContext( TypeMsg.error, - "Erreur lors de l'ajout", + addingSellerErrorMsg, ); } }); diff --git a/lib/paiement/ui/pages/store_admin_page/seller_right_card.dart b/lib/paiement/ui/pages/store_admin_page/seller_right_card.dart index e77f16f31a..65272ec9dc 100644 --- a/lib/paiement/ui/pages/store_admin_page/seller_right_card.dart +++ b/lib/paiement/ui/pages/store_admin_page/seller_right_card.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:heroicons/heroicons.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/paiement/class/seller.dart'; import 'package:titan/paiement/providers/selected_store_provider.dart'; import 'package:titan/paiement/providers/store_sellers_list_provider.dart'; @@ -67,11 +68,11 @@ class SellerRightCard extends ConsumerWidget { ); final labels = [ - "Encaisser", - "Voir l'historique", - "Annuler les transactions", - "Gérer les vendeurs", - "Administrateur de la structure", + AppLocalizations.of(context)!.paiementBank, + AppLocalizations.of(context)!.paiementSeeHistory, + AppLocalizations.of(context)!.paiementCancelTransactions, + AppLocalizations.of(context)!.paiementManageSellers, + AppLocalizations.of(context)!.paiementStructureAdmin, ]; List sellerRights = [ @@ -119,7 +120,7 @@ class SellerRightCard extends ConsumerWidget { const SizedBox(height: 20), Expanded( child: Text( - "Droits de ${storeSeller.user.nickname ?? ("${storeSeller.user.firstname} ${storeSeller.user.name}")}", + "${AppLocalizations.of(context)!.paiementRightsOf} ${storeSeller.user.nickname ?? ("${storeSeller.user.firstname} ${storeSeller.user.name}")}", overflow: TextOverflow.ellipsis, style: const TextStyle( color: Color.fromARGB(255, 0, 29, 29), @@ -155,6 +156,14 @@ class SellerRightCard extends ConsumerWidget { ), onChanged: (value) async { await tokenExpireWrapper(ref, () async { + final rightsUpdatedMsg = + AppLocalizations.of( + context, + )!.paiementRightsUpdated; + final rightsUpdateErrorMsg = + AppLocalizations.of( + context, + )!.paiementRightsUpdateError; final value = await sellerStoreNotifier .updateStoreSeller( storeSeller.copyWith( @@ -175,7 +184,7 @@ class SellerRightCard extends ConsumerWidget { if (value) { displayToastWithContext( TypeMsg.msg, - "Droits mis à jour", + rightsUpdatedMsg, ); sellerRights[i] = !sellerRights[i]; if (context.mounted) { @@ -184,7 +193,7 @@ class SellerRightCard extends ConsumerWidget { } else { displayToastWithContext( TypeMsg.error, - "Impossible de mettre à jour les droits", + rightsUpdateErrorMsg, ); } }); @@ -199,17 +208,27 @@ class SellerRightCard extends ConsumerWidget { await showDialog( context: context, builder: (context) => CustomDialogBox( - title: "Supprimer l'association", - descriptions: - "Voulez-vous vraiment supprimer ce vendeur ?", + title: AppLocalizations.of( + context, + )!.paiementDeleteStore, + descriptions: AppLocalizations.of( + context, + )!.paiementDeleteSellerDescription, onYes: () { tokenExpireWrapper(ref, () async { + final deleteSellerMsg = AppLocalizations.of( + context, + )!.paiementDeletedSeller; + final deletingSellerErrorMsg = + AppLocalizations.of( + context, + )!.paiementDeletingSellerError; final value = await sellerStoreNotifier .deleteStoreSeller(storeSeller); if (value) { displayToastWithContext( TypeMsg.msg, - "Vendeur supprimé", + deleteSellerMsg, ); if (context.mounted) { Navigator.pop(context); @@ -217,7 +236,7 @@ class SellerRightCard extends ConsumerWidget { } else { displayToastWithContext( TypeMsg.error, - "Impossible de supprimer le vendeur", + deletingSellerErrorMsg, ); } }); @@ -225,22 +244,27 @@ class SellerRightCard extends ConsumerWidget { ), ); }, - child: const Padding( - padding: EdgeInsets.symmetric(vertical: 10), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), child: AddEditButtonLayout( - colors: [Color(0xFF9E131F), Color(0xFF590512)], + colors: const [ + Color(0xFF9E131F), + Color(0xFF590512), + ], child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - HeroIcon( + const HeroIcon( HeroIcons.trash, color: Colors.white, size: 20, ), - SizedBox(width: 15), + const SizedBox(width: 15), Text( - "Supprimer le vendeur", - style: TextStyle( + AppLocalizations.of( + context, + )!.paiementDeleteSeller, + style: const TextStyle( color: Colors.white, fontSize: 14, ), diff --git a/lib/paiement/ui/pages/store_admin_page/seller_right_dialog.dart b/lib/paiement/ui/pages/store_admin_page/seller_right_dialog.dart index ea73482ff6..6d3cafa4ec 100644 --- a/lib/paiement/ui/pages/store_admin_page/seller_right_dialog.dart +++ b/lib/paiement/ui/pages/store_admin_page/seller_right_dialog.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; class Consts { @@ -102,10 +103,12 @@ class SellerRightDialog extends StatelessWidget { ), child: child, ), - child: const Center( + child: Center( child: Text( - "Ajouter", - style: TextStyle(fontWeight: FontWeight.bold), + AppLocalizations.of(context)!.paiementAdd, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), ), ), ), diff --git a/lib/paiement/ui/pages/store_admin_page/store_admin_page.dart b/lib/paiement/ui/pages/store_admin_page/store_admin_page.dart index 4321a85809..67e3ea4b97 100644 --- a/lib/paiement/ui/pages/store_admin_page/store_admin_page.dart +++ b/lib/paiement/ui/pages/store_admin_page/store_admin_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/paiement/providers/selected_store_provider.dart'; import 'package:titan/paiement/providers/store_sellers_list_provider.dart'; import 'package:titan/paiement/ui/pages/store_admin_page/search_result.dart'; @@ -33,6 +34,7 @@ class StoreAdminPage extends HookConsumerWidget { return PaymentTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await storeSellersNotifier.getStoreSellerList(store.id); }, @@ -42,7 +44,7 @@ class StoreAdminPage extends HookConsumerWidget { padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20), alignment: Alignment.centerLeft, child: Text( - "Les vendeurs de ${store.name}", + "${AppLocalizations.of(context)!.paiementSellersOf} ${store.name}", style: const TextStyle( color: Color.fromARGB(255, 0, 29, 29), fontSize: 20, @@ -55,22 +57,22 @@ class StoreAdminPage extends HookConsumerWidget { onTap: () { isSearching.value = true; }, - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 30), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 30), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Center( child: Text( - "Ajouter un vendeur", - style: TextStyle( + AppLocalizations.of(context)!.paiementAddSeller, + style: const TextStyle( color: Color.fromARGB(255, 0, 29, 29), fontSize: 14, ), ), ), - Spacer(), - CardButton( + const Spacer(), + const CardButton( size: 35, child: HeroIcon( HeroIcons.plus, @@ -90,8 +92,10 @@ class StoreAdminPage extends HookConsumerWidget { .where((seller) => seller.user.id == me.id) .firstOrNull; if (mySellers == null) { - return const Center( - child: Text('You are not a seller in this store'), + return Center( + child: Text( + AppLocalizations.of(context)!.paiementSellerError, + ), ); } return Column( @@ -114,7 +118,7 @@ class StoreAdminPage extends HookConsumerWidget { children: [ Expanded( child: TextEntry( - label: "Ajouter un vendeur", + label: AppLocalizations.of(context)!.paiementAddSeller, onChanged: (value) { tokenExpireWrapper(ref, () async { if (queryController.text.isNotEmpty) { diff --git a/lib/paiement/ui/pages/store_pages/add_edit_store.dart b/lib/paiement/ui/pages/store_pages/add_edit_store.dart index 1d57bb0a24..5efe7a9783 100644 --- a/lib/paiement/ui/pages/store_pages/add_edit_store.dart +++ b/lib/paiement/ui/pages/store_pages/add_edit_store.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/paiement/class/store.dart' as store_class; import 'package:titan/paiement/class/structure.dart'; import 'package:titan/paiement/providers/my_stores_provider.dart'; @@ -40,7 +41,11 @@ class AddEditStorePage extends HookConsumerWidget { children: [ const SizedBox(height: 10), AlignLeftText( - "${isEdit ? 'Modifier' : 'Ajouter'} une association ${structure.name}", + isEdit + ? AppLocalizations.of( + context, + )!.paiementEditStore(store.name) + : AppLocalizations.of(context)!.paiementAddStore, padding: const EdgeInsets.symmetric(horizontal: 30), color: Colors.grey, ), @@ -54,7 +59,9 @@ class AddEditStorePage extends HookConsumerWidget { children: [ TextEntry( controller: name, - label: "Nom de l'association", + label: AppLocalizations.of( + context, + )!.paiementStoreName, ), const SizedBox(height: 50), WaitingButton( @@ -69,6 +76,21 @@ class AddEditStorePage extends HookConsumerWidget { if (key.currentState == null) { return; } + final successfullyAddedStoreMsg = isEdit + ? AppLocalizations.of( + context, + )!.paiementSuccessfullyModifiedStore + : AppLocalizations.of( + context, + )!.paiementSuccessfullyAddedStore; + final addingErrorMsg = isEdit + ? AppLocalizations.of( + context, + )!.paiementModifyingStoreError + : AppLocalizations.of( + context, + )!.paiementAddingStoreError; + if (key.currentState!.validate()) { store_class.Store newStore = store.copyWith( name: name.text, @@ -89,22 +111,20 @@ class AddEditStorePage extends HookConsumerWidget { QR.back(); displayToastWithContext( TypeMsg.msg, - isEdit - ? "L'association a été modifiée avec succès" - : "L'association a été ajoutée avec succès", + successfullyAddedStoreMsg, ); } else { displayToastWithContext( TypeMsg.error, - isEdit - ? "Une erreur est survenue lors de la modification de l'association" - : "Une erreur est survenue lors de l'ajout de l'association", + addingErrorMsg, ); } } }, child: Text( - isEdit ? "Modifier" : "Ajouter", + isEdit + ? AppLocalizations.of(context)!.paiementModify + : AppLocalizations.of(context)!.paiementAdd, style: const TextStyle( color: Colors.white, fontSize: 25, diff --git a/lib/paiement/ui/pages/store_stats_page/interval_selector.dart b/lib/paiement/ui/pages/store_stats_page/interval_selector.dart index 0d54a46a7d..18ba1a626f 100644 --- a/lib/paiement/ui/pages/store_stats_page/interval_selector.dart +++ b/lib/paiement/ui/pages/store_stats_page/interval_selector.dart @@ -5,6 +5,7 @@ import 'package:intl/intl.dart'; import 'package:titan/paiement/providers/selected_interval_provider.dart'; import 'package:titan/paiement/providers/selected_store_history.dart'; import 'package:titan/paiement/providers/selected_store_provider.dart'; +import 'package:titan/tools/providers/locale_notifier.dart'; class IntervalSelector extends ConsumerWidget { const IntervalSelector({super.key}); @@ -12,6 +13,7 @@ class IntervalSelector extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final now = DateTime.now(); + final locale = ref.watch(localeProvider); final selectedStore = ref.watch(selectedStoreProvider); final selectedHistoryNotifier = ref.read(sellerHistoryProvider.notifier); final selectedInterval = ref.watch(selectedIntervalProvider); @@ -81,9 +83,8 @@ class IntervalSelector extends ConsumerWidget { borderRadius: BorderRadius.circular(10), ), child: Text( - DateFormat( - "dd MMM yyyy", - "fr_FR", + DateFormat.yMd( + locale.toString(), ).format(selectedInterval.start), style: TextStyle( color: const Color(0xff204550), @@ -116,7 +117,9 @@ class IntervalSelector extends ConsumerWidget { borderRadius: BorderRadius.circular(10), ), child: Text( - DateFormat("HH:mm", "fr_FR").format(selectedInterval.start), + DateFormat.Hm( + locale.toString(), + ).format(selectedInterval.start), style: TextStyle( color: const Color(0xff204550), fontWeight: FontWeight.bold, @@ -165,9 +168,8 @@ class IntervalSelector extends ConsumerWidget { borderRadius: BorderRadius.circular(10), ), child: Text( - DateFormat( - "dd MMM yyyy", - "fr_FR", + DateFormat.yMd( + locale.toString(), ).format(selectedInterval.end), style: TextStyle( color: const Color(0xff204550), @@ -200,7 +202,9 @@ class IntervalSelector extends ConsumerWidget { borderRadius: BorderRadius.circular(10), ), child: Text( - DateFormat("HH:mm", "fr_FR").format(selectedInterval.end), + DateFormat.Hm( + locale.toString(), + ).format(selectedInterval.end), style: TextStyle( color: const Color(0xff204550), fontWeight: FontWeight.bold, diff --git a/lib/paiement/ui/pages/store_stats_page/refund_page.dart b/lib/paiement/ui/pages/store_stats_page/refund_page.dart index c253b5be01..4a9c5346a0 100644 --- a/lib/paiement/ui/pages/store_stats_page/refund_page.dart +++ b/lib/paiement/ui/pages/store_stats_page/refund_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:intl/intl.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/paiement/class/history.dart'; import 'package:titan/paiement/class/refund.dart'; import 'package:titan/paiement/providers/refund_amount_provider.dart'; @@ -9,6 +10,7 @@ import 'package:titan/paiement/providers/transaction_provider.dart'; import 'package:titan/paiement/ui/components/digit_fade_in_animation.dart'; import 'package:titan/paiement/ui/components/keyboard.dart'; import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/providers/locale_notifier.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; import 'package:titan/tools/ui/layouts/add_edit_button_layout.dart'; @@ -18,10 +20,14 @@ class ReFundPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final locale = ref.watch(localeProvider); final refundAmount = ref.watch(refundAmountProvider); final refundAmountNotifier = ref.watch(refundAmountProvider.notifier); final transactionNotifier = ref.watch(transactionProvider.notifier); - final formatter = NumberFormat("#,##0.00", "fr_FR"); + final formatter = NumberFormat.currency( + locale: locale.toString(), + symbol: "€", + ); final isValid = double.tryParse(refundAmount.replaceAll(",", ".")) != null && @@ -48,7 +54,7 @@ class ReFundPage extends ConsumerWidget { children: [ const SizedBox(height: 20), Text( - 'Remboursement', + AppLocalizations.of(context)!.paiementRefund, style: const TextStyle( color: Colors.white, fontSize: 20, @@ -57,7 +63,7 @@ class ReFundPage extends ConsumerWidget { ), const SizedBox(height: 5), Text( - '${history.otherWalletName} (max: ${formatter.format(history.total / 100)} €)', + '${history.otherWalletName} (max: ${formatter.format(history.total / 100)})', style: const TextStyle(color: Colors.white, fontSize: 15), ), Expanded( @@ -144,7 +150,9 @@ class ReFundPage extends ConsumerWidget { data: (value) { displayToastWithContext( TypeMsg.msg, - "Transaction effectuée", + AppLocalizations.of( + context, + )!.paiementDoneTransaction, ); ref.invalidate(sellerHistoryProvider); Navigator.of(context).pop(); @@ -164,7 +172,7 @@ class ReFundPage extends ConsumerWidget { child: child, ), child: Text( - "Rembourser", + AppLocalizations.of(context)!.paiementRefundAction, style: TextStyle( color: Colors.black, fontSize: 20, diff --git a/lib/paiement/ui/pages/store_stats_page/store_stats_page.dart b/lib/paiement/ui/pages/store_stats_page/store_stats_page.dart index 0edab7025e..c3323ed507 100644 --- a/lib/paiement/ui/pages/store_stats_page/store_stats_page.dart +++ b/lib/paiement/ui/pages/store_stats_page/store_stats_page.dart @@ -21,6 +21,7 @@ class StoreStatsPage extends ConsumerWidget { final selectedInterval = ref.watch(selectedIntervalProvider); return PaymentTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await selectedHistoryNotifier.getHistory( selectedStore.id, diff --git a/lib/paiement/ui/pages/store_stats_page/summary_card.dart b/lib/paiement/ui/pages/store_stats_page/summary_card.dart index a0622285f3..507cb5b92f 100644 --- a/lib/paiement/ui/pages/store_stats_page/summary_card.dart +++ b/lib/paiement/ui/pages/store_stats_page/summary_card.dart @@ -1,15 +1,19 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:heroicons/heroicons.dart'; import 'package:intl/intl.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/paiement/class/history.dart'; +import 'package:titan/tools/providers/locale_notifier.dart'; -class SummaryCard extends StatelessWidget { +class SummaryCard extends ConsumerWidget { final List history; const SummaryCard({super.key, required this.history}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final locale = ref.watch(localeProvider); int total = 0; int numberTransactions = 0; @@ -46,7 +50,10 @@ class SummaryCard extends StatelessWidget { final mean = total / numberTransactions; - final formatter = NumberFormat("#,##0.00", "fr_FR"); + final formatter = NumberFormat.currency( + locale: locale.toString(), + symbol: "€", + ); return Container( height: 75, padding: const EdgeInsets.symmetric(horizontal: 20), @@ -70,8 +77,8 @@ class SummaryCard extends StatelessWidget { children: [ Row( children: [ - const AutoSizeText( - "Total sur la période", + AutoSizeText( + AppLocalizations.of(context)!.paiementTotalDuringPeriod, maxLines: 2, style: TextStyle(color: Color(0xff204550), fontSize: 14), ), @@ -79,7 +86,7 @@ class SummaryCard extends StatelessWidget { ), const SizedBox(height: 5), Text( - "Moyenne : ${formatter.format(mean / 100)} € / transaction", + "${AppLocalizations.of(context)!.paiementMean} ${formatter.format(mean / 100)} / ${AppLocalizations.of(context)!.paiementTransaction}", style: const TextStyle( color: Color(0xff204550), fontSize: 12, @@ -91,7 +98,7 @@ class SummaryCard extends StatelessWidget { ), const SizedBox(width: 10), Text( - "${formatter.format(total / 100)} €", + formatter.format(total / 100), style: TextStyle( color: const Color(0xff204550), fontSize: 18, diff --git a/lib/paiement/ui/pages/admin_page/add_store_card.dart b/lib/paiement/ui/pages/structure_admin_page/add_store_card.dart similarity index 92% rename from lib/paiement/ui/pages/admin_page/add_store_card.dart rename to lib/paiement/ui/pages/structure_admin_page/add_store_card.dart index 80fda502a2..c43c414c4b 100644 --- a/lib/paiement/ui/pages/admin_page/add_store_card.dart +++ b/lib/paiement/ui/pages/structure_admin_page/add_store_card.dart @@ -16,7 +16,9 @@ class AddStoreCard extends ConsumerWidget { onTap: () { storeNotifier.updateStore(Store.empty()); QR.to( - PaymentRouter.root + PaymentRouter.admin + PaymentRouter.addEditStore, + PaymentRouter.root + + PaymentRouter.structureStores + + PaymentRouter.addEditStore, ); }, child: Container( diff --git a/lib/paiement/ui/pages/admin_page/admin_store_card.dart b/lib/paiement/ui/pages/structure_admin_page/admin_store_card.dart similarity index 83% rename from lib/paiement/ui/pages/admin_page/admin_store_card.dart rename to lib/paiement/ui/pages/structure_admin_page/admin_store_card.dart index b857f06412..1f81751a1b 100644 --- a/lib/paiement/ui/pages/admin_page/admin_store_card.dart +++ b/lib/paiement/ui/pages/structure_admin_page/admin_store_card.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/paiement/class/store.dart'; import 'package:titan/paiement/providers/store_provider.dart'; import 'package:titan/paiement/providers/stores_list_provider.dart'; @@ -59,7 +60,7 @@ class AdminStoreCard extends ConsumerWidget { storeNotifier.updateStore(store); QR.to( PaymentRouter.root + - PaymentRouter.admin + + PaymentRouter.structureStores + PaymentRouter.addEditStore, ); }, @@ -77,23 +78,30 @@ class AdminStoreCard extends ConsumerWidget { await showDialog( context: context, builder: (context) => CustomDialogBox( - title: "Supprimer l'association", - descriptions: - "Voulez-vous vraiment supprimer cette association ?", + title: AppLocalizations.of(context)!.paiementDeleteStore, + descriptions: AppLocalizations.of( + context, + )!.paiementDeleteStoreDescription, onYes: () { tokenExpireWrapper(ref, () async { + final storeDeletedMsg = AppLocalizations.of( + context, + )!.paiementStoreDeleted; + final storeDeleteErrorMsg = AppLocalizations.of( + context, + )!.paiementDeleteStoreError; final value = await storeListNotifier.deleteStore( store, ); if (value) { displayToastWithContext( TypeMsg.msg, - "Association supprimée", + storeDeletedMsg, ); } else { displayToastWithContext( TypeMsg.error, - "Impossible de supprimer l'association", + storeDeleteErrorMsg, ); } }); diff --git a/lib/paiement/ui/pages/admin_page/admin_page.dart b/lib/paiement/ui/pages/structure_admin_page/structure_admin_page.dart similarity index 78% rename from lib/paiement/ui/pages/admin_page/admin_page.dart rename to lib/paiement/ui/pages/structure_admin_page/structure_admin_page.dart index dcf9e1a91c..420614ac27 100644 --- a/lib/paiement/ui/pages/admin_page/admin_page.dart +++ b/lib/paiement/ui/pages/structure_admin_page/structure_admin_page.dart @@ -1,16 +1,17 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/paiement/providers/selected_structure_provider.dart'; import 'package:titan/paiement/providers/stores_list_provider.dart'; -import 'package:titan/paiement/ui/pages/admin_page/add_store_card.dart'; -import 'package:titan/paiement/ui/pages/admin_page/admin_store_card.dart'; +import 'package:titan/paiement/ui/pages/structure_admin_page/add_store_card.dart'; +import 'package:titan/paiement/ui/pages/structure_admin_page/admin_store_card.dart'; import 'package:titan/paiement/ui/paiement.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/layouts/refresher.dart'; import 'package:titan/tools/ui/widgets/align_left_text.dart'; -class AdminPage extends ConsumerWidget { - const AdminPage({super.key}); +class StructureStoresPage extends ConsumerWidget { + const StructureStoresPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -19,6 +20,7 @@ class AdminPage extends ConsumerWidget { final structure = ref.watch(selectedStructureProvider); return PaymentTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await storeListNotifier.getStores(); }, @@ -26,7 +28,9 @@ class AdminPage extends ConsumerWidget { children: [ const SizedBox(height: 10), AlignLeftText( - "Gestion des associations ${structure.name}", + AppLocalizations.of( + context, + )!.paiementStructureManagement(structure.name), color: Colors.grey, fontSize: 20, fontWeight: FontWeight.bold, diff --git a/lib/paiement/ui/pages/transfer_structure_page/search_result.dart b/lib/paiement/ui/pages/transfer_structure_page/search_result.dart index 92885bbf95..558201fcb1 100644 --- a/lib/paiement/ui/pages/transfer_structure_page/search_result.dart +++ b/lib/paiement/ui/pages/transfer_structure_page/search_result.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/paiement/providers/selected_store_provider.dart'; import 'package:titan/paiement/providers/transfer_structure_provider.dart'; import 'package:titan/tools/constants.dart'; @@ -32,10 +33,16 @@ class SearchResult extends HookConsumerWidget { context: context, builder: (context) { return CustomDialogBox( - title: 'Transfert de structure', + title: AppLocalizations.of(context)!.paiementTransferStructure, descriptions: - 'Vous êtes sur le point de transférer la structure à ${simpleUser.getName()}. Le nouveau responsable aura accès à toutes les fonctionnalités de gestion de la structure. Vous allez recevoir un email pour valider ce transfert. Le lien ne sera actif que pendant 20 minutes. Cette action est irréversible. Êtes-vous sûr de vouloir continuer ?', + '${AppLocalizations.of(context)!.paiementYouAreTransferingStructureTo} ${simpleUser.getName()}. ${AppLocalizations.of(context)!.paiementTransferStructureDescription}', onYes: () async { + final transferStructureSeccessMsg = AppLocalizations.of( + context, + )!.paiementTransferStructureSuccess; + final transferStructureErrorMsg = AppLocalizations.of( + context, + )!.paiementTransferStructureError; final value = await transferStructureNotifier.initTransfer( selectedStore.structure, simpleUser.id, @@ -43,12 +50,12 @@ class SearchResult extends HookConsumerWidget { if (value) { displayToastWithContext( TypeMsg.msg, - "Transfert de structure demandé avec succès.", + transferStructureSeccessMsg, ); } else { displayToastWithContext( TypeMsg.error, - "Une erreur est survenue lors du transfert de structure.", + transferStructureErrorMsg, ); } }, diff --git a/lib/paiement/ui/pages/transfer_structure_page/transfer_structure_page.dart b/lib/paiement/ui/pages/transfer_structure_page/transfer_structure_page.dart index 3ee0e22ab4..def4355d33 100644 --- a/lib/paiement/ui/pages/transfer_structure_page/transfer_structure_page.dart +++ b/lib/paiement/ui/pages/transfer_structure_page/transfer_structure_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/paiement/ui/pages/transfer_structure_page/search_result.dart'; import 'package:titan/paiement/ui/paiement.dart'; import 'package:titan/tools/ui/widgets/styled_search_bar.dart'; @@ -20,7 +21,7 @@ class TransferStructurePage extends HookConsumerWidget { child: Column( children: [ StyledSearchBar( - label: "Prochain responsable", + label: AppLocalizations.of(context)!.paiementNextAccountable, color: Color.fromARGB(255, 6, 75, 75), padding: const EdgeInsets.all(0), editingController: queryController, diff --git a/lib/paiement/ui/paiement.dart b/lib/paiement/ui/paiement.dart index d5f5ad63a4..c1f2773542 100644 --- a/lib/paiement/ui/paiement.dart +++ b/lib/paiement/ui/paiement.dart @@ -1,23 +1,27 @@ import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/paiement/router.dart'; import 'package:titan/tools/ui/widgets/top_bar.dart'; -import 'package:titan/paiement/tools/constants.dart'; +import 'package:titan/tools/constants.dart'; -class PaymentTemplate extends StatelessWidget { +class PaymentTemplate extends HookConsumerWidget { final Widget child; const PaymentTemplate({super.key, required this.child}); @override - Widget build(BuildContext context) { - return SafeArea( - child: Column( - children: [ - const TopBar( - title: PaiementTextConstants.paiement, - root: PaymentRouter.root, + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + body: Container( + decoration: const BoxDecoration(color: ColorConstants.background), + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + TopBar(root: PaymentRouter.root), + Expanded(child: child), + ], ), - Expanded(child: child), - ], + ), ), ); } diff --git a/lib/ph/providers/is_ph_admin_provider.dart b/lib/ph/providers/is_ph_admin_provider.dart index dd9d26b1ea..0497d8ace7 100644 --- a/lib/ph/providers/is_ph_admin_provider.dart +++ b/lib/ph/providers/is_ph_admin_provider.dart @@ -3,10 +3,7 @@ import 'package:titan/user/providers/user_provider.dart'; final isPhAdminProvider = StateProvider((ref) { final me = ref.watch(userProvider); - for (final group in me.groups) { - if (group.name == "ph") { - return true; - } - } - return false; + return me.groups + .map((e) => e.id) + .contains("4ec5ae77-f955-4309-96a5-19cc3c8be71c"); // admin_ph }); diff --git a/lib/ph/router.dart b/lib/ph/router.dart index c9685aae76..07c52b8819 100644 --- a/lib/ph/router.dart +++ b/lib/ph/router.dart @@ -1,9 +1,9 @@ // ignore_for_file: constant_identifier_names -import 'package:either_dart/either.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:titan/drawer/class/module.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/class/module.dart'; import 'package:titan/ph/providers/is_ph_admin_provider.dart'; import 'package:titan/ph/ui/pages/form_page/add_edit_ph_page.dart' deferred as add_edit_ph_page; @@ -26,10 +26,10 @@ class PhRouter { static const String admin = '/admin'; static const String add_ph = '/add_ph'; static final Module module = Module( - name: "PH", - icon: const Left(HeroIcons.newspaper), + getName: (context) => AppLocalizations.of(context)!.modulePh, + getDescription: (context) => + AppLocalizations.of(context)!.modulePhDescription, root: PhRouter.root, - selected: false, ); PhRouter(this.ref); QRoute route() => QRoute( @@ -37,6 +37,10 @@ class PhRouter { path: PhRouter.root, builder: () => main_page.PhMainPage(), middleware: [DeferredLoadingMiddleware(main_page.loadLibrary)], + pageType: QCustomPage( + transitionsBuilder: (_, animation, _, child) => + FadeTransition(opacity: animation, child: child), + ), children: [ QRoute( path: past_ph_selection, diff --git a/lib/ph/tools/constants.dart b/lib/ph/tools/constants.dart deleted file mode 100644 index 249a264f6f..0000000000 --- a/lib/ph/tools/constants.dart +++ /dev/null @@ -1,23 +0,0 @@ -class PhTextConstants { - static const String addNewJournal = "Ajouter un nouveau journal"; - static const String nameField = "Nom : "; - static const String dateField = "Date : "; - static const String delete = "Voulez-vous vraiment supprimer ce journal ?"; - static const String irreversibleAction = "Cette action est irréversible"; - static const String toHeavyFile = "Fichier trop volumineux"; - static const String addPdfFile = "Ajouter un fichier PDF"; - static const String editPdfFile = "Modifier le fichier PDF"; - static const String phName = "Nom du PH"; - static const String date = "Date"; - static const String added = "Ajouté"; - static const String edited = "Modifié"; - static const String addingFileError = "Erreur d'ajout"; - static const String missingInformatonsOrPdf = - "Informations manquantes ou fichier PDF manquant"; - static const String add = "Ajouter"; - static const String edit = "Modifier"; - static const String seePreviousJournal = "Voir les anciens journaux"; - static const String noJournalInDatabase = - "Pas encore de PH dans la base de donnée"; - static const String succesDowloading = "Téléchargé avec succès"; -} diff --git a/lib/ph/tools/functions.dart b/lib/ph/tools/functions.dart index 1820167e0c..4a5d6685cd 100644 --- a/lib/ph/tools/functions.dart +++ b/lib/ph/tools/functions.dart @@ -1,12 +1,12 @@ import 'package:intl/intl.dart'; -String phFormatDate(DateTime date) { - final DateFormat formatter = DateFormat('dd MMMM yyyy', 'fr_FR'); +String phFormatDate(DateTime date, String locale) { + final DateFormat formatter = DateFormat.yMMMMd(locale); return formatter.format(date); } -String phFormatDateEntry(DateTime date) { - final DateFormat formatter = DateFormat('dd/MM/yyyy', 'fr_FR'); +String phFormatDateEntry(DateTime date, String locale) { + final DateFormat formatter = DateFormat.yMMMd(locale); return formatter.format(date); } diff --git a/lib/ph/ui/pages/admin_page/admin_page.dart b/lib/ph/ui/pages/admin_page/admin_page.dart index f99f34daba..f527a7229a 100644 --- a/lib/ph/ui/pages/admin_page/admin_page.dart +++ b/lib/ph/ui/pages/admin_page/admin_page.dart @@ -7,12 +7,12 @@ import 'package:titan/ph/providers/file_picker_result_provider.dart'; import 'package:titan/ph/providers/ph_provider.dart'; import 'package:titan/ph/providers/ph_send_pdf_provider.dart'; import 'package:titan/ph/router.dart'; -import 'package:titan/ph/tools/constants.dart'; import 'package:titan/ph/ui/button.dart'; import 'package:titan/ph/ui/components/year_bar.dart'; import 'package:titan/ph/ui/pages/admin_page/admin_ph_list.dart'; import 'package:titan/ph/ui/pages/ph.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AdminPage extends HookConsumerWidget { const AdminPage({super.key}); @@ -35,7 +35,9 @@ class AdminPage extends HookConsumerWidget { resultNotifier.setFilePickerResult(null); QR.to(PhRouter.root + PhRouter.admin + PhRouter.add_ph); }, - child: const MyButton(text: PhTextConstants.addNewJournal), + child: MyButton( + text: AppLocalizations.of(context)!.phAddNewJournal, + ), ), const SizedBox(height: 20), ], diff --git a/lib/ph/ui/pages/admin_page/admin_ph_card.dart b/lib/ph/ui/pages/admin_page/admin_ph_card.dart index e03b0fd8b6..b1239287e0 100644 --- a/lib/ph/ui/pages/admin_page/admin_ph_card.dart +++ b/lib/ph/ui/pages/admin_page/admin_ph_card.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:titan/ph/class/ph.dart'; -import 'package:titan/ph/tools/constants.dart'; import 'package:titan/ph/tools/functions.dart'; import 'package:titan/tools/ui/layouts/card_button.dart'; import 'package:titan/tools/ui/layouts/card_layout.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AdminPhCard extends StatelessWidget { final VoidCallback onEdit, onDelete; @@ -18,6 +18,7 @@ class AdminPhCard extends StatelessWidget { @override Widget build(BuildContext context) { + final locale = Localizations.localeOf(context).toString(); return GestureDetector( onTap: () {}, child: CardLayout( @@ -29,9 +30,9 @@ class AdminPhCard extends StatelessWidget { children: [ Row( children: [ - const Text( - PhTextConstants.nameField, - style: TextStyle(fontWeight: FontWeight.bold), + Text( + AppLocalizations.of(context)!.phNameField, + style: const TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.left, ), Text(shortenText(ph.name, 28)), @@ -39,12 +40,12 @@ class AdminPhCard extends StatelessWidget { ), Row( children: [ - const Text( - PhTextConstants.dateField, - style: TextStyle(fontWeight: FontWeight.bold), + Text( + AppLocalizations.of(context)!.phDateField, + style: const TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.left, ), - Text(shortenText(phFormatDate(ph.date), 28)), + Text(shortenText(phFormatDate(ph.date, locale), 28)), ], ), ], diff --git a/lib/ph/ui/pages/admin_page/admin_ph_list.dart b/lib/ph/ui/pages/admin_page/admin_ph_list.dart index 92f747ce47..fbdf19236f 100644 --- a/lib/ph/ui/pages/admin_page/admin_ph_list.dart +++ b/lib/ph/ui/pages/admin_page/admin_ph_list.dart @@ -4,11 +4,11 @@ import 'package:titan/ph/providers/ph_list_provider.dart'; import 'package:titan/ph/providers/ph_provider.dart'; import 'package:titan/ph/providers/selected_year_list_provider.dart'; import 'package:titan/ph/router.dart'; -import 'package:titan/ph/tools/constants.dart'; import 'package:titan/ph/ui/pages/admin_page/admin_ph_card.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AdminPhList extends HookConsumerWidget { const AdminPhList({super.key}); @@ -41,8 +41,10 @@ class AdminPhList extends HookConsumerWidget { context: context, builder: (context) { return CustomDialogBox( - title: PhTextConstants.delete, - descriptions: PhTextConstants.irreversibleAction, + title: AppLocalizations.of(context)!.phDelete, + descriptions: AppLocalizations.of( + context, + )!.phIrreversibleAction, onYes: () { phListNotifier.deletePh(ph); }, diff --git a/lib/ph/ui/pages/file_picker/pdf_picker.dart b/lib/ph/ui/pages/file_picker/pdf_picker.dart index 3e696db197..26ad1e842e 100644 --- a/lib/ph/ui/pages/file_picker/pdf_picker.dart +++ b/lib/ph/ui/pages/file_picker/pdf_picker.dart @@ -8,9 +8,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/ph/providers/edit_pdf_provider.dart'; import 'package:titan/ph/providers/file_picker_result_provider.dart'; import 'package:titan/ph/providers/ph_send_pdf_provider.dart'; -import 'package:titan/ph/tools/constants.dart'; import 'package:titan/ph/ui/button.dart'; import 'package:titan/tools/functions.dart'; +import 'package:titan/l10n/app_localizations.dart'; class PdfPicker extends HookConsumerWidget { final bool isEdit; @@ -30,6 +30,7 @@ class PdfPicker extends HookConsumerWidget { height: 40, child: GestureDetector( onTap: () async { + final tooHeavyFileMsg = AppLocalizations.of(context)!.phToHeavyFile; final selectedFile = await FilePicker.platform.pickFiles( allowMultiple: false, type: FileType.custom, @@ -46,10 +47,7 @@ class PdfPicker extends HookConsumerWidget { if (bytes.length < 10000000) { phSendPdfNotifier.set(bytes); } else { - displayToastWithContext( - TypeMsg.error, - PhTextConstants.toHeavyFile, - ); + displayToastWithContext(TypeMsg.error, tooHeavyFileMsg); } } if (isEdit) { @@ -58,10 +56,10 @@ class PdfPicker extends HookConsumerWidget { }, child: MyButton( text: isEdit - ? PhTextConstants.editPdfFile + ? AppLocalizations.of(context)!.phEditPdfFile : (result != null) ? result.files.single.name - : PhTextConstants.addPdfFile, + : AppLocalizations.of(context)!.phAddPdfFile, ), ), ); diff --git a/lib/ph/ui/pages/form_page/add_edit_ph_page.dart b/lib/ph/ui/pages/form_page/add_edit_ph_page.dart index 37a3cf4c4f..5d0484fe80 100644 --- a/lib/ph/ui/pages/form_page/add_edit_ph_page.dart +++ b/lib/ph/ui/pages/form_page/add_edit_ph_page.dart @@ -9,7 +9,6 @@ import 'package:titan/ph/providers/ph_pdf_provider.dart'; import 'package:titan/ph/providers/ph_send_pdf_provider.dart'; import 'package:titan/ph/providers/ph_provider.dart'; import 'package:titan/ph/providers/edit_pdf_provider.dart'; -import 'package:titan/ph/tools/constants.dart'; import 'package:titan/ph/tools/functions.dart'; import 'package:titan/ph/ui/pages/file_picker/pdf_picker.dart'; import 'package:titan/ph/ui/pages/ph.dart'; @@ -20,16 +19,18 @@ import 'package:titan/tools/ui/layouts/add_edit_button_layout.dart'; import 'package:titan/tools/ui/widgets/date_entry.dart'; import 'package:titan/tools/ui/widgets/text_entry.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class PhAddEditPhPage extends HookConsumerWidget { const PhAddEditPhPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final locale = Localizations.localeOf(context).toString(); final ph = ref.watch(phProvider); final isEdit = ph.id != Ph.empty().id; final dateController = TextEditingController( - text: phFormatDateEntry(ph.date), + text: phFormatDateEntry(ph.date, locale), ); final key = GlobalKey(); final name = useTextEditingController(text: ph.name); @@ -55,7 +56,7 @@ class PhAddEditPhPage extends HookConsumerWidget { children: [ TextEntry( maxLines: 1, - label: PhTextConstants.phName, + label: AppLocalizations.of(context)!.phPhName, controller: name, textInputAction: TextInputAction.done, ), @@ -64,7 +65,7 @@ class PhAddEditPhPage extends HookConsumerWidget { child: Column( children: [ DateEntry( - label: PhTextConstants.date, + label: AppLocalizations.of(context)!.phDate, controller: dateController, onTap: () { getOnlyDayDate( @@ -95,6 +96,12 @@ class PhAddEditPhPage extends HookConsumerWidget { if (key.currentState == null) { return; } + final addedPhMsg = isEdit + ? AppLocalizations.of(context)!.phEdited + : AppLocalizations.of(context)!.phAdded; + final phAddingFileErrorMsg = AppLocalizations.of( + context, + )!.phAddingFileError; if (true && (!listEquals(phSendPdf, Uint8List(0)) || isEdit)) { await tokenExpireWrapper(ref, () async { @@ -102,7 +109,10 @@ class PhAddEditPhPage extends HookConsumerWidget { Ph newPh = Ph( id: isEdit ? ph.id : '', date: DateTime.parse( - processDateBack(dateController.text), + processDateBack( + dateController.text, + locale.toString(), + ), ), name: name.text, ); @@ -134,16 +144,14 @@ class PhAddEditPhPage extends HookConsumerWidget { } displayPhToastWithContext( TypeMsg.msg, - isEdit - ? PhTextConstants.edited - : PhTextConstants.added, + addedPhMsg, ); editPdfNotifier.editPdf(false); } } else { displayPhToastWithContext( TypeMsg.error, - PhTextConstants.addingFileError, + phAddingFileErrorMsg, ); } }); @@ -151,12 +159,16 @@ class PhAddEditPhPage extends HookConsumerWidget { displayToast( context, TypeMsg.error, - PhTextConstants.missingInformatonsOrPdf, + AppLocalizations.of( + context, + )!.phMissingInformatonsOrPdf, ); } }, child: Text( - isEdit ? PhTextConstants.edit : PhTextConstants.add, + isEdit + ? AppLocalizations.of(context)!.phEdit + : AppLocalizations.of(context)!.phAdd, style: const TextStyle( color: Colors.white, fontSize: 25, diff --git a/lib/ph/ui/pages/main_page/main_page.dart b/lib/ph/ui/pages/main_page/main_page.dart index 45379c091b..7ef6ec2ba4 100644 --- a/lib/ph/ui/pages/main_page/main_page.dart +++ b/lib/ph/ui/pages/main_page/main_page.dart @@ -5,13 +5,14 @@ import 'package:titan/ph/providers/is_ph_admin_provider.dart'; import 'package:titan/ph/providers/ph_list_provider.dart'; import 'package:titan/ph/providers/ph_pdf_provider.dart'; import 'package:titan/ph/router.dart'; -import 'package:titan/ph/tools/constants.dart'; + import 'package:titan/ph/ui/button.dart'; import 'package:titan/ph/ui/pages/ph.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/widgets/admin_button.dart'; import 'package:pdfx/pdfx.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class PhMainPage extends HookConsumerWidget { const PhMainPage({super.key}); @@ -40,7 +41,9 @@ class PhMainPage extends HookConsumerWidget { onTap: () { QR.to(PhRouter.root + PhRouter.past_ph_selection); }, - child: const MyButton(text: PhTextConstants.seePreviousJournal), + child: MyButton( + text: AppLocalizations.of(context)!.phSeePreviousJournal, + ), ), ), const SizedBox(height: 10), @@ -49,7 +52,9 @@ class PhMainPage extends HookConsumerWidget { builder: (context, phs) { phs.sort((a, b) => a.date.compareTo(b.date)); if (phs.isEmpty) { - return const Text(PhTextConstants.noJournalInDatabase); + return Text( + AppLocalizations.of(context)!.phNoJournalInDatabase, + ); } else { final idLastPh = phs.last.id; final lastPhPdf = ref.watch(phPdfProvider(idLastPh)); diff --git a/lib/ph/ui/pages/past_ph_selection_page/ph_card.dart b/lib/ph/ui/pages/past_ph_selection_page/ph_card.dart index 1e1f90ec26..1055f966d0 100644 --- a/lib/ph/ui/pages/past_ph_selection_page/ph_card.dart +++ b/lib/ph/ui/pages/past_ph_selection_page/ph_card.dart @@ -8,12 +8,12 @@ import 'package:titan/ph/providers/ph_cover_provider.dart'; import 'package:titan/ph/providers/ph_provider.dart'; import 'package:titan/ph/providers/ph_pdf_provider.dart'; import 'package:titan/ph/router.dart'; -import 'package:titan/ph/tools/constants.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/layouts/card_button.dart'; import 'package:titan/tools/ui/layouts/card_layout.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class PhCard extends HookConsumerWidget { final Ph ph; @@ -54,6 +54,9 @@ class PhCard extends HookConsumerWidget { right: 5, child: GestureDetector( onTap: () async { + final successDownloadingMsg = AppLocalizations.of( + context, + )!.phSuccesDowloading; late final Uint8List pdfBytes; try { @@ -80,7 +83,7 @@ class PhCard extends HookConsumerWidget { if (path != null) { displayPhToastWithContext( TypeMsg.msg, - PhTextConstants.succesDowloading, + successDownloadingMsg, ); } }, diff --git a/lib/ph/ui/pages/ph.dart b/lib/ph/ui/pages/ph.dart index 76d2853998..8946f343f3 100644 --- a/lib/ph/ui/pages/ph.dart +++ b/lib/ph/ui/pages/ph.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/ph/router.dart'; import 'package:titan/tools/ui/widgets/top_bar.dart'; +import 'package:titan/tools/constants.dart'; class PhTemplate extends HookConsumerWidget { final Widget child; @@ -9,13 +10,18 @@ class PhTemplate extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const TopBar(title: "PH", root: PhRouter.root), - Expanded(child: child), - ], + return Scaffold( + body: Container( + decoration: const BoxDecoration(color: ColorConstants.background), + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const TopBar(root: PhRouter.root), + Expanded(child: child), + ], + ), + ), ), ); } diff --git a/lib/phonebook/class/association.dart b/lib/phonebook/class/association.dart index fca496dbdb..fe47c1b045 100644 --- a/lib/phonebook/class/association.dart +++ b/lib/phonebook/class/association.dart @@ -3,7 +3,7 @@ class Association { required this.id, required this.name, required this.description, - required this.kind, + required this.groupementId, required this.mandateYear, required this.deactivated, required this.associatedGroups, @@ -12,7 +12,7 @@ class Association { late final String id; late final String name; late final String description; - late final String kind; + late final String groupementId; late final int mandateYear; late final bool deactivated; late final List associatedGroups; @@ -21,7 +21,7 @@ class Association { id = json['id']; name = json['name']; description = json['description']; - kind = json['kind']; + groupementId = json['groupement_id']; mandateYear = json['mandate_year']; deactivated = json['deactivated']; associatedGroups = List.from(json['associated_groups']); @@ -32,7 +32,7 @@ class Association { 'id': id, 'name': name, 'description': description, - 'kind': kind, + 'groupement_id': groupementId, 'mandate_year': mandateYear, 'deactivated': deactivated, 'associated_groups': associatedGroups, @@ -44,7 +44,7 @@ class Association { String? id, String? name, String? description, - String? kind, + String? groupementId, int? mandateYear, bool? deactivated, List? associatedGroups, @@ -53,7 +53,7 @@ class Association { id: id ?? this.id, name: name ?? this.name, description: description ?? this.description, - kind: kind ?? this.kind, + groupementId: groupementId ?? this.groupementId, mandateYear: mandateYear ?? this.mandateYear, deactivated: deactivated ?? this.deactivated, associatedGroups: associatedGroups ?? this.associatedGroups, @@ -64,7 +64,7 @@ class Association { id = ""; name = ""; description = ""; - kind = ""; + groupementId = ""; mandateYear = 0; deactivated = false; associatedGroups = []; @@ -76,6 +76,6 @@ class Association { @override String toString() { - return "Association(Nom : $name, id : $id, description : $description, kind : $kind, mandate_year : $mandateYear, deactivated : $deactivated, associated_groups : $associatedGroups)"; + return "Association(Nom : $name, id : $id, description : $description, groupement_id : $groupementId, mandate_year : $mandateYear, deactivated : $deactivated, associated_groups : $associatedGroups)"; } } diff --git a/lib/phonebook/class/association_groupement.dart b/lib/phonebook/class/association_groupement.dart new file mode 100644 index 0000000000..67b84e765e --- /dev/null +++ b/lib/phonebook/class/association_groupement.dart @@ -0,0 +1,26 @@ +class AssociationGroupement { + AssociationGroupement({required this.id, required this.name}); + + late final String id; + late final String name; + + AssociationGroupement.fromJson(Map json) { + id = json['id']; + name = json['name']; + } + + Map toJson() { + final data = {'id': id, 'name': name}; + return data; + } + + AssociationGroupement.empty() { + id = ""; + name = ""; + } + + @override + String toString() { + return 'AssociationGroupement(kinds: $id, name: $name)'; + } +} diff --git a/lib/phonebook/class/association_kinds.dart b/lib/phonebook/class/association_kinds.dart deleted file mode 100644 index 0a7e60e8f2..0000000000 --- a/lib/phonebook/class/association_kinds.dart +++ /dev/null @@ -1,23 +0,0 @@ -class AssociationKinds { - AssociationKinds({required this.kinds}); - - late final List kinds; - - AssociationKinds.fromJson(Map json) { - kinds = json['kinds'].map((dynamic tag) => tag.toString()).toList(); - } - - Map toJson() { - final data = {'kinds': kinds}; - return data; - } - - AssociationKinds empty() { - return AssociationKinds(kinds: []); - } - - @override - String toString() { - return 'AssociationKinds(kinds: $kinds)'; - } -} diff --git a/lib/phonebook/class/complete_member.dart b/lib/phonebook/class/complete_member.dart index 666c3b0f88..58323c66ed 100644 --- a/lib/phonebook/class/complete_member.dart +++ b/lib/phonebook/class/complete_member.dart @@ -1,4 +1,3 @@ -import 'package:titan/admin/class/account_type.dart'; import 'package:titan/phonebook/class/membership.dart'; import 'member.dart'; @@ -9,16 +8,7 @@ class CompleteMember { late final List memberships; CompleteMember.fromJson(Map json) { - member = Member( - name: json['name'], - firstname: json['firstname'], - nickname: json['nickname'] ?? "", - id: json['id'], - accountType: AccountType(type: json['account_type']), - email: json['email'], - phone: json['phone'], - promotion: json['promo'] ?? 0, - ); + member = Member.fromJson(json); memberships = List.from( json['memberships'].map((membership) { return Membership.fromJson(membership); @@ -60,4 +50,8 @@ class CompleteMember { .firstWhere((element) => element.associationId == associationId) .rolesTags; } + + String getName() { + return "${member.firstname} ${member.name}"; + } } diff --git a/lib/phonebook/class/member.dart b/lib/phonebook/class/member.dart index 149466b0e3..6e01c5e8a0 100644 --- a/lib/phonebook/class/member.dart +++ b/lib/phonebook/class/member.dart @@ -1,11 +1,11 @@ -import 'package:titan/admin/class/account_type.dart'; +import 'package:titan/super_admin/class/account_type.dart'; import 'package:titan/user/class/simple_users.dart'; class Member extends SimpleUser { Member({ required super.name, required super.firstname, - required super.nickname, + super.nickname, required super.id, required super.accountType, required this.email, diff --git a/lib/phonebook/providers/association_filtered_list_provider.dart b/lib/phonebook/providers/association_filtered_list_provider.dart index 1b5e931038..ae533612ba 100644 --- a/lib/phonebook/providers/association_filtered_list_provider.dart +++ b/lib/phonebook/providers/association_filtered_list_provider.dart @@ -1,7 +1,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:titan/phonebook/class/association.dart'; -import 'package:titan/phonebook/providers/association_kind_provider.dart'; -import 'package:titan/phonebook/providers/association_kinds_provider.dart'; +import 'package:titan/phonebook/providers/association_groupement_provider.dart'; +import 'package:titan/phonebook/providers/association_groupement_list_provider.dart'; import 'package:titan/phonebook/providers/association_list_provider.dart'; import 'package:titan/phonebook/providers/research_filter_provider.dart'; import 'package:titan/phonebook/tools/function.dart'; @@ -9,8 +9,8 @@ import 'package:diacritic/diacritic.dart'; final associationFilteredListProvider = Provider>((ref) { final associationsProvider = ref.watch(associationListProvider); - final associationKinds = ref.watch(associationKindsProvider); - final kindFilter = ref.watch(associationKindProvider); + final associationGroupements = ref.watch(associationGroupementListProvider); + final associationGroupement = ref.watch(associationGroupementProvider); final searchFilter = ref.watch(filterProvider); return associationsProvider.maybeWhen( data: (associations) { @@ -21,13 +21,17 @@ final associationFilteredListProvider = Provider>((ref) { ).contains(removeDiacritics(searchFilter.toLowerCase())), ) .toList(); - if (kindFilter != "") { + if (associationGroupement.id != "") { filteredAssociations = filteredAssociations - .where((association) => association.kind == kindFilter) + .where( + (association) => + association.groupementId == associationGroupement.id, + ) .toList(); } - return associationKinds.maybeWhen( - data: (kinds) => sortedAssociationByKind(filteredAssociations, kinds), + return associationGroupements.maybeWhen( + data: (groupements) => + sortedAssociationByKind(filteredAssociations, groupements), orElse: () => filteredAssociations, ); }, diff --git a/lib/phonebook/providers/association_groupement_list_provider.dart b/lib/phonebook/providers/association_groupement_list_provider.dart new file mode 100644 index 0000000000..e61666d9df --- /dev/null +++ b/lib/phonebook/providers/association_groupement_list_provider.dart @@ -0,0 +1,73 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/auth/providers/openid_provider.dart'; +import 'package:titan/phonebook/class/association_groupement.dart'; +import 'package:titan/phonebook/repositories/association_groupement_repository.dart'; +import 'package:titan/tools/providers/list_notifier.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; + +class AssociationGroupementListNotifier + extends ListNotifier { + final AssociationGroupementRepository associationGroupementRepository = + AssociationGroupementRepository(); + AssociationGroupementListNotifier({required String token}) + : super(const AsyncValue.loading()) { + associationGroupementRepository.setToken(token); + } + + Future>> + loadAssociationGroupement() async { + return await loadList( + associationGroupementRepository.getAssociationGroupements, + ); + } + + Future createAssociationGroupement( + AssociationGroupement associationGroupement, + ) async { + return await add( + associationGroupementRepository.createAssociationGroupement, + associationGroupement, + ); + } + + Future updateAssociationGroupement( + AssociationGroupement associationGroupement, + ) async { + return await update( + associationGroupementRepository.updateAssociationGroupement, + (associationGroupements, associationGroupement) => associationGroupements + ..[associationGroupements.indexWhere( + (g) => g.id == associationGroupement.id, + )] = + associationGroupement, + associationGroupement, + ); + } + + Future deleteAssociationGroupement( + AssociationGroupement associationGroupement, + ) async { + return await delete( + associationGroupementRepository.deleteAssociationGroupement, + (associationGroupements, associationGroupement) => + associationGroupements + ..removeWhere((i) => i.id == associationGroupement.id), + associationGroupement.id, + associationGroupement, + ); + } +} + +final associationGroupementListProvider = + StateNotifierProvider< + AssociationGroupementListNotifier, + AsyncValue> + >((ref) { + final token = ref.watch(tokenProvider); + AssociationGroupementListNotifier notifier = + AssociationGroupementListNotifier(token: token); + tokenExpireWrapperAuth(ref, () async { + await notifier.loadAssociationGroupement(); + }); + return notifier; + }); diff --git a/lib/phonebook/providers/association_groupement_provider.dart b/lib/phonebook/providers/association_groupement_provider.dart new file mode 100644 index 0000000000..8b2f1b88fc --- /dev/null +++ b/lib/phonebook/providers/association_groupement_provider.dart @@ -0,0 +1,22 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/phonebook/class/association_groupement.dart'; + +final associationGroupementProvider = + StateNotifierProvider( + (ref) { + return AssociationGroupementNotifier(); + }, + ); + +class AssociationGroupementNotifier + extends StateNotifier { + AssociationGroupementNotifier() : super(AssociationGroupement.empty()); + + void setAssociationGroupement(AssociationGroupement i) { + state = i; + } + + void resetAssociationGroupement() { + state = AssociationGroupement.empty(); + } +} diff --git a/lib/phonebook/providers/association_kind_provider.dart b/lib/phonebook/providers/association_kind_provider.dart deleted file mode 100644 index 26215b091f..0000000000 --- a/lib/phonebook/providers/association_kind_provider.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -final associationKindProvider = - StateNotifierProvider((ref) { - return AssociationKindNotifier(); - }); - -class AssociationKindNotifier extends StateNotifier { - AssociationKindNotifier() : super(""); - - void setKind(String i) { - state = i; - } -} diff --git a/lib/phonebook/providers/association_kinds_provider.dart b/lib/phonebook/providers/association_kinds_provider.dart deleted file mode 100644 index 78c233adb0..0000000000 --- a/lib/phonebook/providers/association_kinds_provider.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:titan/auth/providers/openid_provider.dart'; -import 'package:titan/phonebook/class/association_kinds.dart'; -import 'package:titan/phonebook/repositories/association_repository.dart'; -import 'package:titan/tools/providers/single_notifier.dart'; -import 'package:titan/tools/token_expire_wrapper.dart'; - -class AssociationKindsNotifier extends SingleNotifier { - final AssociationRepository associationRepository = AssociationRepository(); - AssociationKindsNotifier({required String token}) - : super(const AsyncValue.loading()) { - associationRepository.setToken(token); - } - - void setKind(AssociationKinds i) { - state = AsyncValue.data(i); - } - - Future> loadAssociationKinds() async { - return await load(associationRepository.getAssociationKinds); - } -} - -final associationKindsProvider = - StateNotifierProvider< - AssociationKindsNotifier, - AsyncValue - >((ref) { - final token = ref.watch(tokenProvider); - AssociationKindsNotifier notifier = AssociationKindsNotifier( - token: token, - ); - tokenExpireWrapperAuth(ref, () async { - await notifier.loadAssociationKinds(); - }); - return notifier; - }); diff --git a/lib/phonebook/providers/association_member_list_provider.dart b/lib/phonebook/providers/association_member_list_provider.dart index e6495639df..78fdbc9843 100644 --- a/lib/phonebook/providers/association_member_list_provider.dart +++ b/lib/phonebook/providers/association_member_list_provider.dart @@ -17,7 +17,7 @@ class AssociationMemberListNotifier extends ListNotifier { Future>> loadMembers( String associationId, - String year, + int year, ) async { return await loadList( () async => associationMemberRepository.getAssociationMemberList( @@ -86,13 +86,7 @@ class AssociationMemberListNotifier extends ListNotifier { e.associationId == membership.associationId && e.mandateYear == membership.mandateYear, ); - memberships.remove( - memberships.firstWhere( - (e) => - e.associationId == membership.associationId && - e.mandateYear == membership.mandateYear, - ), - ); + memberships.remove(oldMembership); memberships.add(oldMembership.copyWith(order: i)); members[i].copyWith(membership: memberships); } @@ -128,10 +122,7 @@ final associationMemberListProvider = tokenExpireWrapperAuth(ref, () async { final association = ref.watch(associationProvider); - await provider.loadMembers( - association.id, - association.mandateYear.toString(), - ); + await provider.loadMembers(association.id, association.mandateYear); }); return provider; }); diff --git a/lib/phonebook/providers/association_member_sorted_list_provider.dart b/lib/phonebook/providers/association_member_sorted_list_provider.dart index aece1237d5..b41782785e 100644 --- a/lib/phonebook/providers/association_member_sorted_list_provider.dart +++ b/lib/phonebook/providers/association_member_sorted_list_provider.dart @@ -11,7 +11,7 @@ final associationMemberSortedListProvider = Provider>(( final association = ref.watch(associationProvider); return memberListProvider.maybeWhen( data: (members) { - return sortedMembers(members, association.id); + return sortedMembers(members, association); }, orElse: () => List.empty(), ); diff --git a/lib/phonebook/providers/association_picture_provider.dart b/lib/phonebook/providers/association_picture_provider.dart index e875d88304..b675507aeb 100644 --- a/lib/phonebook/providers/association_picture_provider.dart +++ b/lib/phonebook/providers/association_picture_provider.dart @@ -1,41 +1,66 @@ -import 'dart:typed_data'; - import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:titan/auth/providers/openid_provider.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:titan/phonebook/providers/associations_picture_map_provider.dart'; import 'package:titan/phonebook/repositories/association_picture_repository.dart'; import 'package:titan/tools/providers/single_notifier.dart'; -final associationPictureProvider = - StateNotifierProvider>((ref) { - final token = ref.watch(tokenProvider); - AssociationPictureNotifier notifier = AssociationPictureNotifier( - token: token, - ); - return notifier; - }); +class AssociationPictureProvider extends SingleNotifier { + final AssociationPictureRepository associationPictureRepository; + final AssociationPictureMapNotifier associationPictureMapNotifier; + final ImagePicker _picker = ImagePicker(); -class AssociationPictureNotifier extends SingleNotifier { - final AssociationPictureRepository associationPictureRepository = - AssociationPictureRepository(); - AssociationPictureNotifier({required String token}) - : super(const AsyncLoading()) { - associationPictureRepository.setToken(token); - } + AssociationPictureProvider({ + required this.associationPictureRepository, + required this.associationPictureMapNotifier, + }) : super(const AsyncLoading()); Future getAssociationPicture(String associationId) async { - return await associationPictureRepository.getAssociationPicture( + final image = await associationPictureRepository.getAssociationPicture( associationId, ); + associationPictureMapNotifier.setTData(associationId, AsyncData([image])); + state = AsyncData(image); + return image; } - Future updateAssociationPicture( + Future setProfilePicture( + ImageSource source, String associationId, - Uint8List bytes, ) async { - return await associationPictureRepository.addAssociationPicture( - bytes, - associationId, + final previousState = state; + state = const AsyncLoading(); + final XFile? image = await _picker.pickImage( + source: source, + imageQuality: 20, ); + if (image != null) { + try { + final i = await associationPictureRepository.addAssociationPicture( + await image.readAsBytes(), + associationId, + ); + state = AsyncValue.data(i); + associationPictureMapNotifier.setTData(associationId, AsyncData([i])); + return true; + } catch (e) { + state = previousState; + return false; + } + } + state = previousState; + return null; } } + +final associationPictureProvider = + StateNotifierProvider>((ref) { + final associationPicture = ref.watch(associationPictureRepository); + final sessionPosterMapNotifier = ref.watch( + associationPictureMapProvider.notifier, + ); + return AssociationPictureProvider( + associationPictureRepository: associationPicture, + associationPictureMapNotifier: sessionPosterMapNotifier, + ); + }); diff --git a/lib/phonebook/providers/association_provider.dart b/lib/phonebook/providers/association_provider.dart index 29112c185f..85fb315d21 100644 --- a/lib/phonebook/providers/association_provider.dart +++ b/lib/phonebook/providers/association_provider.dart @@ -10,6 +10,10 @@ class AssociationNotifier extends Notifier { void setAssociation(Association association) { state = association; } + + void resetAssociation() { + state = Association.empty(); + } } final associationProvider = NotifierProvider( diff --git a/lib/phonebook/providers/associations_picture_map_provider.dart b/lib/phonebook/providers/associations_picture_map_provider.dart new file mode 100644 index 0000000000..ce98d6f641 --- /dev/null +++ b/lib/phonebook/providers/associations_picture_map_provider.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/tools/providers/map_provider.dart'; + +class AssociationPictureMapNotifier extends MapNotifier { + AssociationPictureMapNotifier() : super(); +} + +final associationPictureMapProvider = + StateNotifierProvider< + AssociationPictureMapNotifier, + Map>?> + >((ref) { + AssociationPictureMapNotifier associationPictureNotifier = + AssociationPictureMapNotifier(); + return associationPictureNotifier; + }); diff --git a/lib/phonebook/providers/associations_pictures_provider.dart b/lib/phonebook/providers/associations_pictures_provider.dart deleted file mode 100644 index e4bbf6c58d..0000000000 --- a/lib/phonebook/providers/associations_pictures_provider.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:titan/phonebook/class/association.dart'; -import 'package:titan/phonebook/providers/association_list_provider.dart'; -import 'package:titan/tools/providers/map_provider.dart'; -import 'package:titan/tools/token_expire_wrapper.dart'; - -class AssociationPictureNotifier extends MapNotifier { - AssociationPictureNotifier() : super(); -} - -final associationPicturesProvider = - StateNotifierProvider< - AssociationPictureNotifier, - Map>?> - >((ref) { - AssociationPictureNotifier associationPictureNotifier = - AssociationPictureNotifier(); - tokenExpireWrapperAuth(ref, () async { - ref - .watch(associationListProvider) - .maybeWhen( - data: (association) { - associationPictureNotifier.loadTList(association); - for (final l in association) { - associationPictureNotifier.setTData( - l, - const AsyncValue.data([]), - ); - } - return associationPictureNotifier; - }, - orElse: () { - associationPictureNotifier.loadTList([]); - return associationPictureNotifier; - }, - ); - }); - return associationPictureNotifier; - }); diff --git a/lib/phonebook/providers/is_phonebook_admin_provider.dart b/lib/phonebook/providers/is_phonebook_admin_provider.dart new file mode 100644 index 0000000000..3ab06abd8d --- /dev/null +++ b/lib/phonebook/providers/is_phonebook_admin_provider.dart @@ -0,0 +1,46 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/admin/providers/is_admin_provider.dart'; +import 'package:titan/phonebook/class/complete_member.dart'; +import 'package:titan/phonebook/providers/association_member_list_provider.dart'; +import 'package:titan/phonebook/providers/association_provider.dart'; +import 'package:titan/phonebook/providers/roles_tags_provider.dart'; +import 'package:titan/phonebook/tools/function.dart'; +import 'package:titan/user/providers/user_provider.dart'; + +final isPhonebookAdminProvider = StateProvider((ref) { + final user = ref.watch(userProvider); + return user.groups + .map((e) => e.id) + .contains("d3f91313-d7e5-49c6-b01f-c19932a7e09b"); // admin_phonebook +}); + +final hasPhonebookAdminAccessProvider = StateProvider((ref) { + final isPhonebookAdmin = ref.watch(isPhonebookAdminProvider); + final isAdmin = ref.watch(isAdminProvider); + return isPhonebookAdmin || isAdmin; +}); + +final isAssociationPresidentProvider = Provider((ref) { + final association = ref.watch(associationProvider); + final rolesTags = ref.watch(rolesTagsProvider); + final membersList = ref.watch(associationMemberListProvider); + final me = ref.watch(userProvider); + + return membersList.maybeWhen( + data: (members) { + final member = members.firstWhere( + (m) => m.member.id == me.id, + orElse: () => CompleteMember.empty(), + ); + if (member.member.id == "") return false; + final membership = getMembershipForAssociation(member, association); + return rolesTags.maybeWhen( + data: (tags) { + return membership.rolesTags.contains(tags.first); + }, + orElse: () => false, + ); + }, + orElse: () => false, + ); +}); diff --git a/lib/phonebook/providers/member_role_tags_provider.dart b/lib/phonebook/providers/member_role_tags_provider.dart deleted file mode 100644 index 5b351c8d3f..0000000000 --- a/lib/phonebook/providers/member_role_tags_provider.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -final memberRoleTagsProvider = - StateNotifierProvider>((ref) { - return MemberRoleTagsProvider(); - }); - -class MemberRoleTagsProvider extends StateNotifier> { - MemberRoleTagsProvider() : super([]); - - void setRoleTagsWithFilter(Map>?> data) { - List newRoleTags = []; - data.forEach((key, value) { - value?.whenData((d) { - if (d[0]) { - newRoleTags.add(key); - } - }); - }); - state = newRoleTags; - } - - void reset() { - state = []; - } -} diff --git a/lib/phonebook/providers/phonebook_admin_provider.dart b/lib/phonebook/providers/phonebook_admin_provider.dart deleted file mode 100644 index bdf53bd438..0000000000 --- a/lib/phonebook/providers/phonebook_admin_provider.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:titan/admin/providers/is_admin_provider.dart'; -import 'package:titan/phonebook/providers/association_member_list_provider.dart'; -import 'package:titan/phonebook/providers/association_provider.dart'; -import 'package:titan/phonebook/tools/constants.dart'; -import 'package:titan/user/providers/user_provider.dart'; - -final isPhonebookAdminProvider = StateProvider((ref) { - final user = ref.watch(userProvider); - if (user.groups - .map((e) => e.id) - .contains("53a669d6-84b1-4352-8d7c-421c1fbd9c6a") || - user.groups - .map((e) => e.id) - .contains("6c6d7e88-fdb8-4e42-b2b5-3d3cfd12e7d6")) { - return true; - } - return false; -}); - -final hasPhonebookAdminAccessProvider = StateProvider((ref) { - final isPhonebookAdmin = ref.watch(isPhonebookAdminProvider); - final isAdmin = ref.watch(isAdminProvider); - return isPhonebookAdmin || isAdmin; -}); - -final isAssociationPresidentProvider = StateProvider((ref) { - final association = ref.watch(associationProvider); - final membersList = ref.watch(associationMemberListProvider); - final me = ref.watch(userProvider); - bool isPresident = false; - membersList.whenData((members) { - if (members.map((e) => e.member.id).contains(me.id)) { - if (members - .firstWhere((completeMember) => completeMember.member.id == me.id) - .memberships - .firstWhere( - (membership) => membership.associationId == association.id, - ) - .rolesTags - .contains(PhonebookTextConstants.presidentRoleTag)) { - isPresident = true; - } - } - }); - return isPresident; -}); diff --git a/lib/phonebook/providers/roles_tags_provider.dart b/lib/phonebook/providers/roles_tags_provider.dart index 1e38843f3e..443a10fea4 100644 --- a/lib/phonebook/providers/roles_tags_provider.dart +++ b/lib/phonebook/providers/roles_tags_provider.dart @@ -1,44 +1,23 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:titan/auth/providers/openid_provider.dart'; -import 'package:titan/phonebook/class/association.dart'; -import 'package:titan/phonebook/class/complete_member.dart'; import 'package:titan/phonebook/repositories/role_tags_repository.dart'; -import 'package:titan/tools/providers/map_provider.dart'; +import 'package:titan/tools/providers/list_notifier.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; -class RolesTagsNotifier extends MapNotifier { +class RolesTagsNotifier extends ListNotifier { final RolesTagsRepository rolesTagsRepository = RolesTagsRepository(); - RolesTagsNotifier({required String token}) { + RolesTagsNotifier({required String token}) + : super(const AsyncValue.loading()) { rolesTagsRepository.setToken(token); } - Future loadRolesTags() async { - loadTList([]); - final result = await rolesTagsRepository.getRolesTags(); - for (int i = 0; i < result.tags.length; i++) { - setTData(result.tags[i], const AsyncData([false])); - } - } - - void resetChecked() { - state.forEach((key, value) => state[key] = const AsyncData([false])); - state = Map.of(state); - } - - void loadRoleTagsFromMember(CompleteMember member, Association association) { - List roleTags = member.getRolesTags(association.id); - for (var value in roleTags) { - state[value] = const AsyncData([true]); - } - state = Map.of(state); + Future>> loadRolesTags() async { + return loadList(rolesTagsRepository.getRolesTags); } } final rolesTagsProvider = - StateNotifierProvider< - RolesTagsNotifier, - Map>?> - >((ref) { + StateNotifierProvider>>((ref) { final token = ref.watch(tokenProvider); RolesTagsNotifier notifier = RolesTagsNotifier(token: token); tokenExpireWrapperAuth(ref, () async { diff --git a/lib/phonebook/repositories/association_groupement_repository.dart b/lib/phonebook/repositories/association_groupement_repository.dart new file mode 100644 index 0000000000..e44e1c4546 --- /dev/null +++ b/lib/phonebook/repositories/association_groupement_repository.dart @@ -0,0 +1,39 @@ +import 'package:titan/phonebook/class/association_groupement.dart'; +import 'package:titan/tools/repository/repository.dart'; + +class AssociationGroupementRepository extends Repository { + @override + // ignore: overridden_fields + final ext = "phonebook/groupements/"; + + Future> getAssociationGroupements() async { + return List.from( + (await getList()).map((x) => AssociationGroupement.fromJson(x)), + ); + } + + Future getAssociationGroupementById(String id) async { + return AssociationGroupement.fromJson(await getOne(id)); + } + + Future updateAssociationGroupement( + AssociationGroupement associationGroupement, + ) async { + return await update( + associationGroupement.toJson(), + associationGroupement.id, + ); + } + + Future createAssociationGroupement( + AssociationGroupement associationGroupement, + ) async { + return AssociationGroupement.fromJson( + await create(associationGroupement.toJson()), + ); + } + + Future deleteAssociationGroupement(String id) async { + return await delete(id); + } +} diff --git a/lib/phonebook/repositories/association_member_repository.dart b/lib/phonebook/repositories/association_member_repository.dart index 9f89f6c87d..935ee08dc6 100644 --- a/lib/phonebook/repositories/association_member_repository.dart +++ b/lib/phonebook/repositories/association_member_repository.dart @@ -9,7 +9,7 @@ class AssociationMemberRepository extends Repository { Future> getAssociationMemberList( String associationId, - String year, + int year, ) async { return List.from( (await getList( diff --git a/lib/phonebook/repositories/association_picture_repository.dart b/lib/phonebook/repositories/association_picture_repository.dart index e882ddac28..21c4ab7eff 100644 --- a/lib/phonebook/repositories/association_picture_repository.dart +++ b/lib/phonebook/repositories/association_picture_repository.dart @@ -1,6 +1,9 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/auth/providers/openid_provider.dart'; import 'package:titan/tools/repository/logo_repository.dart'; class AssociationPictureRepository extends LogoRepository { @@ -11,7 +14,7 @@ class AssociationPictureRepository extends LogoRepository { Future getAssociationPicture(String associationId) async { final uint8List = await getLogo(associationId, suffix: "/picture"); if (uint8List.isEmpty) { - return Image.asset("assets/images/logo.png"); + return Image.asset('assets/images/vache.png'); } return Image.memory(uint8List); } @@ -24,3 +27,10 @@ class AssociationPictureRepository extends LogoRepository { return Image.memory(uint8List); } } + +final associationPictureRepository = Provider(( + ref, +) { + final token = ref.watch(tokenProvider); + return AssociationPictureRepository()..setToken(token); +}); diff --git a/lib/phonebook/repositories/association_repository.dart b/lib/phonebook/repositories/association_repository.dart index 349f817f25..bdfa337542 100644 --- a/lib/phonebook/repositories/association_repository.dart +++ b/lib/phonebook/repositories/association_repository.dart @@ -1,5 +1,4 @@ import 'package:titan/phonebook/class/association.dart'; -import 'package:titan/phonebook/class/association_kinds.dart'; import 'package:titan/tools/repository/repository.dart'; class AssociationRepository extends Repository { @@ -25,10 +24,6 @@ class AssociationRepository extends Repository { return Association.fromJson(await create(association.toJson())); } - Future getAssociationKinds() async { - return AssociationKinds.fromJson(await getOne("kinds")); - } - Future deactivateAssociation(Association association) async { return await update(null, association.id, suffix: "/deactivate"); } diff --git a/lib/phonebook/repositories/role_tags_repository.dart b/lib/phonebook/repositories/role_tags_repository.dart index e8024368e8..4cd75649b6 100644 --- a/lib/phonebook/repositories/role_tags_repository.dart +++ b/lib/phonebook/repositories/role_tags_repository.dart @@ -1,4 +1,3 @@ -import 'package:titan/phonebook/class/roles_tags.dart'; import 'package:titan/tools/repository/repository.dart'; class RolesTagsRepository extends Repository { @@ -6,8 +5,7 @@ class RolesTagsRepository extends Repository { // ignore: overridden_fields final ext = "phonebook/"; - Future getRolesTags() async { - RolesTags rolesTags = RolesTags.fromJson(await getOne("roletags")); - return rolesTags; + Future> getRolesTags() async { + return List.from((await getOne("roletags"))["tags"]); } } diff --git a/lib/phonebook/router.dart b/lib/phonebook/router.dart index 6df9f47b06..c62e6c4282 100644 --- a/lib/phonebook/router.dart +++ b/lib/phonebook/router.dart @@ -1,81 +1,170 @@ -import 'package:either_dart/either.dart'; -import 'package:heroicons/heroicons.dart'; +import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/drawer/class/module.dart'; -import 'package:titan/phonebook/providers/phonebook_admin_provider.dart'; -import 'package:titan/phonebook/ui/pages/admin_page/admin_page.dart'; -import 'package:titan/phonebook/ui/pages/association_creation_page/association_creation_page.dart'; -import 'package:titan/phonebook/ui/pages/association_editor_page/association_editor_page.dart'; -import 'package:titan/phonebook/ui/pages/association_page/association_page.dart'; -import 'package:titan/phonebook/ui/pages/main_page/main_page.dart'; -import 'package:titan/phonebook/ui/pages/member_detail_page/member_detail_page.dart'; -import 'package:titan/phonebook/ui/pages/membership_editor_page/membership_editor_page.dart'; +import 'package:titan/admin/providers/is_admin_provider.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/class/module.dart'; +import 'package:titan/phonebook/providers/is_phonebook_admin_provider.dart'; +import 'package:titan/phonebook/ui/pages/add_edit_groupement_page/groupement_add_edit_page.dart' + deferred as groupement_add_edit_page; +import 'package:titan/phonebook/ui/pages/admin_page/admin_page.dart' + deferred as admin_page; +import 'package:titan/phonebook/ui/pages/association_add_edit_page/association_add_edit_page.dart' + deferred as association_add_edit_page; +import 'package:titan/phonebook/ui/pages/association_groups_page/association_groups_page.dart' + deferred as association_groups_page; +import 'package:titan/phonebook/ui/pages/association_members_page/association_members_page.dart' + deferred as association_members_page; +import 'package:titan/phonebook/ui/pages/association_page/association_page.dart' + deferred as association_page; +import 'package:titan/phonebook/ui/pages/main_page/main_page.dart' + deferred as main_page; +import 'package:titan/phonebook/ui/pages/member_detail_page/member_detail_page.dart' + deferred as member_detail_page; +import 'package:titan/phonebook/ui/pages/membership_editor_page/membership_editor_page.dart' + deferred as membership_editor_page; import 'package:titan/tools/middlewares/admin_middleware.dart'; import 'package:titan/tools/middlewares/authenticated_middleware.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/tools/middlewares/deferred_middleware.dart'; class PhonebookRouter { final Ref ref; static const String root = '/phonebook'; static const String admin = '/admin'; - static const String createAssociaiton = '/create_association'; - static const String editAssociation = '/edit_association'; + static const String addEditGroupement = '/add_edit_groupement'; + static const String addEditAssociation = '/add_edit_association'; + static const String editAssociationMembers = '/edit_association_members'; + static const String editAssociationGroups = '/edit_association_groups'; static const String associationDetail = '/association_detail'; static const String memberDetail = '/member_detail'; static const String addEditMember = '/add_edit_member'; static final Module module = Module( - name: "Annuaire", - icon: const Left(HeroIcons.phone), + getName: (context) => AppLocalizations.of(context)!.modulePhonebook, + getDescription: (context) => + AppLocalizations.of(context)!.modulePhonebookDescription, root: PhonebookRouter.root, - selected: false, ); PhonebookRouter(this.ref); QRoute route() => QRoute( name: "phonebook", path: PhonebookRouter.root, - builder: () => const PhonebookMainPage(), - middleware: [AuthenticatedMiddleware(ref)], + builder: () => main_page.PhonebookMainPage(), + middleware: [ + AuthenticatedMiddleware(ref), + DeferredLoadingMiddleware(main_page.loadLibrary), + ], + pageType: QCustomPage( + transitionsBuilder: (_, animation, _, child) => + FadeTransition(opacity: animation, child: child), + ), children: [ QRoute( path: admin, - builder: () => const AdminPage(), - middleware: [AdminMiddleware(ref, hasPhonebookAdminAccessProvider)], + builder: () => admin_page.AdminPage(), + middleware: [ + AdminMiddleware(ref, hasPhonebookAdminAccessProvider), + DeferredLoadingMiddleware(admin_page.loadLibrary), + ], children: [ QRoute( - path: editAssociation, - builder: () => AssociationEditorPage(), + path: addEditAssociation, + builder: () => association_add_edit_page.AssociationAddEditPage(), + middleware: [ + DeferredLoadingMiddleware(association_add_edit_page.loadLibrary), + AdminMiddleware(ref, isPhonebookAdminProvider), + ], + children: [ + QRoute( + path: addEditGroupement, + builder: () => + groupement_add_edit_page.AssociationGroupementAddEditPage(), + middleware: [ + DeferredLoadingMiddleware( + groupement_add_edit_page.loadLibrary, + ), + AdminMiddleware(ref, isPhonebookAdminProvider), + ], + ), + ], + ), + QRoute( + path: editAssociationMembers, + builder: () => association_members_page.AssociationMembersPage(), + middleware: [ + DeferredLoadingMiddleware(association_members_page.loadLibrary), + AdminMiddleware(ref, isPhonebookAdminProvider), + ], children: [ QRoute( path: addEditMember, - builder: () => const MembershipEditorPage(), + builder: () => membership_editor_page.MembershipEditorPage(), + middleware: [ + DeferredLoadingMiddleware(membership_editor_page.loadLibrary), + ], ), ], ), QRoute( - path: createAssociaiton, - builder: () => AssociationCreationPage(), + path: editAssociationGroups, + builder: () => association_groups_page.AssociationGroupsPage(), + middleware: [ + DeferredLoadingMiddleware(association_groups_page.loadLibrary), + AdminMiddleware(ref, isAdminProvider), + ], ), ], ), QRoute( path: associationDetail, - builder: () => const AssociationPage(), + builder: () => association_page.AssociationPage(), + middleware: [DeferredLoadingMiddleware(association_page.loadLibrary)], children: [ QRoute( - path: editAssociation, - builder: () => AssociationEditorPage(), - middleware: [AdminMiddleware(ref, isAssociationPresidentProvider)], + path: addEditAssociation, + builder: () => association_add_edit_page.AssociationAddEditPage(), + middleware: [ + DeferredLoadingMiddleware(association_add_edit_page.loadLibrary), + AdminMiddleware(ref, isAssociationPresidentProvider), + ], + children: [ + QRoute( + path: addEditGroupement, + builder: () => + groupement_add_edit_page.AssociationGroupementAddEditPage(), + middleware: [ + DeferredLoadingMiddleware( + groupement_add_edit_page.loadLibrary, + ), + AdminMiddleware(ref, isPhonebookAdminProvider), + ], + ), + ], + ), + QRoute( + path: editAssociationMembers, + builder: () => association_members_page.AssociationMembersPage(), + middleware: [ + DeferredLoadingMiddleware(association_members_page.loadLibrary), + AdminMiddleware(ref, isAssociationPresidentProvider), + ], children: [ QRoute( path: addEditMember, - builder: () => const MembershipEditorPage(), + builder: () => membership_editor_page.MembershipEditorPage(), + middleware: [ + DeferredLoadingMiddleware(membership_editor_page.loadLibrary), + ], ), ], ), ], ), - QRoute(path: memberDetail, builder: () => const MemberDetailPage()), + QRoute( + path: memberDetail, + builder: () => member_detail_page.MemberDetailPage(), + middleware: [DeferredLoadingMiddleware(member_detail_page.loadLibrary)], + ), ], ); } diff --git a/lib/phonebook/tools/constants.dart b/lib/phonebook/tools/constants.dart deleted file mode 100644 index d065a5a769..0000000000 --- a/lib/phonebook/tools/constants.dart +++ /dev/null @@ -1,129 +0,0 @@ -import 'package:flutter/material.dart'; - -class PhonebookTextConstants { - static const String activeMandate = "Mandat actif :"; - static const String add = "Ajouter"; - static const String addAssociation = "Ajouter une association"; - static const String addedAssociation = "Association ajoutée"; - static const String addedMember = "Membre ajouté"; - static const String addingError = "Erreur lors de l'ajout"; - static const String addMember = "Ajouter un membre"; - static const String addRole = "Ajouter un rôle"; - static const String admin = "Admin"; - static const String adminPage = "Page Administrateur"; - static const String all = "Toutes"; - static const String apparentName = "Nom public du rôle :"; - static const String association = "Association :"; - static const String associationDetail = "Détail de l'association :"; - static const String associationKind = "Type d'association :"; - static const String associationPure = "Association"; - static const String associationPureSearch = " Association"; - static const String associations = "Associations :"; - - static const String cancel = "Annuler"; - static const String changeMandate = "Passer au mandat "; - static const String changeMandateConfirm = - "Êtes-vous sûr de vouloir changer tout le mandat ?\nCette action est irréversible !"; - static const String copied = "Copié dans le presse-papier"; - - static const String deactivateAssociation = - "Êtes-vous sûr de vouloir désactiver cette association ?\nCette action est irréversible !"; - static const String deactivatedAssociation = "Association désactivée"; - static const String deactivatedAssociationWarning = - "Attention, cette association est désactivée, vous ne pouvez pas la modifier"; - static const String deactivating = "Désactiver l'association ?"; - static const String deactivatingError = "Erreur lors de la désactivation"; - static const String detail = "Détail :"; - static const String deleteAssociation = - "Supprimer l'association ?\nCela va effacer tout l'historique de l'association"; - static const String deletedAssociation = "Association supprimée"; - static const String deletedMember = "Membre supprimé"; - static const String deleting = "Suppression"; - static const String deletingError = "Erreur lors de la suppression"; - static const String description = "Description"; - - static const String edit = "Modifier"; - static const String editMembership = "Modifier le rôle"; - static const String email = "Email :"; - static const String emailCopied = "Email copié dans le presse-papier"; - static const String emptyApparentName = "Veuillez entrer un nom de role"; - static const String emptyFieldError = "Un champ n'est pas rempli"; - static const String emptyKindError = "Veuillez choisir un type d'association"; - static const String emptyMember = "Aucun membre sélectionné"; - static const String errorAssociationLoading = - "Erreur lors du chargement de l'association"; - static const String errorAssociationNameEmpty = - "Veuillez entrer un nom d'association"; - static const String errorAssociationPicture = - "Erreur lors de la modification de la photo d'association"; - static const String errorKindsLoading = - "Erreur lors du chargement des types d'association"; - static const String errorLoadAssociationList = - "Erreur lors du chargement de la liste des associations"; - static const String errorLoadAssociationMember = - "Erreur lors du chargement des membres de l'association"; - static const String errorLoadAssociationPicture = - "Erreur lors du chargement de la photo d'association"; - static const String errorLoadProfilePicture = "Erreur"; - static const String errorRoleTagsLoading = - "Erreur lors du chargement des tags de rôle"; - static const String existingMembership = - "Ce membre est déjà dans le mandat actuel"; - - static const String firstname = "Prénom :"; - - static const String groups = "Groupes associés :"; - - static const String mandateChangingError = - "Erreur lors du changement de mandat"; - static const String member = "Membre"; - static const String memberReordered = "Membre réordonné"; - static const String members = "Membres"; - static const String membershipAssociationError = - "Veuillez choisir une association"; - static const String membershipRole = "Rôle :"; - static const String membershipRoleError = "Veuillez choisir un rôle"; - - static const String name = "Nom :"; - static const String nameCopied = "Nom et prénom copié dans le presse-papier"; - static const String namePure = "Nom"; - static const String newMandate = "Nouveau mandat"; - static const String newMandateConfirmed = "Mandat changé"; - static const String nickname = "Surnom :"; - static const String nicknameCopied = "Surnom copié dans le presse-papier"; - static const String noAssociationFound = "Aucune association trouvée"; - static const String noMember = "Aucun membre"; - static const String noMemberRole = "Aucun role trouvé"; - - static const String phone = "Téléphone :"; - static const String phonebook = "Annuaire"; - static const String phonebookSearch = "Rechercher"; - static const String phonebookSearchAssociation = "Association"; - static const String phonebookSearchField = "Rechercher :"; - static const String phonebookSearchName = "Nom/Prénom/Surnom"; - static const String phonebookSearchRole = "Poste"; - static const String presidentRoleTag = "Prez'"; - static const String promoNotGiven = "Promo non renseignée"; - static const String promotion = "Promotion :"; - - static const String reorderingError = "Erreur lors du réordonnement"; - static const String research = "Rechercher"; - static const String rolePure = "Rôle"; - - static const String tooHeavyAssociationPicture = - "L'image est trop lourde (max 4Mo)"; - - static const String updateGroups = "Mettre à jour les groupes"; - static const String updatedAssociation = "Association modifiée"; - static const String updatedAssociationPicture = - "La photo d'association a été changée"; - static const String updatedGroups = "Groupes mis à jour"; - static const String updatedMember = "Membre modifié"; - static const String updatingError = "Erreur lors de la modification"; - - static const String validation = "Valider"; -} - -class PhonebookColorConstants { - static const Color textDark = Color(0xFF1D1D1D); -} diff --git a/lib/phonebook/tools/function.dart b/lib/phonebook/tools/function.dart index 5c4653d02c..0b0d535fcd 100644 --- a/lib/phonebook/tools/function.dart +++ b/lib/phonebook/tools/function.dart @@ -1,67 +1,53 @@ import 'package:diacritic/diacritic.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/phonebook/class/association.dart'; -import 'package:titan/phonebook/class/association_kinds.dart'; +import 'package:titan/phonebook/class/association_groupement.dart'; import 'package:titan/phonebook/class/complete_member.dart'; import 'package:titan/phonebook/class/membership.dart'; -import 'package:titan/phonebook/providers/roles_tags_provider.dart'; -int getPosition(CompleteMember member, String associationId) { - Membership membership = member.memberships.firstWhere( - (element) => element.associationId == associationId, +Membership getMembershipForAssociation( + CompleteMember member, + Association association, +) { + return member.memberships.firstWhere( + (element) => + element.associationId == association.id && + element.mandateYear == association.mandateYear, + orElse: () => Membership.empty(), ); +} + +int getPosition(CompleteMember member, Association association) { + final membership = getMembershipForAssociation(member, association); return membership.order; } List sortedMembers( List members, - String associationId, + Association association, ) { return members..sort( (a, b) => - getPosition(a, associationId).compareTo(getPosition(b, associationId)), + getPosition(a, association).compareTo(getPosition(b, association)), ); } List sortedAssociationByKind( List associations, - AssociationKinds kinds, + List groupements, ) { - List sorted = []; - List> sortedByKind = List.generate( - kinds.kinds.length, - (index) => [], - ); + Map> sortedByGroupement = { + for (var groupement in groupements) groupement.id: [], + }; for (Association association in associations) { - sortedByKind[kinds.kinds.indexOf(association.kind)].add(association); + sortedByGroupement[association.groupementId]!.add(association); } - for (List list in sortedByKind) { + for (List list in sortedByGroupement.values) { list.sort( (a, b) => removeDiacritics( a.name, ).toLowerCase().compareTo(removeDiacritics(b.name).toLowerCase()), ); - sorted.addAll(list); - } - return sorted; -} - -Color getColorFromTagList(WidgetRef ref, List tags) { - final rolesTags = ref.watch(rolesTagsProvider).keys.toList(); - int index = 3; - for (String tag in tags) { - if (rolesTags.indexOf(tag) < index) { - index = rolesTags.indexOf(tag); - } - } - switch (index) { - case 0: - return const Color.fromARGB(255, 251, 109, 16); - case 1: - return const Color.fromARGB(255, 252, 145, 74); - case 2: - return const Color.fromARGB(255, 253, 193, 153); } - return Colors.white; + // Flatten the sorted map values into a single list + return sortedByGroupement.values.expand((list) => list).toList(); } diff --git a/lib/phonebook/ui/components/association_research_bar.dart b/lib/phonebook/ui/components/association_research_bar.dart new file mode 100644 index 0000000000..e89f234ed4 --- /dev/null +++ b/lib/phonebook/ui/components/association_research_bar.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/phonebook/providers/association_groupement_list_provider.dart'; +import 'package:titan/phonebook/providers/research_filter_provider.dart'; +import 'package:titan/phonebook/ui/components/groupement_bar.dart'; +import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; +import 'package:titan/tools/ui/styleguide/searchbar.dart'; + +class AssociationResearchBar extends HookConsumerWidget { + const AssociationResearchBar({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final filterNotifier = ref.watch(filterProvider.notifier); + final associaiontGroupementList = ref.watch( + associationGroupementListProvider, + ); + + final localizeWithContext = AppLocalizations.of(context)!; + + return AsyncChild( + value: associaiontGroupementList, + builder: (context, groupements) { + return CustomSearchBar( + onSearch: (value) { + filterNotifier.setFilter(value); + }, + onFilter: groupements.length <= 1 + ? null + : () => showCustomBottomModal( + ref: ref, + context: context, + modal: BottomModalTemplate( + title: localizeWithContext.phonebookFilter, + description: localizeWithContext.phonebookFilterDescription, + // "Sélectionnez un ou plusieurs groupements pour filtrer les associations.", + actions: [ + Button( + text: localizeWithContext.phonebookClose, + onPressed: () => Navigator.pop(context), + ), + ], + child: Padding( + padding: EdgeInsetsGeometry.symmetric(vertical: 10), + child: AssociationGroupementBar( + scrollDirection: Axis.vertical, + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/phonebook/ui/components/copiabled_text.dart b/lib/phonebook/ui/components/copiabled_text.dart index d146e7f4e9..e368100546 100644 --- a/lib/phonebook/ui/components/copiabled_text.dart +++ b/lib/phonebook/ui/components/copiabled_text.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:titan/phonebook/tools/constants.dart'; import 'package:titan/tools/functions.dart'; +import 'package:titan/l10n/app_localizations.dart'; class CopiabledText extends StatelessWidget { const CopiabledText( @@ -27,7 +27,10 @@ class CopiabledText extends StatelessWidget { style: style, onTap: () { Clipboard.setData(ClipboardData(text: data)); - displayToastWithContext(TypeMsg.msg, PhonebookTextConstants.copied); + displayToastWithContext( + TypeMsg.msg, + AppLocalizations.of(context)!.phonebookCopied, + ); }, ); } diff --git a/lib/phonebook/ui/components/groupement_bar.dart b/lib/phonebook/ui/components/groupement_bar.dart new file mode 100644 index 0000000000..e635670c82 --- /dev/null +++ b/lib/phonebook/ui/components/groupement_bar.dart @@ -0,0 +1,180 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/phonebook/class/association_groupement.dart'; +import 'package:titan/phonebook/providers/association_groupement_provider.dart'; +import 'package:titan/phonebook/providers/association_groupement_list_provider.dart'; +import 'package:titan/phonebook/router.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; +import 'package:titan/tools/ui/styleguide/item_chip.dart'; + +class AssociationGroupementBar extends HookConsumerWidget { + AssociationGroupementBar({ + super.key, + this.editable = false, + this.scrollDirection = Axis.horizontal, + }); + final dataKey = GlobalKey(); + final bool editable; + final Axis scrollDirection; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final associationGroupement = ref.watch(associationGroupementProvider); + final associationGroupementNotifier = ref.watch( + associationGroupementProvider.notifier, + ); + final associationGroupementList = ref.watch( + associationGroupementListProvider, + ); + final associationGroupementListNotifier = ref.watch( + associationGroupementListProvider.notifier, + ); + + void displayToastWithContext(TypeMsg type, String msg) { + displayToast(context, type, msg); + } + + void popWithContext() { + Navigator.of(context).pop(); + } + + final localizeWithContext = AppLocalizations.of(context)!; + + void showEditDialog(AssociationGroupement item) => showCustomBottomModal( + ref: ref, + context: context, + modal: BottomModalTemplate( + title: item.name, + actions: [ + Button( + text: localizeWithContext.phonebookEdit, + onPressed: () { + associationGroupementNotifier.setAssociationGroupement(item); + QR.to( + PhonebookRouter.root + + PhonebookRouter.admin + + PhonebookRouter.addEditAssociation + + PhonebookRouter.addEditGroupement, + ); + }, + ), + SizedBox(height: 20), + Button.danger( + text: localizeWithContext.phonebookDelete, + onPressed: () async { + final result = await associationGroupementListNotifier + .deleteAssociationGroupement(item); + if (result && context.mounted) { + popWithContext(); + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.phonebookGroupementDeleted, + ); + } + if (!result && context.mounted) { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.phonebookGroupementDeleteError, + ); + } + }, + ), + ], + child: SizedBox.shrink(), + ), + ); + + useEffect(() { + Future(() { + if (associationGroupement.id != "") { + Scrollable.ensureVisible( + dataKey.currentContext!, + duration: const Duration(milliseconds: 500), + alignment: 0.5, + ); + } + }); + return; + }, [dataKey]); + return AsyncChild( + value: associationGroupementList, + builder: (context, associationGroupements) => SizedBox( + width: MediaQuery.of(context).size.width, + height: scrollDirection == Axis.horizontal + ? 40 + : min(associationGroupements.length * 52, 220), + child: ListView.builder( + physics: const BouncingScrollPhysics(), + scrollDirection: scrollDirection, + itemCount: editable + ? associationGroupements.length + 1 + : associationGroupements.length, + itemBuilder: (context, index) { + if (editable && index == 0) { + return Padding( + padding: const EdgeInsets.only(left: 15), + child: ItemChip( + key: Key("add"), + scrollDirection: scrollDirection, + onTap: () { + associationGroupementNotifier.resetAssociationGroupement(); + QR.to( + PhonebookRouter.root + + PhonebookRouter.admin + + PhonebookRouter.addEditAssociation + + PhonebookRouter.addEditGroupement, + ); + }, + child: Text( + "+", + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + ); + } + final item = associationGroupements[editable ? index - 1 : index]; + final selected = associationGroupement.id == item.id; + return Padding( + padding: EdgeInsets.only( + left: index == 0 ? 15 : 0, + right: + index == associationGroupements.length - (editable ? 0 : 1) + ? 15 + : 0, + ), + child: ItemChip( + key: selected ? dataKey : null, + scrollDirection: scrollDirection, + selected: selected, + onTap: () { + associationGroupement.id != item.id + ? associationGroupementNotifier.setAssociationGroupement( + item, + ) + : associationGroupementNotifier + .resetAssociationGroupement(); + }, + onLongPress: () => showEditDialog(item), + child: Text( + item.name, + style: TextStyle( + color: selected ? Colors.white : Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/phonebook/ui/components/kinds_bar.dart b/lib/phonebook/ui/components/kinds_bar.dart deleted file mode 100644 index 7bdc8891c8..0000000000 --- a/lib/phonebook/ui/components/kinds_bar.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/phonebook/providers/association_kind_provider.dart'; -import 'package:titan/phonebook/providers/association_kinds_provider.dart'; -import 'package:titan/tools/ui/builders/async_child.dart'; -import 'package:titan/tools/ui/layouts/item_chip.dart'; - -class KindsBar extends HookConsumerWidget { - KindsBar({super.key}); - final dataKey = GlobalKey(); - @override - Widget build(BuildContext context, WidgetRef ref) { - final kind = ref.watch(associationKindProvider); - final kindNotifier = ref.watch(associationKindProvider.notifier); - final associationKinds = ref.watch(associationKindsProvider); - useEffect(() { - Future(() { - if (kind != "") { - Scrollable.ensureVisible( - dataKey.currentContext!, - duration: const Duration(milliseconds: 500), - alignment: 0.5, - ); - } - }); - return; - }, [dataKey]); - return AsyncChild( - value: associationKinds, - builder: (context, kinds) => SizedBox( - width: MediaQuery.of(context).size.width, - height: 40, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: kinds.kinds.length, - itemBuilder: (context, index) { - final item = kinds.kinds[index]; - final selected = kind == item; - return ItemChip( - key: selected ? dataKey : null, - onTap: () { - kindNotifier.setKind(!selected ? item : ""); - }, - selected: selected, - child: Text( - item, - style: TextStyle( - color: selected ? Colors.white : Colors.black, - fontWeight: FontWeight.bold, - ), - ), - ); - }, - ), - ), - ); - } -} diff --git a/lib/phonebook/ui/components/member_card.dart b/lib/phonebook/ui/components/member_card.dart new file mode 100644 index 0000000000..cad37f1762 --- /dev/null +++ b/lib/phonebook/ui/components/member_card.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/phonebook/class/association.dart'; +import 'package:titan/phonebook/class/complete_member.dart'; +import 'package:titan/phonebook/class/membership.dart'; +import 'package:titan/phonebook/providers/complete_member_provider.dart'; +import 'package:titan/phonebook/providers/member_pictures_provider.dart'; +import 'package:titan/phonebook/providers/profile_picture_provider.dart'; +import 'package:titan/phonebook/router.dart'; +import 'package:titan/phonebook/tools/function.dart'; +import 'package:titan/phonebook/ui/pages/association_members_page/member_edition_modal.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/ui/builders/auto_loader_child.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/list_item_template.dart'; + +class MemberCard extends HookConsumerWidget { + const MemberCard({ + super.key, + required this.member, + required this.association, + required this.deactivated, + this.editable = false, + }); + + final CompleteMember member; + final Association association; + final bool deactivated; + final bool editable; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final profilePictureNotifier = ref.watch(profilePictureProvider.notifier); + final memberNotifier = ref.watch(completeMemberProvider.notifier); + + final memberPictures = ref.watch( + memberPicturesProvider.select((value) => value[member]), + ); + final memberPicturesNotifier = ref.watch(memberPicturesProvider.notifier); + + Membership assoMembership = getMembershipForAssociation( + member, + association, + ); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 5.0), + child: ListItemTemplate( + title: + "${(member.member.nickname ?? member.getName())} - ${assoMembership.apparentName}", + subtitle: member.member.nickname != null + ? "${member.member.firstname} ${member.member.name}" + : null, + icon: AutoLoaderChild( + group: memberPictures, + notifier: memberPicturesNotifier, + mapKey: member, + loader: (ref) => + profilePictureNotifier.getProfilePicture(member.member.id), + loadingBuilder: (context) => const CircleAvatar( + radius: 20, + child: CircularProgressIndicator(), + ), + dataBuilder: (context, data) => CircleAvatar( + radius: 20, + backgroundColor: Colors.white, + backgroundImage: Image(image: data.first.image).image, + ), + ), + onTap: editable + ? () { + showCustomBottomModal( + ref: ref, + context: context, + modal: MemberEditionModal( + member: member, + membership: assoMembership, + ), + ); + } + : () { + memberNotifier.setCompleteMember(member); + QR.to(PhonebookRouter.root + PhonebookRouter.memberDetail); + }, + trailing: editable + ? const HeroIcon( + HeroIcons.chevronUpDown, + color: ColorConstants.tertiary, + ) + : SizedBox.shrink(), + ), + ); + } +} diff --git a/lib/phonebook/ui/components/radio_chip.dart b/lib/phonebook/ui/components/radio_chip.dart deleted file mode 100644 index 5e8047274f..0000000000 --- a/lib/phonebook/ui/components/radio_chip.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter/material.dart'; - -class RadioChip extends StatelessWidget { - final bool selected; - final String label; - final Function() onTap; - const RadioChip({ - super.key, - required this.label, - required this.selected, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 10.0), - child: Chip( - label: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - label, - style: TextStyle( - color: selected ? Colors.white : Colors.black, - fontWeight: FontWeight.bold, - ), - ), - ), - backgroundColor: selected ? Colors.black : Colors.grey.shade200, - ), - ), - ); - } -} diff --git a/lib/phonebook/ui/components/text_input_dialog.dart b/lib/phonebook/ui/components/text_input_dialog.dart index 2b7bd0beeb..2db5213687 100644 --- a/lib/phonebook/ui/components/text_input_dialog.dart +++ b/lib/phonebook/ui/components/text_input_dialog.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/phonebook/tools/constants.dart'; +import 'package:titan/l10n/app_localizations.dart'; class TextInputDialog extends HookConsumerWidget { const TextInputDialog({ @@ -47,14 +47,14 @@ class TextInputDialog extends HookConsumerWidget { onPressed: () { Navigator.of(context).pop(); }, - child: const Text(PhonebookTextConstants.cancel), + child: Text(AppLocalizations.of(context)!.phonebookCancel), ), TextButton( onPressed: () { Navigator.of(context).pop(); onConfirm(); }, - child: const Text(PhonebookTextConstants.validation), + child: Text(AppLocalizations.of(context)!.phonebookValidation), ), ], ); diff --git a/lib/phonebook/ui/pages/add_edit_groupement_page/groupement_add_edit_page.dart b/lib/phonebook/ui/pages/add_edit_groupement_page/groupement_add_edit_page.dart new file mode 100644 index 0000000000..fadafbc7b8 --- /dev/null +++ b/lib/phonebook/ui/pages/add_edit_groupement_page/groupement_add_edit_page.dart @@ -0,0 +1,120 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/phonebook/class/association_groupement.dart'; +import 'package:titan/phonebook/providers/association_groupement_list_provider.dart'; +import 'package:titan/phonebook/providers/association_groupement_provider.dart'; +import 'package:titan/phonebook/ui/phonebook.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; +import 'package:titan/tools/ui/styleguide/text_entry.dart'; + +class AssociationGroupementAddEditPage extends HookConsumerWidget { + const AssociationGroupementAddEditPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final associationGroupement = ref.watch(associationGroupementProvider); + final associaitonGroupementListNotifier = ref.watch( + associationGroupementListProvider.notifier, + ); + final name = useTextEditingController(text: associationGroupement.name); + + void displayToastWithContext(TypeMsg type, String msg) { + displayToast(context, type, msg); + } + + AppLocalizations localizeWithContext = AppLocalizations.of(context)!; + + return PhonebookTemplate( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 20.0), + child: Column( + children: [ + const SizedBox(height: 20), + Align( + alignment: Alignment.centerLeft, + child: Text( + associationGroupement.id.isNotEmpty + ? localizeWithContext.phonebookEditAssociationGroupement + : localizeWithContext.phonebookAddAssociationGroupement, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: ColorConstants.title, + ), + ), + ), + const SizedBox(height: 20), + TextEntry( + controller: name, + label: localizeWithContext.phonebookGroupementName, + canBeEmpty: false, + ), + const SizedBox(height: 20), + Button( + text: associationGroupement.id != "" + ? localizeWithContext.phonebookEdit + : localizeWithContext.phonebookAdd, + onPressed: () async { + if (name.text.isEmpty) { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.phonebookEmptyFieldError, + ); + return; + } + await tokenExpireWrapper(ref, () async { + if (associationGroupement.id != "") { + final value = await associaitonGroupementListNotifier + .updateAssociationGroupement( + AssociationGroupement( + id: associationGroupement.id, + name: name.text, + ), + ); + if (value) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.phonebookAddedAssociation, + ); + QR.back(); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.phonebookUpdatingError, + ); + } + return; + } + final value = await associaitonGroupementListNotifier + .createAssociationGroupement( + AssociationGroupement(id: "", name: name.text), + ); + if (value) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.phonebookAddedAssociation, + ); + QR.back(); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.phonebookAddingError, + ); + } + }); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/phonebook/ui/pages/admin_page/admin_page.dart b/lib/phonebook/ui/pages/admin_page/admin_page.dart index e2743badd4..fa5bd81bfb 100644 --- a/lib/phonebook/ui/pages/admin_page/admin_page.dart +++ b/lib/phonebook/ui/pages/admin_page/admin_page.dart @@ -1,186 +1,124 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/admin/providers/is_admin_provider.dart'; import 'package:titan/phonebook/providers/association_filtered_list_provider.dart'; -import 'package:titan/phonebook/providers/association_kind_provider.dart'; +import 'package:titan/phonebook/providers/association_groupement_list_provider.dart'; +import 'package:titan/phonebook/providers/association_groupement_provider.dart'; import 'package:titan/phonebook/providers/association_list_provider.dart'; import 'package:titan/phonebook/providers/association_provider.dart'; -import 'package:titan/phonebook/providers/phonebook_admin_provider.dart'; +import 'package:titan/phonebook/providers/is_phonebook_admin_provider.dart'; import 'package:titan/phonebook/providers/roles_tags_provider.dart'; import 'package:titan/phonebook/router.dart'; -import 'package:titan/phonebook/tools/constants.dart'; -import 'package:titan/phonebook/ui/components/kinds_bar.dart'; +import 'package:titan/phonebook/ui/components/association_research_bar.dart'; import 'package:titan/phonebook/ui/phonebook.dart'; -import 'package:titan/phonebook/ui/pages/admin_page/association_research_bar.dart'; import 'package:titan/phonebook/ui/pages/admin_page/editable_association_card.dart'; import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/functions.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; -import 'package:titan/tools/ui/layouts/card_layout.dart'; import 'package:titan/tools/ui/layouts/refresher.dart'; -import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/tools/ui/styleguide/icon_button.dart'; +import 'package:tuple/tuple.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AdminPage extends HookConsumerWidget { const AdminPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final associationNotifier = ref.watch(associationProvider.notifier); - final kindNotifier = ref.watch(associationKindProvider.notifier); + final associationGroupementList = ref.watch( + associationGroupementListProvider, + ); final associationListNotifier = ref.watch(associationListProvider.notifier); final associationList = ref.watch(associationListProvider); + final associationNotifier = ref.watch(associationProvider.notifier); + final associationGroupementNotifier = ref.watch( + associationGroupementProvider.notifier, + ); final associationFilteredList = ref.watch(associationFilteredListProvider); final roleNotifier = ref.watch(rolesTagsProvider.notifier); final isPhonebookAdmin = ref.watch(isPhonebookAdminProvider); - void displayToastWithContext(TypeMsg type, String msg) { - displayToast(context, type, msg); - } + final isAdmin = ref.watch(isAdminProvider); + + final localizeWithContext = AppLocalizations.of(context)!; return PhonebookTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await associationListNotifier.loadAssociations(); await roleNotifier.loadRolesTags(); }, - child: Column( - children: [ - const Padding( - padding: EdgeInsets.all(30), - child: AssociationResearchBar(), - ), - const SizedBox(height: 10), - AsyncChild( - value: associationList, - builder: (context, associations) { - return Column( - children: [ - KindsBar(), - GestureDetector( - onTap: isPhonebookAdmin - ? () { - QR.to( - PhonebookRouter.root + - PhonebookRouter.admin + - PhonebookRouter.createAssociaiton, - ); - } - : null, - child: CardLayout( - margin: const EdgeInsets.only( - bottom: 10, - top: 20, - left: 40, - right: 40, - ), - width: double.infinity, - height: 100, - color: isPhonebookAdmin - ? Colors.white - : ColorConstants.deactivated2, - child: Center( - child: HeroIcon( - HeroIcons.plus, - size: 40, - color: Colors.grey.shade500, - ), - ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + Row( + children: [ + Text( + AppLocalizations.of(context)!.phonebookAssociations, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorConstants.title, + ), + ), + const Spacer(), + if (isPhonebookAdmin) + CustomIconButton( + icon: HeroIcon( + HeroIcons.plus, + color: Colors.white, + size: 30, ), + onPressed: () { + associationNotifier.resetAssociation(); + associationGroupementNotifier + .resetAssociationGroupement(); + QR.to( + PhonebookRouter.root + + PhonebookRouter.admin + + PhonebookRouter.addEditAssociation, + ); + }, ), - const SizedBox(height: 30), - if (associations.isEmpty) - const Center( - child: Text(PhonebookTextConstants.noAssociationFound), - ) - else - ...associationFilteredList.map( - (association) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: EditableAssociationCard( + ], + ), + const SizedBox(height: 20), + AssociationResearchBar(), + const SizedBox(height: 10), + Async2Children( + values: Tuple2(associationList, associationGroupementList), + builder: (context, associations, associationGroupements) { + return Column( + children: [ + if (associations.isEmpty) + Center( + child: Text( + localizeWithContext.phonebookNoAssociationFound, + ), + ) + else + ...associationFilteredList.map( + (association) => EditableAssociationCard( association: association, + groupement: associationGroupements.firstWhere( + (groupement) => + groupement.id == association.groupementId, + ), isPhonebookAdmin: isPhonebookAdmin, - onEdit: () { - kindNotifier.setKind(association.kind); - associationNotifier.setAssociation(association); - QR.to( - PhonebookRouter.root + - PhonebookRouter.admin + - PhonebookRouter.editAssociation, - ); - }, - onDelete: () async { - association.deactivated - ? await showDialog( - context: context, - builder: (context) { - return CustomDialogBox( - title: - PhonebookTextConstants.deleting, - descriptions: PhonebookTextConstants - .deleteAssociation, - onYes: () async { - final result = - await associationListNotifier - .deleteAssociation( - association, - ); - if (result) { - displayToastWithContext( - TypeMsg.msg, - PhonebookTextConstants - .deletedAssociation, - ); - } else { - displayToastWithContext( - TypeMsg.error, - PhonebookTextConstants - .deletingError, - ); - } - }, - ); - }, - ) - : await showDialog( - context: context, - builder: (context) { - return CustomDialogBox( - title: PhonebookTextConstants - .deactivating, - descriptions: PhonebookTextConstants - .deactivateAssociation, - onYes: () async { - final result = - await associationListNotifier - .deactivateAssociation( - association, - ); - if (result) { - displayToastWithContext( - TypeMsg.msg, - PhonebookTextConstants - .deactivatedAssociation, - ); - } else { - displayToastWithContext( - TypeMsg.error, - PhonebookTextConstants - .deactivatingError, - ); - } - }, - ); - }, - ); - }, + isAdmin: isAdmin, ), ), - ), - ], - ); - }, - ), - ], + ], + ); + }, + ), + const SizedBox(height: 80), + ], + ), ), ), ); diff --git a/lib/phonebook/ui/pages/admin_page/association_admin_edition_modal.dart b/lib/phonebook/ui/pages/admin_page/association_admin_edition_modal.dart new file mode 100644 index 0000000000..3b40a7e0a7 --- /dev/null +++ b/lib/phonebook/ui/pages/admin_page/association_admin_edition_modal.dart @@ -0,0 +1,213 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/phonebook/class/association.dart'; +import 'package:titan/phonebook/class/association_groupement.dart'; +import 'package:titan/phonebook/providers/association_groupement_provider.dart'; +import 'package:titan/phonebook/providers/association_list_provider.dart'; +import 'package:titan/phonebook/providers/association_picture_provider.dart'; +import 'package:titan/phonebook/providers/association_provider.dart'; +import 'package:titan/phonebook/router.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; +import 'package:titan/tools/ui/styleguide/confirm_modal.dart'; + +class AssociationAdminEditionModal extends HookConsumerWidget { + final Association association; + final AssociationGroupement groupement; + final bool isPhonebookAdmin; + final bool isAdmin; + const AssociationAdminEditionModal({ + super.key, + required this.association, + required this.groupement, + required this.isPhonebookAdmin, + required this.isAdmin, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final associationGroupementsNotifier = ref.watch( + associationGroupementProvider.notifier, + ); + final associationNotifier = ref.watch(associationProvider.notifier); + final associationListNotifier = ref.watch(associationListProvider.notifier); + final associationPictureNotifier = ref.watch( + associationPictureProvider.notifier, + ); + + void displayToastWithContext(TypeMsg type, String msg) { + displayToast(context, type, msg); + } + + AppLocalizations localizeWithContext = AppLocalizations.of(context)!; + + return BottomModalTemplate( + title: association.name, + child: SingleChildScrollView( + child: Column( + children: [ + if (isPhonebookAdmin) ...[ + Button( + text: localizeWithContext.phonebookEditAssociationInfo, + onPressed: () { + associationPictureNotifier.getAssociationPicture( + association.id, + ); + associationGroupementsNotifier.setAssociationGroupement( + groupement, + ); + associationNotifier.setAssociation(association); + Navigator.of(context).pop(); + QR.to( + PhonebookRouter.root + + PhonebookRouter.admin + + PhonebookRouter.addEditAssociation, + ); + }, + ), + SizedBox(height: 5), + Button( + text: localizeWithContext.phonebookEditAssociationMembers, + onPressed: () { + associationGroupementsNotifier.setAssociationGroupement( + groupement, + ); + associationNotifier.setAssociation(association); + Navigator.of(context).pop(); + QR.to( + PhonebookRouter.root + + PhonebookRouter.admin + + PhonebookRouter.editAssociationMembers, + ); + }, + ), + ], + if (isAdmin) ...[ + SizedBox(height: 5), + Button( + text: localizeWithContext.phonebookEditAssociationGroups, + onPressed: () { + associationGroupementsNotifier.setAssociationGroupement( + groupement, + ); + associationNotifier.setAssociation(association); + Navigator.of(context).pop(); + QR.to( + PhonebookRouter.root + + PhonebookRouter.admin + + PhonebookRouter.editAssociationGroups, + ); + }, + ), + ], + if (isPhonebookAdmin) ...[ + SizedBox(height: 15), + Button.danger( + text: localizeWithContext.phonebookChangeTermYear( + association.mandateYear + 1, + ), + onPressed: () { + Navigator.of(context).pop(); + showCustomBottomModal( + context: context, + ref: ref, + modal: ConfirmModal.danger( + title: localizeWithContext.phonebookChangeTermYear( + association.mandateYear + 1, + ), + description: localizeWithContext.globalIrreversibleAction, + onYes: () async { + final result = await associationListNotifier + .updateAssociation( + association.copyWith( + mandateYear: association.mandateYear + 1, + ), + ); + if (result) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.phonebookUpdatedAssociation, + ); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.phonebookUpdatingError, + ); + } + }, + ), + ); + }, + ), + SizedBox(height: 5), + Button.danger( + text: association.deactivated + ? localizeWithContext.phonebookDeleteAssociation + : localizeWithContext.phonebookDeactivateAssociation, + onPressed: () async { + Navigator.of(context).pop(); + showCustomBottomModal( + context: context, + ref: ref, + modal: ConfirmModal.danger( + title: association.deactivated + ? localizeWithContext + .phonebookDeleteSelectedAssociation( + association.name, + ) + : localizeWithContext + .phonebookDeactivateSelectedAssociation( + association.name, + ), + description: association.deactivated + ? localizeWithContext + .phonebookDeleteAssociationDescription + : localizeWithContext.globalIrreversibleAction, + onYes: association.deactivated + ? () async { + final result = await associationListNotifier + .deactivateAssociation(association); + if (result) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext + .phonebookDeactivatedAssociation, + ); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext + .phonebookDeactivatingError, + ); + } + } + : () async { + final result = await associationListNotifier + .deleteAssociation(association); + if (result) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext + .phonebookDeletedAssociation, + ); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.phonebookDeletingError, + ); + } + }, + ), + ); + }, + ), + ], + ], + ), + ), + ); + } +} diff --git a/lib/phonebook/ui/pages/admin_page/association_research_bar.dart b/lib/phonebook/ui/pages/admin_page/association_research_bar.dart deleted file mode 100644 index 86b4a7f703..0000000000 --- a/lib/phonebook/ui/pages/admin_page/association_research_bar.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/phonebook/providers/research_filter_provider.dart'; -import 'package:titan/phonebook/tools/constants.dart'; -import 'package:titan/tools/constants.dart'; - -class AssociationResearchBar extends HookConsumerWidget { - const AssociationResearchBar({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final focusNode = useFocusNode(); - final editingController = useTextEditingController(); - final filterNotifier = ref.watch(filterProvider.notifier); - - return TextField( - onChanged: (value) async { - filterNotifier.setFilter(value); - }, - focusNode: focusNode, - controller: editingController, - cursorColor: PhonebookColorConstants.textDark, - decoration: const InputDecoration( - isDense: true, - suffixIcon: Icon( - Icons.search, - color: PhonebookColorConstants.textDark, - size: 30, - ), - label: Text( - PhonebookTextConstants.research, - style: TextStyle(color: PhonebookColorConstants.textDark), - ), - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide(color: ColorConstants.gradient1), - ), - ), - ); - } -} diff --git a/lib/phonebook/ui/pages/admin_page/delete_button.dart b/lib/phonebook/ui/pages/admin_page/delete_button.dart deleted file mode 100644 index 93b7c44ca3..0000000000 --- a/lib/phonebook/ui/pages/admin_page/delete_button.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/ui/builders/waiting_button.dart'; - -class DeleteButton extends StatelessWidget { - final Future Function() onDelete; - final bool deactivated; - final bool deletion; - - const DeleteButton({ - super.key, - required this.onDelete, - required this.deactivated, - required this.deletion, - }); - - @override - Widget build(BuildContext context) { - return WaitingButton( - builder: (child) => Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: !deactivated - ? [ColorConstants.gradient1, ColorConstants.gradient2] - : [ColorConstants.deactivated1, ColorConstants.deactivated2], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - boxShadow: [ - BoxShadow( - color: !deactivated - ? ColorConstants.gradient2.withValues(alpha: 0.2) - : ColorConstants.deactivated2.withValues(alpha: 0.2), - blurRadius: 10, - offset: const Offset(2, 3), - ), - ], - borderRadius: BorderRadius.circular(10), - ), - child: child, - ), - onTap: !deactivated ? onDelete : () async {}, - child: HeroIcon( - deletion ? HeroIcons.trash : HeroIcons.noSymbol, - size: 30, - color: Colors.white, - ), - ); - } -} diff --git a/lib/phonebook/ui/pages/admin_page/editable_association_card.dart b/lib/phonebook/ui/pages/admin_page/editable_association_card.dart index e2fe0d5b2a..e8faf226ff 100644 --- a/lib/phonebook/ui/pages/admin_page/editable_association_card.dart +++ b/lib/phonebook/ui/pages/admin_page/editable_association_card.dart @@ -1,74 +1,69 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/phonebook/class/association.dart'; -import 'package:titan/phonebook/ui/pages/admin_page/delete_button.dart'; -import 'package:titan/phonebook/ui/pages/admin_page/edition_button.dart'; +import 'package:titan/phonebook/class/association_groupement.dart'; +import 'package:titan/phonebook/providers/association_picture_provider.dart'; +import 'package:titan/phonebook/providers/associations_picture_map_provider.dart'; +import 'package:titan/phonebook/ui/pages/admin_page/association_admin_edition_modal.dart'; +import 'package:titan/tools/ui/builders/auto_loader_child.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/list_item.dart'; class EditableAssociationCard extends HookConsumerWidget { final Association association; + final AssociationGroupement groupement; final bool isPhonebookAdmin; - final void Function() onEdit; - final Future Function() onDelete; + final bool isAdmin; const EditableAssociationCard({ super.key, required this.association, + required this.groupement, required this.isPhonebookAdmin, - required this.onEdit, - required this.onDelete, + required this.isAdmin, }); @override Widget build(BuildContext context, WidgetRef ref) { - return Container( - margin: const EdgeInsets.symmetric(vertical: 5), - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: association.deactivated ? Colors.grey[500] : Colors.white, - borderRadius: BorderRadius.circular(15), - boxShadow: [ - BoxShadow( - color: Colors.grey.withValues(alpha: 0.2), - blurRadius: 5, - spreadRadius: 2, - ), - ], - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - const SizedBox(width: 10), - Expanded( - child: Text( - association.name, - style: const TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - Expanded( - child: Text( - association.kind, - style: const TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.bold, - ), + final associationPicture = ref.watch( + associationPictureMapProvider.select((value) => value[association.id]), + ); + final associationPictureMapNotifier = ref.watch( + associationPictureMapProvider.notifier, + ); + final associationPictureNotifier = ref.watch( + associationPictureProvider.notifier, + ); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: ListItem( + title: association.name, + subtitle: groupement.name, + icon: AutoLoaderChild( + group: associationPicture, + notifier: associationPictureMapNotifier, + mapKey: association.id, + loader: (associationId) => + associationPictureNotifier.getAssociationPicture(associationId), + dataBuilder: (context, data) { + return CircleAvatar( + radius: 20, + backgroundColor: Colors.white, + backgroundImage: Image(image: data.first.image).image, + ); + }, + ), + onTap: () { + showCustomBottomModal( + ref: ref, + context: context, + modal: AssociationAdminEditionModal( + association: association, + groupement: groupement, + isPhonebookAdmin: isPhonebookAdmin, + isAdmin: isAdmin, ), - ), - Row( - children: [ - EditionButton(onEdition: onEdit, deactivated: false), - const SizedBox(width: 5), - DeleteButton( - onDelete: onDelete, - deactivated: !isPhonebookAdmin, - deletion: association.deactivated, - ), - ], - ), - ], + ); + }, ), ); } diff --git a/lib/phonebook/ui/pages/admin_page/edition_button.dart b/lib/phonebook/ui/pages/admin_page/edition_button.dart deleted file mode 100644 index 0610372c55..0000000000 --- a/lib/phonebook/ui/pages/admin_page/edition_button.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/tools/constants.dart'; - -class EditionButton extends HookConsumerWidget { - const EditionButton({ - super.key, - required this.onEdition, - required this.deactivated, - }); - final void Function() onEdition; - final bool deactivated; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return GestureDetector( - onTap: !deactivated ? onEdition : null, - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: !deactivated - ? Colors.grey.shade200 - : ColorConstants.deactivated1, - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: Colors.grey.withValues(alpha: 0.2), - blurRadius: 10, - offset: const Offset(2, 3), - ), - ], - ), - child: const HeroIcon(HeroIcons.pencil, color: Colors.black), - ), - ); - } -} diff --git a/lib/phonebook/ui/pages/association_add_edit_page/association_add_edit_page.dart b/lib/phonebook/ui/pages/association_add_edit_page/association_add_edit_page.dart new file mode 100644 index 0000000000..b14f4b99f7 --- /dev/null +++ b/lib/phonebook/ui/pages/association_add_edit_page/association_add_edit_page.dart @@ -0,0 +1,312 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/phonebook/providers/association_groupement_provider.dart'; +import 'package:titan/phonebook/providers/association_list_provider.dart'; +import 'package:titan/phonebook/providers/association_picture_provider.dart'; +import 'package:titan/phonebook/providers/association_provider.dart'; +import 'package:titan/phonebook/router.dart'; +import 'package:titan/phonebook/ui/components/groupement_bar.dart'; +import 'package:titan/phonebook/ui/phonebook.dart'; +import 'package:titan/settings/ui/pages/main_page/picture_button.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; +import 'package:titan/tools/ui/styleguide/text_entry.dart'; + +class AssociationAddEditPage extends HookConsumerWidget { + const AssociationAddEditPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final key = GlobalKey(); + final associationListNotifier = ref.watch(associationListProvider.notifier); + final associations = ref.watch(associationListProvider); + final association = ref.watch(associationProvider); + final associationNotifier = ref.watch(associationProvider.notifier); + final associationPicture = ref.watch(associationPictureProvider); + final associationPictureNotifier = ref.watch( + associationPictureProvider.notifier, + ); + final associationGroupement = ref.watch(associationGroupementProvider); + final associationGroupementNotifier = ref.watch( + associationGroupementProvider.notifier, + ); + final name = useTextEditingController(text: association.name); + final description = useTextEditingController(text: association.description); + + void displayToastWithContext(TypeMsg type, String msg) { + displayToast(context, type, msg); + } + + final localizeWithContext = AppLocalizations.of(context)!; + + return PhonebookTemplate( + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics( + parent: BouncingScrollPhysics(), + ), + child: Form( + key: key, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + Padding( + padding: EdgeInsets.symmetric(horizontal: 20.0), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + association.id == "" + ? localizeWithContext.phonebookAddAssociation + : localizeWithContext.phonebookEdit, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: ColorConstants.title, + ), + ), + ), + ), + if (association.id != "") ...[ + const SizedBox(height: 30), + AsyncChild( + value: associationPicture, + builder: (context, image) { + return Center( + child: Stack( + clipBehavior: Clip.none, + children: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + spreadRadius: 5, + blurRadius: 10, + offset: const Offset(2, 3), + ), + ], + ), + child: CircleAvatar( + radius: 80, + backgroundColor: Colors.white, + backgroundImage: image.image, + ), + ), + Positioned( + bottom: 0, + left: 0, + child: GestureDetector( + onTap: () async { + final value = await associationPictureNotifier + .setProfilePicture( + ImageSource.gallery, + association.id, + ); + if (value != null) { + if (value) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext + .settingsUpdatedProfilePicture, + ); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext + .settingsTooHeavyProfilePicture, + ); + } + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext + .settingsErrorProfilePicture, + ); + } + }, + child: const PictureButton(icon: HeroIcons.photo), + ), + ), + Positioned( + bottom: 0, + right: 0, + child: GestureDetector( + onTap: () async { + final value = await associationPictureNotifier + .setProfilePicture( + ImageSource.camera, + association.id, + ); + if (value != null) { + if (value) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext + .settingsUpdatedProfilePicture, + ); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext + .settingsTooHeavyProfilePicture, + ); + } + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext + .settingsErrorProfilePicture, + ); + } + }, + child: const PictureButton( + icon: HeroIcons.camera, + ), + ), + ), + ], + ), + ); + }, + ), + ], + const SizedBox(height: 20), + Padding( + padding: EdgeInsets.symmetric(horizontal: 20.0), + child: Text( + localizeWithContext.phonebookAssociationGroupement, + style: TextStyle(fontSize: 16, fontWeight: FontWeight.normal), + ), + ), + const SizedBox(height: 10), + AssociationGroupementBar(editable: true), + Container(margin: const EdgeInsets.symmetric(vertical: 10)), + Padding( + padding: EdgeInsets.symmetric(horizontal: 20.0), + child: TextEntry( + controller: name, + label: localizeWithContext.phonebookAssociationName, + canBeEmpty: false, + ), + ), + const SizedBox(height: 10), + Padding( + padding: EdgeInsets.symmetric(horizontal: 20.0), + child: TextEntry( + controller: description, + label: localizeWithContext.phonebookDescription, + canBeEmpty: true, + ), + ), + const SizedBox(height: 30), + Padding( + padding: EdgeInsets.symmetric(horizontal: 20.0), + child: Button( + onPressed: () async { + if (!key.currentState!.validate()) { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.phonebookEmptyFieldError, + ); + return; + } + if (associationGroupement.id == '') { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.phonebookEmptyKindError, + ); + return; + } + await tokenExpireWrapper(ref, () async { + if (association.id == '') { + final value = await associationListNotifier + .createAssociation( + association.copyWith( + name: name.text, + description: description.text, + groupementId: associationGroupement.id, + mandateYear: DateTime.now().year, + ), + ); + if (value) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.phonebookAddedAssociation, + ); + associations.when( + data: (d) { + associationNotifier.setAssociation(d.last); + QR.to( + PhonebookRouter.root + + PhonebookRouter.admin + + PhonebookRouter.addEditAssociation, + ); + }, + error: (e, s) => displayToastWithContext( + TypeMsg.error, + localizeWithContext + .phonebookErrorAssociationLoading, + ), + loading: () {}, + ); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.phonebookAddingError, + ); + } + } else { + final value = await associationListNotifier + .updateAssociation( + association.copyWith( + name: name.text, + description: description.text, + groupementId: associationGroupement.id, + ), + ); + if (value) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.phonebookUpdatedAssociation, + ); + associationNotifier.setAssociation( + association.copyWith( + name: name.text, + description: description.text, + groupementId: associationGroupement.id, + ), + ); + associationGroupementNotifier + .resetAssociationGroupement(); + QR.back(); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.phonebookUpdatingError, + ); + } + } + }); + }, + text: association.id != "" + ? localizeWithContext.phonebookEdit + : localizeWithContext.phonebookAdd, + ), + ), + SizedBox(height: 80), + ], + ), + ), + ), + ); + } +} diff --git a/lib/phonebook/ui/pages/association_creation_page/association_creation_page.dart b/lib/phonebook/ui/pages/association_creation_page/association_creation_page.dart deleted file mode 100644 index 9da39dafdc..0000000000 --- a/lib/phonebook/ui/pages/association_creation_page/association_creation_page.dart +++ /dev/null @@ -1,160 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/tools/constants.dart'; -import 'package:titan/phonebook/class/association.dart'; -import 'package:titan/phonebook/providers/association_kind_provider.dart'; -import 'package:titan/phonebook/providers/association_list_provider.dart'; -import 'package:titan/phonebook/providers/association_provider.dart'; -import 'package:titan/phonebook/router.dart'; -import 'package:titan/phonebook/tools/constants.dart'; -import 'package:titan/phonebook/ui/components/kinds_bar.dart'; -import 'package:titan/phonebook/ui/phonebook.dart'; -import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:titan/tools/token_expire_wrapper.dart'; -import 'package:titan/tools/ui/builders/waiting_button.dart'; -import 'package:titan/tools/ui/layouts/add_edit_button_layout.dart'; -import 'package:titan/tools/ui/widgets/text_entry.dart'; -import 'package:qlevar_router/qlevar_router.dart'; - -class AssociationCreationPage extends HookConsumerWidget { - final scrollKey = GlobalKey(); - AssociationCreationPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final key = GlobalKey(); - final name = useTextEditingController(); - final description = useTextEditingController(); - final associationListNotifier = ref.watch(associationListProvider.notifier); - final associations = ref.watch(associationListProvider); - final associationNotifier = ref.watch(associationProvider.notifier); - final kind = ref.watch(associationKindProvider); - void displayToastWithContext(TypeMsg type, String msg) { - displayToast(context, type, msg); - } - - return PhonebookTemplate( - child: SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics( - parent: BouncingScrollPhysics(), - ), - child: Form( - key: key, - child: Column( - children: [ - const SizedBox(height: 30), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 30.0), - child: Align( - alignment: Alignment.centerLeft, - child: Text( - PhonebookTextConstants.addAssociation, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w700, - color: ColorConstants.gradient1, - ), - ), - ), - ), - const SizedBox(height: 30), - KindsBar(key: scrollKey), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 30.0), - child: Column( - children: [ - Container(margin: const EdgeInsets.symmetric(vertical: 10)), - TextEntry( - controller: name, - label: AdminTextConstants.name, - canBeEmpty: false, - ), - const SizedBox(height: 30), - TextEntry( - controller: description, - label: AdminTextConstants.description, - canBeEmpty: true, - ), - const SizedBox(height: 50), - WaitingButton( - builder: (child) => AddEditButtonLayout( - colors: const [ - ColorConstants.gradient1, - ColorConstants.gradient2, - ], - child: child, - ), - onTap: () async { - if (!key.currentState!.validate()) { - displayToastWithContext( - TypeMsg.error, - PhonebookTextConstants.emptyFieldError, - ); - return; - } - if (kind == '') { - displayToastWithContext( - TypeMsg.error, - PhonebookTextConstants.emptyKindError, - ); - return; - } - await tokenExpireWrapper(ref, () async { - final value = await associationListNotifier - .createAssociation( - Association.empty().copyWith( - name: name.text, - description: description.text, - kind: kind, - mandateYear: DateTime.now().year, - ), - ); - if (value) { - displayToastWithContext( - TypeMsg.msg, - PhonebookTextConstants.addedAssociation, - ); - associations.when( - data: (d) { - associationNotifier.setAssociation(d.last); - QR.to( - PhonebookRouter.root + - PhonebookRouter.admin + - PhonebookRouter.editAssociation, - ); - }, - error: (e, s) => displayToastWithContext( - TypeMsg.error, - PhonebookTextConstants.errorAssociationLoading, - ), - loading: () {}, - ); - } else { - displayToastWithContext( - TypeMsg.error, - AdminTextConstants.addingError, - ); - } - }); - }, - child: const Text( - AdminTextConstants.add, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Color.fromARGB(255, 255, 255, 255), - ), - ), - ), - ], - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/phonebook/ui/pages/association_creation_page/text_entry.dart b/lib/phonebook/ui/pages/association_creation_page/text_entry.dart deleted file mode 100644 index f3d011e7a4..0000000000 --- a/lib/phonebook/ui/pages/association_creation_page/text_entry.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:titan/admin/tools/constants.dart'; -import 'package:titan/tools/constants.dart'; - -class AddAssociationTextEntry extends StatelessWidget { - final TextEditingController controller; - final String title; - final bool canBeEmpty; - const AddAssociationTextEntry({ - super.key, - required this.controller, - required this.title, - required this.canBeEmpty, - }); - - @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.symmetric(vertical: 20), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - alignment: Alignment.centerLeft, - child: Text( - title, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - color: Color.fromARGB(255, 158, 158, 158), - ), - ), - ), - SizedBox( - child: TextFormField( - controller: controller, - decoration: const InputDecoration( - isDense: true, - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide(color: ColorConstants.gradient1), - ), - ), - validator: canBeEmpty - ? null - : (value) { - if (value == null || value.isEmpty) { - return AdminTextConstants.emptyFieldError; - } - return null; - }, - ), - ), - ], - ), - ); - } -} diff --git a/lib/phonebook/ui/pages/association_editor_page/association_editor_page.dart b/lib/phonebook/ui/pages/association_editor_page/association_editor_page.dart deleted file mode 100644 index 702dc02013..0000000000 --- a/lib/phonebook/ui/pages/association_editor_page/association_editor_page.dart +++ /dev/null @@ -1,328 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/phonebook/class/complete_member.dart'; -import 'package:titan/phonebook/class/membership.dart'; -import 'package:titan/phonebook/providers/association_kind_provider.dart'; -import 'package:titan/phonebook/providers/association_list_provider.dart'; -import 'package:titan/phonebook/providers/association_member_list_provider.dart'; -import 'package:titan/phonebook/providers/association_member_sorted_list_provider.dart'; -import 'package:titan/phonebook/providers/association_picture_provider.dart'; -import 'package:titan/phonebook/providers/association_provider.dart'; -import 'package:titan/phonebook/providers/complete_member_provider.dart'; -import 'package:titan/phonebook/providers/member_role_tags_provider.dart'; -import 'package:titan/phonebook/providers/membership_provider.dart'; -import 'package:titan/phonebook/providers/phonebook_admin_provider.dart'; -import 'package:titan/phonebook/providers/roles_tags_provider.dart'; -import 'package:titan/phonebook/router.dart'; -import 'package:titan/phonebook/tools/constants.dart'; -import 'package:titan/phonebook/ui/pages/association_editor_page/association_information_editor.dart'; -import 'package:titan/phonebook/ui/phonebook.dart'; -import 'package:titan/phonebook/ui/pages/association_editor_page/member_editable_card.dart'; -import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:titan/tools/token_expire_wrapper.dart'; -import 'package:titan/tools/ui/builders/async_child.dart'; -import 'package:titan/tools/ui/builders/waiting_button.dart'; -import 'package:titan/tools/ui/layouts/add_edit_button_layout.dart'; -import 'package:titan/tools/ui/layouts/refresher.dart'; -import 'package:qlevar_router/qlevar_router.dart'; - -class AssociationEditorPage extends HookConsumerWidget { - final scrollKey = GlobalKey(); - AssociationEditorPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final association = ref.watch(associationProvider); - final associationNotifier = ref.watch(associationProvider.notifier); - final associationMemberListNotifier = ref.watch( - associationMemberListProvider.notifier, - ); - final associationMemberList = ref.watch(associationMemberListProvider); - final associationMemberSortedList = ref.watch( - associationMemberSortedListProvider, - ); - final associationPictureNotifier = ref.watch( - associationPictureProvider.notifier, - ); - final associationListNotifier = ref.watch(associationListProvider.notifier); - final rolesTagsNotifier = ref.watch(rolesTagsProvider.notifier); - final membershipNotifier = ref.watch(membershipProvider.notifier); - final completeMemberNotifier = ref.watch(completeMemberProvider.notifier); - final memberRoleTagsNotifier = ref.watch(memberRoleTagsProvider.notifier); - final isPhonebookAdmin = ref.watch(isPhonebookAdminProvider); - final isAssociationPresident = ref.watch(isAssociationPresidentProvider); - final kindNotifier = ref.watch(associationKindProvider.notifier); - - void displayToastWithContext(TypeMsg type, String msg) { - displayToast(context, type, msg); - } - - return PhonebookTemplate( - child: Refresher( - onRefresh: () async { - await associationMemberListNotifier.loadMembers( - association.id, - association.mandateYear.toString(), - ); - await associationPictureNotifier.getAssociationPicture( - association.id, - ); - }, - child: Column( - children: [ - const SizedBox(height: 20), - Container( - padding: const EdgeInsets.symmetric(horizontal: 30), - alignment: Alignment.centerLeft, - child: const Text( - PhonebookTextConstants.edit, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w700, - color: ColorConstants.gradient1, - ), - ), - ), - const SizedBox(height: 20), - AssociationInformationEditor(), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 30), - child: Row( - children: [ - const Text(PhonebookTextConstants.members), - const Spacer(), - WaitingButton( - builder: (child) => Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: - (isPhonebookAdmin || isAssociationPresident) && - !association.deactivated - ? ColorConstants.gradient1 - : ColorConstants.deactivated1, - borderRadius: BorderRadius.circular(10), - ), - child: child, - ), - onTap: - (isPhonebookAdmin || isAssociationPresident) && - !association.deactivated - ? () async { - rolesTagsNotifier.resetChecked(); - memberRoleTagsNotifier.reset(); - completeMemberNotifier.setCompleteMember( - CompleteMember.empty(), - ); - membershipNotifier.setMembership( - Membership.empty().copyWith( - associationId: association.id, - ), - ); - if (QR.currentPath.contains( - PhonebookRouter.admin, - )) { - QR.to( - PhonebookRouter.root + - PhonebookRouter.admin + - PhonebookRouter.editAssociation + - PhonebookRouter.addEditMember, - ); - } else { - QR.to( - PhonebookRouter.root + - PhonebookRouter.associationDetail + - PhonebookRouter.editAssociation + - PhonebookRouter.addEditMember, - ); - } - } - : () async {}, - child: const HeroIcon( - HeroIcons.plus, - size: 30, - color: Colors.white, - ), - ), - ], - ), - ), - const SizedBox(height: 10), - AsyncChild( - value: associationMemberList, - builder: (context, associationMembers) => - associationMembers.isEmpty - ? const Text(PhonebookTextConstants.noMember) - : (isPhonebookAdmin || isAssociationPresident) && - !association.deactivated - ? SizedBox( - height: 400, - child: ReorderableListView( - proxyDecorator: (child, index, animation) { - return Material( - child: FadeTransition( - opacity: animation, - child: child, - ), - ); - }, - onReorder: (int oldIndex, int newIndex) async { - await tokenExpireWrapper(ref, () async { - final result = await associationMemberListNotifier - .reorderMember( - associationMemberSortedList[oldIndex], - associationMemberSortedList[oldIndex] - .memberships - .firstWhere( - (element) => - element.associationId == - association.id && - element.mandateYear == - association.mandateYear, - ) - .copyWith(order: newIndex), - oldIndex, - newIndex, - ); - if (result) { - displayToastWithContext( - TypeMsg.msg, - PhonebookTextConstants.memberReordered, - ); - } else { - displayToastWithContext( - TypeMsg.error, - PhonebookTextConstants.reorderingError, - ); - } - }); - }, - children: associationMemberSortedList - .map( - (member) => MemberEditableCard( - deactivated: false, - key: ValueKey(member.member.id), - member: member, - association: association, - ), - ) - .toList(), - ), - ) - : SizedBox( - height: 400, - child: ListView.builder( - itemCount: associationMembers.length, - itemBuilder: (context, index) { - return MemberEditableCard( - deactivated: true, - key: ValueKey(associationMembers[index].member.id), - member: associationMembers[index], - association: association, - ); - }, - ), - ), - ), - const SizedBox(height: 10), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 30), - child: WaitingButton( - builder: (child) => AddEditButtonLayout( - colors: isPhonebookAdmin && !association.deactivated - ? [ColorConstants.gradient1, ColorConstants.gradient2] - : [ - ColorConstants.deactivated1, - ColorConstants.deactivated2, - ], - child: child, - ), - onTap: isPhonebookAdmin && !association.deactivated - ? () async { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text( - PhonebookTextConstants.newMandate, - ), - content: const Text( - PhonebookTextConstants.changeMandateConfirm, - ), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: const Text( - PhonebookTextConstants.cancel, - ), - ), - TextButton( - onPressed: () async { - Navigator.pop(context); - await tokenExpireWrapper(ref, () async { - final value = await associationListNotifier - .updateAssociation( - association.copyWith( - mandateYear: - association.mandateYear + 1, - ), - ); - if (value) { - displayToastWithContext( - TypeMsg.msg, - PhonebookTextConstants - .newMandateConfirmed, - ); - associationNotifier.setAssociation( - association.copyWith( - mandateYear: - association.mandateYear + 1, - ), - ); - if (QR.currentPath.contains( - PhonebookRouter.associationDetail, - )) { - kindNotifier.setKind(""); - QR.to( - PhonebookRouter.root + - PhonebookRouter.associationDetail, - ); - } - } else { - displayToastWithContext( - TypeMsg.error, - PhonebookTextConstants - .mandateChangingError, - ); - } - }); - }, - child: const Text( - PhonebookTextConstants.validation, - ), - ), - ], - ), - ); - } - : () async {}, - child: Text( - "${PhonebookTextConstants.changeMandate} ${association.mandateYear + 1}", - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Color.fromARGB(255, 255, 255, 255), - ), - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/phonebook/ui/pages/association_editor_page/association_information_editor.dart b/lib/phonebook/ui/pages/association_editor_page/association_information_editor.dart deleted file mode 100644 index e839ab6366..0000000000 --- a/lib/phonebook/ui/pages/association_editor_page/association_information_editor.dart +++ /dev/null @@ -1,368 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/class/simple_group.dart'; -import 'package:titan/admin/providers/group_list_provider.dart'; -import 'package:titan/admin/providers/is_admin_provider.dart'; -import 'package:titan/phonebook/providers/association_kind_provider.dart'; -import 'package:titan/phonebook/providers/association_list_provider.dart'; -import 'package:titan/phonebook/providers/association_provider.dart'; -import 'package:titan/phonebook/providers/phonebook_admin_provider.dart'; -import 'package:titan/phonebook/tools/constants.dart'; -import 'package:titan/phonebook/ui/components/kinds_bar.dart'; -import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:titan/tools/token_expire_wrapper.dart'; -import 'package:titan/tools/ui/builders/waiting_button.dart'; -import 'package:titan/tools/ui/layouts/add_edit_button_layout.dart'; - -class AssociationInformationEditor extends HookConsumerWidget { - final scrollKey = GlobalKey(); - AssociationInformationEditor({super.key}); - - @override - Widget build(context, ref) { - void displayToastWithContext(TypeMsg type, String msg) { - displayToast(context, type, msg); - } - - final kind = ref.watch(associationKindProvider); - final association = ref.watch(associationProvider); - final name = useTextEditingController(text: association.name); - final description = useTextEditingController(text: association.description); - final associationListNotifier = ref.watch(associationListProvider.notifier); - final isAdmin = ref.watch(isAdminProvider); - final isPhonebookAdmin = ref.watch(isPhonebookAdminProvider); - - final groups = ref.watch(allGroupListProvider); - List selectedGroups = groups.maybeWhen( - data: (value) { - return value.where((element) { - return association.associatedGroups.contains(element.id); - }).toList(); - }, - orElse: () { - return []; - }, - ); - final key = GlobalKey(); - - return Column( - children: [ - isPhonebookAdmin && !association.deactivated - ? Form( - key: key, - child: Column( - children: [ - KindsBar(key: scrollKey), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 30), - child: Column( - children: [ - Container( - margin: const EdgeInsets.symmetric(vertical: 10), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - child: TextFormField( - controller: name, - cursorColor: ColorConstants.gradient1, - decoration: InputDecoration( - labelText: - PhonebookTextConstants.namePure, - labelStyle: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - suffixIcon: Container( - padding: const EdgeInsets.all(10), - child: const HeroIcon(HeroIcons.pencil), - ), - enabledBorder: const UnderlineInputBorder( - borderSide: BorderSide( - color: Colors.transparent, - ), - ), - focusedBorder: const UnderlineInputBorder( - borderSide: BorderSide( - color: ColorConstants.gradient1, - ), - ), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return PhonebookTextConstants - .emptyFieldError; - } - return null; - }, - ), - ), - ], - ), - ), - Container( - margin: const EdgeInsets.symmetric(vertical: 10), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - child: TextFormField( - controller: description, - cursorColor: ColorConstants.gradient1, - decoration: InputDecoration( - labelText: - PhonebookTextConstants.description, - labelStyle: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - suffixIcon: Container( - padding: const EdgeInsets.all(10), - child: const HeroIcon(HeroIcons.pencil), - ), - enabledBorder: const UnderlineInputBorder( - borderSide: BorderSide( - color: Colors.transparent, - ), - ), - focusedBorder: const UnderlineInputBorder( - borderSide: BorderSide( - color: ColorConstants.gradient1, - ), - ), - ), - ), - ), - ], - ), - ), - WaitingButton( - builder: (child) => AddEditButtonLayout( - colors: const [ - ColorConstants.gradient1, - ColorConstants.gradient2, - ], - child: child, - ), - onTap: () async { - if (!key.currentState!.validate()) { - return; - } - if (kind == '') { - displayToastWithContext( - TypeMsg.error, - PhonebookTextConstants.emptyKindError, - ); - return; - } - await tokenExpireWrapper(ref, () async { - final value = await associationListNotifier - .updateAssociation( - association.copyWith( - name: name.text, - description: description.text, - kind: kind, - ), - ); - if (value) { - displayToastWithContext( - TypeMsg.msg, - PhonebookTextConstants.updatedAssociation, - ); - } else { - displayToastWithContext( - TypeMsg.msg, - PhonebookTextConstants.updatingError, - ); - } - }); - }, - child: const Text( - PhonebookTextConstants.edit, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Color.fromARGB(255, 255, 255, 255), - ), - ), - ), - ], - ), - ), - ], - ), - ) - : Padding( - padding: const EdgeInsets.symmetric(horizontal: 30), - child: Column( - children: [ - if (association.deactivated) - Container( - margin: const EdgeInsets.symmetric(vertical: 10), - child: const Text( - PhonebookTextConstants.deactivatedAssociationWarning, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.red, - ), - ), - ), - Container( - margin: const EdgeInsets.symmetric(vertical: 10), - child: Text( - association.name, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ), - Container( - margin: const EdgeInsets.symmetric(vertical: 10), - child: SizedBox( - child: Text( - association.description, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ], - ), - ), - if (isAdmin && !association.deactivated) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 30), - child: Column( - children: [ - Container( - margin: const EdgeInsets.symmetric(vertical: 10), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - child: ExpansionTile( - title: const Text(PhonebookTextConstants.groups), - children: groups.maybeWhen( - data: (data) { - return data.map((group) { - return Container( - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 20, - ), - decoration: BoxDecoration( - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.black.withValues( - alpha: 0.1, - ), - offset: const Offset(0, 1), - blurRadius: 4, - spreadRadius: 2, - ), - ], - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - SizedBox( - child: Text( - group.name, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ), - StatefulBuilder( - builder: (context, setState) { - return Checkbox( - value: selectedGroups.contains( - group, - ), - onChanged: (value) { - if (value == true) { - selectedGroups.add(group); - } else { - selectedGroups.remove(group); - } - setState(() {}); - }, - ); - }, - ), - ], - ), - ); - }).toList(); - }, - orElse: () { - return []; - }, - ), - ), - ), - ], - ), - ), - WaitingButton( - builder: (child) => AddEditButtonLayout( - colors: const [ - ColorConstants.gradient1, - ColorConstants.gradient2, - ], - child: child, - ), - onTap: () async { - await tokenExpireWrapper(ref, () async { - final value = await associationListNotifier - .updateAssociationGroups( - association.copyWith( - associatedGroups: selectedGroups - .map((e) => e.id) - .toList(), - ), - ); - if (value) { - displayToastWithContext( - TypeMsg.msg, - PhonebookTextConstants.updatedGroups, - ); - } else { - displayToastWithContext( - TypeMsg.msg, - PhonebookTextConstants.updatingError, - ); - } - }); - }, - child: const Text( - PhonebookTextConstants.updateGroups, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Color.fromARGB(255, 255, 255, 255), - ), - ), - ), - ], - ), - ), - ], - ); - } -} diff --git a/lib/phonebook/ui/pages/association_editor_page/member_editable_card.dart b/lib/phonebook/ui/pages/association_editor_page/member_editable_card.dart deleted file mode 100644 index 587851c886..0000000000 --- a/lib/phonebook/ui/pages/association_editor_page/member_editable_card.dart +++ /dev/null @@ -1,183 +0,0 @@ -import 'package:auto_size_text/auto_size_text.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/phonebook/class/association.dart'; -import 'package:titan/phonebook/class/complete_member.dart'; -import 'package:titan/phonebook/class/membership.dart'; -import 'package:titan/phonebook/providers/association_member_list_provider.dart'; -import 'package:titan/phonebook/providers/complete_member_provider.dart'; -import 'package:titan/phonebook/providers/member_pictures_provider.dart'; -import 'package:titan/phonebook/providers/member_role_tags_provider.dart'; -import 'package:titan/phonebook/providers/membership_provider.dart'; -import 'package:titan/phonebook/providers/profile_picture_provider.dart'; -import 'package:titan/phonebook/providers/roles_tags_provider.dart'; -import 'package:titan/phonebook/router.dart'; -import 'package:titan/phonebook/tools/function.dart'; -import 'package:titan/phonebook/ui/pages/admin_page/delete_button.dart'; -import 'package:titan/phonebook/ui/pages/admin_page/edition_button.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:titan/phonebook/tools/constants.dart'; -import 'package:titan/tools/ui/builders/auto_loader_child.dart'; -import 'package:qlevar_router/qlevar_router.dart'; - -class MemberEditableCard extends HookConsumerWidget { - const MemberEditableCard({ - super.key, - required this.member, - required this.association, - required this.deactivated, - }); - - final CompleteMember member; - final Association association; - final bool deactivated; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final profilePictureNotifier = ref.watch(profilePictureProvider.notifier); - final associationMemberListNotifier = ref.watch( - associationMemberListProvider.notifier, - ); - final roleTagsNotifier = ref.watch(rolesTagsProvider.notifier); - final membershipNotifier = ref.watch(membershipProvider.notifier); - final completeMemberNotifier = ref.watch(completeMemberProvider.notifier); - final memberRoleTagsNotifier = ref.watch(memberRoleTagsProvider.notifier); - void displayToastWithContext(TypeMsg type, String msg) { - displayToast(context, type, msg); - } - - final memberPictures = ref.watch( - memberPicturesProvider.select((value) => value[member]), - ); - final memberPicturesNotifier = ref.watch(memberPicturesProvider.notifier); - - Membership assoMembership = member.memberships.firstWhere( - (memberships) => - memberships.associationId == association.id && - memberships.mandateYear == association.mandateYear, - orElse: () => Membership.empty(), - ); - - return Container( - padding: const EdgeInsets.all(5), - margin: const EdgeInsets.symmetric(horizontal: 30, vertical: 10), - decoration: BoxDecoration( - border: Border.all(), - color: getColorFromTagList( - ref, - member.memberships - .firstWhere( - (element) => - element.associationId == association.id && - element.mandateYear == association.mandateYear, - ) - .rolesTags, - ), - borderRadius: const BorderRadius.all(Radius.circular(20)), - ), - child: Row( - children: [ - Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.1), - spreadRadius: 5, - blurRadius: 10, - offset: const Offset(2, 3), - ), - ], - ), - child: AutoLoaderChild( - group: memberPictures, - notifier: memberPicturesNotifier, - mapKey: member, - loader: (ref) => - profilePictureNotifier.getProfilePicture(member.member.id), - loadingBuilder: (context) => const CircleAvatar( - radius: 20, - child: CircularProgressIndicator(), - ), - dataBuilder: (context, data) => - CircleAvatar(radius: 20, backgroundImage: data.first.image), - ), - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AutoSizeText( - "${(member.member.nickname ?? member.member.firstname)} - ${member.memberships.firstWhere((element) => element.associationId == association.id && element.mandateYear == association.mandateYear).apparentName}", - style: const TextStyle(fontWeight: FontWeight.bold), - minFontSize: 10, - maxFontSize: 15, - ), - const SizedBox(height: 3), - AutoSizeText( - member.member.nickname != null - ? "${member.member.firstname} ${member.member.name}" - : member.member.name, - minFontSize: 10, - maxFontSize: 15, - ), - ], - ), - ), - EditionButton( - deactivated: deactivated, - onEdition: () async { - roleTagsNotifier.resetChecked(); - roleTagsNotifier.loadRoleTagsFromMember(member, association); - completeMemberNotifier.setCompleteMember(member); - membershipNotifier.setMembership(assoMembership); - memberRoleTagsNotifier.reset(); - if (QR.currentPath.contains(PhonebookRouter.admin)) { - QR.to( - PhonebookRouter.root + - PhonebookRouter.admin + - PhonebookRouter.editAssociation + - PhonebookRouter.addEditMember, - ); - } else { - QR.to( - PhonebookRouter.root + - PhonebookRouter.associationDetail + - PhonebookRouter.editAssociation + - PhonebookRouter.addEditMember, - ); - } - }, - ), - const SizedBox(width: 10), - DeleteButton( - deactivated: deactivated, - deletion: true, - onDelete: () async { - final result = await associationMemberListNotifier.deleteMember( - member, - member.memberships.firstWhere( - (element) => - element.associationId == association.id && - element.mandateYear == association.mandateYear, - ), - ); - if (result) { - displayToastWithContext( - TypeMsg.msg, - PhonebookTextConstants.deletedMember, - ); - } else { - displayToastWithContext( - TypeMsg.error, - PhonebookTextConstants.deletingError, - ); - } - }, - ), - ], - ), - ); - } -} diff --git a/lib/phonebook/ui/pages/association_groups_page/association_groups_page.dart b/lib/phonebook/ui/pages/association_groups_page/association_groups_page.dart new file mode 100644 index 0000000000..8c086bdf84 --- /dev/null +++ b/lib/phonebook/ui/pages/association_groups_page/association_groups_page.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/admin/class/simple_group.dart'; +import 'package:titan/admin/providers/group_list_provider.dart'; +import 'package:titan/phonebook/providers/association_groupement_provider.dart'; +import 'package:titan/phonebook/providers/association_list_provider.dart'; +import 'package:titan/phonebook/providers/association_provider.dart'; +import 'package:titan/phonebook/ui/phonebook.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; +import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/ui/layouts/refresher.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; +import 'package:titan/tools/ui/styleguide/list_item_toggle.dart'; + +class AssociationGroupsPage extends HookConsumerWidget { + const AssociationGroupsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final association = ref.watch(associationProvider); + final associationListNotifier = ref.watch(associationListProvider.notifier); + final associationGroupementNotifier = ref.watch( + associationGroupementProvider.notifier, + ); + + final groups = ref.watch(allGroupListProvider); + + AppLocalizations localizeWithContext = AppLocalizations.of(context)!; + + final selectedGroups = groups.maybeWhen( + data: (value) { + return useState>( + List.from( + value.where((element) { + return association.associatedGroups.contains(element.id); + }).toList(), + ), + ); + }, + orElse: () { + return useState>([]); + }, + ); + + void displayToastWithContext(TypeMsg type, String msg) { + displayToast(context, type, msg); + } + + return PhonebookTemplate( + child: Refresher( + controller: ScrollController(), + onRefresh: () async { + await tokenExpireWrapper(ref, () async { + await associationListNotifier.loadAssociations(); + await ref.read(allGroupListProvider.notifier).loadGroups(); + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + Text( + localizeWithContext.phonebookGroups(association.name), + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorConstants.title, + ), + ), + const SizedBox(height: 20), + AsyncChild( + value: groups, + builder: (context, groupList) { + return Column( + children: groupList + .map( + (group) => ToggleListItem( + title: group.name, + selected: selectedGroups.value.contains(group), + onTap: () { + final groups = [...selectedGroups.value]; + if (groups.contains(group)) { + groups.remove(group); + } else { + groups.add(group); + } + selectedGroups.value = groups; + }, + ), + ) + .toList(), + ); + }, + ), + const SizedBox(height: 20), + Button( + onPressed: () async { + await tokenExpireWrapper(ref, () async { + final value = await associationListNotifier + .updateAssociationGroups( + association.copyWith( + associatedGroups: selectedGroups.value + .map((e) => e.id) + .toList(), + ), + ); + if (value) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.phonebookUpdatedGroups, + ); + associationGroupementNotifier + .resetAssociationGroupement(); + QR.back(); + } else { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.phonebookUpdatingError, + ); + } + }); + }, + text: localizeWithContext.phonebookUpdateGroups, + ), + SizedBox(height: 80), + ], + ), + ), + ), + ); + } +} diff --git a/lib/phonebook/ui/pages/association_members_page/association_members_page.dart b/lib/phonebook/ui/pages/association_members_page/association_members_page.dart new file mode 100644 index 0000000000..f6a49db1c9 --- /dev/null +++ b/lib/phonebook/ui/pages/association_members_page/association_members_page.dart @@ -0,0 +1,184 @@ +import 'package:flutter/material.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/phonebook/class/complete_member.dart'; +import 'package:titan/phonebook/class/membership.dart'; +import 'package:titan/phonebook/providers/association_member_list_provider.dart'; +import 'package:titan/phonebook/providers/association_member_sorted_list_provider.dart'; +import 'package:titan/phonebook/providers/association_provider.dart'; +import 'package:titan/phonebook/providers/complete_member_provider.dart'; +import 'package:titan/phonebook/providers/membership_provider.dart'; +import 'package:titan/phonebook/router.dart'; +import 'package:titan/phonebook/ui/components/member_card.dart'; +import 'package:titan/phonebook/ui/phonebook.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; +import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/tools/ui/layouts/refresher.dart'; +import 'package:titan/tools/ui/styleguide/list_item_template.dart'; + +class AssociationMembersPage extends HookConsumerWidget { + const AssociationMembersPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final association = ref.watch(associationProvider); + final associationMemberList = ref.watch(associationMemberListProvider); + final associationMemberListNotifier = ref.watch( + associationMemberListProvider.notifier, + ); + final completeMemberNotifier = ref.watch(completeMemberProvider.notifier); + final membershipNotifier = ref.watch(membershipProvider.notifier); + final associationMemberSortedList = ref.watch( + associationMemberSortedListProvider, + ); + + void displayToastWithContext(TypeMsg type, String msg) { + displayToast(context, type, msg); + } + + AppLocalizations localizeWithContext = AppLocalizations.of(context)!; + + return PhonebookTemplate( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 20), + child: Refresher( + controller: ScrollController(), + onRefresh: () { + return tokenExpireWrapper(ref, () async { + await associationMemberListNotifier.loadMembers( + association.id, + association.mandateYear, + ); + }); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + Text( + localizeWithContext.phonebookMembers(association.name), + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorConstants.title, + ), + ), + if (!association.deactivated) ...[ + SizedBox(height: 20), + ListItemTemplate( + icon: const HeroIcon( + HeroIcons.plus, + size: 40, + color: Colors.black, + ), + title: localizeWithContext.phonebookAddMember, + trailing: SizedBox.shrink(), + onTap: () async { + completeMemberNotifier.setCompleteMember( + CompleteMember.empty(), + ); + membershipNotifier.setMembership( + Membership.empty().copyWith( + associationId: association.id, + ), + ); + if (QR.currentPath.contains(PhonebookRouter.admin)) { + QR.to( + PhonebookRouter.root + + PhonebookRouter.admin + + PhonebookRouter.editAssociationMembers + + PhonebookRouter.addEditMember, + ); + } else { + QR.to( + PhonebookRouter.root + + PhonebookRouter.associationDetail + + PhonebookRouter.editAssociationMembers + + PhonebookRouter.addEditMember, + ); + } + }, + ), + ], + AsyncChild( + value: associationMemberList, + builder: (context, associationMembers) => + associationMembers.isEmpty + ? Text(localizeWithContext.phonebookNoMember) + : !association.deactivated + ? SizedBox( + height: MediaQuery.of(context).size.height - 120, + child: ReorderableListView( + physics: const BouncingScrollPhysics(), + proxyDecorator: (child, index, animation) { + return Transform.scale(scale: 1.05, child: child); + }, + onReorder: (int oldIndex, int newIndex) async { + await tokenExpireWrapper(ref, () async { + final result = await associationMemberListNotifier + .reorderMember( + associationMemberSortedList[oldIndex], + associationMemberSortedList[oldIndex] + .memberships + .firstWhere( + (element) => + element.associationId == + association.id && + element.mandateYear == + association.mandateYear, + ) + .copyWith(order: newIndex), + oldIndex, + newIndex, + ); + if (result) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.phonebookMemberReordered, + ); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.phonebookReorderingError, + ); + } + }); + }, + children: associationMemberSortedList + .map( + (member) => MemberCard( + deactivated: false, + key: ValueKey(member.member.id), + member: member, + association: association, + editable: true, + ), + ) + .toList(), + ), + ) + : Column( + children: associationMemberSortedList + .map( + (e) => MemberCard( + deactivated: true, + member: e, + association: association, + editable: true, + ), + ) + .toList(), + ), + ), + SizedBox(height: 20), + ], + ), + ), + ), + ); + } +} diff --git a/lib/phonebook/ui/pages/association_members_page/member_edition_modal.dart b/lib/phonebook/ui/pages/association_members_page/member_edition_modal.dart new file mode 100644 index 0000000000..88b0141c9a --- /dev/null +++ b/lib/phonebook/ui/pages/association_members_page/member_edition_modal.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/phonebook/class/complete_member.dart'; +import 'package:titan/phonebook/class/membership.dart'; +import 'package:titan/phonebook/providers/association_member_list_provider.dart'; +import 'package:titan/phonebook/providers/association_provider.dart'; +import 'package:titan/phonebook/providers/complete_member_provider.dart'; +import 'package:titan/phonebook/providers/membership_provider.dart'; +import 'package:titan/phonebook/router.dart'; +import 'package:titan/phonebook/tools/function.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; +import 'package:titan/tools/ui/styleguide/confirm_modal.dart'; + +class MemberEditionModal extends HookConsumerWidget { + final CompleteMember member; + final Membership membership; + const MemberEditionModal({ + super.key, + required this.member, + required this.membership, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final completeMemberNotifier = ref.watch(completeMemberProvider.notifier); + final associationMemberListNotifier = ref.watch( + associationMemberListProvider.notifier, + ); + final association = ref.watch(associationProvider); + final membershipNotifier = ref.watch(membershipProvider.notifier); + + void displayToastWithContext(TypeMsg type, String msg) { + displayToast(context, type, msg); + } + + AppLocalizations localizeWithContext = AppLocalizations.of(context)!; + + return BottomModalTemplate( + title: + "${member.member.nickname ?? '${member.member.firstname} ${member.member.name}'} - ${membership.apparentName}", + type: BottomModalType.main, + child: SingleChildScrollView( + child: Column( + children: [ + Button( + text: localizeWithContext.phonebookEditRole, + onPressed: () { + Navigator.of(context).pop(); + completeMemberNotifier.setCompleteMember(member); + membershipNotifier.setMembership(membership); + if (QR.currentPath.contains(PhonebookRouter.admin)) { + QR.to( + PhonebookRouter.root + + PhonebookRouter.admin + + PhonebookRouter.editAssociationMembers + + PhonebookRouter.addEditMember, + ); + } else { + QR.to( + PhonebookRouter.root + + PhonebookRouter.associationDetail + + PhonebookRouter.editAssociationMembers + + PhonebookRouter.addEditMember, + ); + } + }, + ), + SizedBox(height: 20), + Button.danger( + text: localizeWithContext.phonebookDeleteRole, + onPressed: () { + Navigator.of(context).pop(); + showCustomBottomModal( + context: context, + ref: ref, + modal: ConfirmModal.danger( + title: localizeWithContext.phonebookDeleteUserRole( + member.member.nickname ?? member.getName(), + ), + description: localizeWithContext.globalIrreversibleAction, + onYes: () async { + final result = await associationMemberListNotifier + .deleteMember( + member, + getMembershipForAssociation(member, association), + ); + if (result) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.phonebookDeletedMember, + ); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.phonebookDeletingError, + ); + } + }, + onNo: () => Navigator.of(context).pop(), + ), + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/phonebook/ui/pages/association_page/association_edition_modal.dart b/lib/phonebook/ui/pages/association_page/association_edition_modal.dart new file mode 100644 index 0000000000..48891390d6 --- /dev/null +++ b/lib/phonebook/ui/pages/association_page/association_edition_modal.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/phonebook/class/association.dart'; +import 'package:titan/phonebook/class/association_groupement.dart'; +import 'package:titan/phonebook/providers/association_groupement_provider.dart'; +import 'package:titan/phonebook/providers/association_picture_provider.dart'; +import 'package:titan/phonebook/providers/association_provider.dart'; +import 'package:titan/phonebook/router.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; + +class AssociationEditionModal extends HookConsumerWidget { + final Association association; + final AssociationGroupement groupement; + const AssociationEditionModal({ + super.key, + required this.association, + required this.groupement, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final associationGroupementsNotifier = ref.watch( + associationGroupementProvider.notifier, + ); + final associationNotifier = ref.watch(associationProvider.notifier); + final associationPictureNotifier = ref.watch( + associationPictureProvider.notifier, + ); + + AppLocalizations localizeWithContext = AppLocalizations.of(context)!; + + return BottomModalTemplate( + title: association.name, + child: SingleChildScrollView( + child: Column( + children: [ + Button( + text: localizeWithContext.phonebookEditAssociationInfo, + onPressed: () { + associationPictureNotifier.getAssociationPicture( + association.id, + ); + associationGroupementsNotifier.setAssociationGroupement( + groupement, + ); + associationNotifier.setAssociation(association); + Navigator.of(context).pop(); + QR.to( + PhonebookRouter.root + + PhonebookRouter.associationDetail + + PhonebookRouter.addEditAssociation, + ); + }, + ), + SizedBox(height: 5), + Button( + text: localizeWithContext.phonebookEditAssociationMembers, + onPressed: () { + associationGroupementsNotifier.setAssociationGroupement( + groupement, + ); + associationNotifier.setAssociation(association); + Navigator.of(context).pop(); + QR.to( + PhonebookRouter.root + + PhonebookRouter.associationDetail + + PhonebookRouter.editAssociationMembers, + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/phonebook/ui/pages/association_page/association_page.dart b/lib/phonebook/ui/pages/association_page/association_page.dart index 0453aa117c..38c3719a51 100644 --- a/lib/phonebook/ui/pages/association_page/association_page.dart +++ b/lib/phonebook/ui/pages/association_page/association_page.dart @@ -1,21 +1,20 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/phonebook/providers/association_kind_provider.dart'; +import 'package:titan/phonebook/providers/association_groupement_provider.dart'; import 'package:titan/phonebook/providers/association_member_sorted_list_provider.dart'; import 'package:titan/phonebook/providers/association_picture_provider.dart'; import 'package:titan/phonebook/providers/association_provider.dart'; import 'package:titan/phonebook/providers/association_member_list_provider.dart'; -import 'package:titan/phonebook/providers/phonebook_admin_provider.dart'; -import 'package:titan/phonebook/router.dart'; -import 'package:titan/phonebook/tools/constants.dart'; -import 'package:titan/phonebook/ui/pages/association_page/member_card.dart'; -import 'package:titan/phonebook/ui/pages/association_page/web_member_card.dart'; +import 'package:titan/phonebook/providers/is_phonebook_admin_provider.dart'; +import 'package:titan/phonebook/ui/components/member_card.dart'; +import 'package:titan/phonebook/ui/pages/association_page/association_edition_modal.dart'; import 'package:titan/phonebook/ui/phonebook.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/layouts/refresher.dart'; -import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/icon_button.dart'; class AssociationPage extends HookConsumerWidget { const AssociationPage({super.key}); @@ -30,125 +29,106 @@ class AssociationPage extends HookConsumerWidget { final associationMemberListNotifier = ref.watch( associationMemberListProvider.notifier, ); + final associationPicture = ref.watch(associationPictureProvider); final associationPictureNotifier = ref.watch( associationPictureProvider.notifier, ); final isPresident = ref.watch(isAssociationPresidentProvider); - final kindNotifier = ref.watch(associationKindProvider.notifier); + final associationGroupement = ref.watch(associationGroupementProvider); + final localizeWithContext = AppLocalizations.of(context)!; return PhonebookTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await associationMemberListNotifier.loadMembers( association.id, - association.mandateYear.toString(), + association.mandateYear, ); await associationPictureNotifier.getAssociationPicture( association.id, ); }, - child: Align( - alignment: Alignment.topCenter, - child: Stack( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: Column( children: [ - Column( + AsyncChild( + value: associationPicture, + builder: (context, image) { + return Center( + child: CircleAvatar( + radius: 80, + backgroundColor: Colors.white, + backgroundImage: image.image, + ), + ); + }, + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - const SizedBox(height: 20), - Text( - association.name, - style: const TextStyle(fontSize: 40, color: Colors.black), - ), - const SizedBox(height: 10), - Text( - association.kind, - style: const TextStyle(fontSize: 20, color: Colors.black), - ), - const SizedBox(height: 10), - Text( - association.description, - style: const TextStyle(fontSize: 15, color: Colors.black), - ), - const SizedBox(height: 10), - Text( - "${PhonebookTextConstants.activeMandate} ${association.mandateYear}", - style: const TextStyle(fontSize: 15, color: Colors.black), + SizedBox( + width: 300, + child: Text( + association.name, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 40, color: Colors.black), + ), ), - const SizedBox(height: 20), - AsyncChild( - value: associationMemberList, - builder: (context, associationMembers) => - associationMembers.isEmpty - ? const Text(PhonebookTextConstants.noMember) - : Column( - children: associationMemberSortedList - .map( - (member) => kIsWeb - ? WebMemberCard( - member: member, - association: association, - ) - : MemberCard( - member: member, - association: association, - ), - ) - .toList(), + if (isPresident) ...[ + const SizedBox(width: 10), + CustomIconButton.secondary( + onPressed: () { + showCustomBottomModal( + context: context, + modal: AssociationEditionModal( + association: association, + groupement: associationGroupement, ), - ), + ref: ref, + ); + }, + icon: const HeroIcon(HeroIcons.pencilSquare, size: 30), + ), + ], ], ), - if (isPresident) - Positioned( - top: 20, - right: 20, - child: GestureDetector( - onTap: () { - kindNotifier.setKind(association.kind); - QR.to( - PhonebookRouter.root + - PhonebookRouter.associationDetail + - PhonebookRouter.editAssociation, - ); - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - gradient: const RadialGradient( - colors: [ - Color.fromARGB(255, 98, 98, 98), - Color.fromARGB(255, 27, 27, 27), - ], - center: Alignment.topLeft, - radius: 1.3, - ), - boxShadow: [ - BoxShadow( - color: const Color.fromARGB( - 255, - 27, - 27, - 27, - ).withValues(alpha: 0.3), - spreadRadius: 5, - blurRadius: 10, - offset: const Offset( - 3, - 3, - ), // changes position of shadow - ), - ], - ), - child: const HeroIcon( - HeroIcons.pencil, - color: Colors.white, + const SizedBox(height: 10), + Text( + associationGroupement.name, + style: const TextStyle(fontSize: 20, color: Colors.black), + ), + const SizedBox(height: 10), + Text( + localizeWithContext.phonebookTerm(association.mandateYear), + style: const TextStyle(fontSize: 15, color: Colors.black), + ), + const SizedBox(height: 10), + Text( + association.description, + style: const TextStyle(fontSize: 15, color: Colors.black), + ), + const SizedBox(height: 20), + AsyncChild( + value: associationMemberList, + builder: (context, associationMembers) => + associationMembers.isEmpty + ? Text(localizeWithContext.phonebookNoMember) + : Column( + children: associationMemberSortedList + .map( + (member) => MemberCard( + member: member, + association: association, + deactivated: false, + ), + ) + .toList(), ), - ), - ), - ), + ), + const SizedBox(height: 80), ], ), ), diff --git a/lib/phonebook/ui/pages/association_page/card_field.dart b/lib/phonebook/ui/pages/association_page/card_field.dart deleted file mode 100644 index 84f38b2b67..0000000000 --- a/lib/phonebook/ui/pages/association_page/card_field.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:titan/phonebook/ui/components/copiabled_text.dart'; - -class CardField extends StatelessWidget { - final String label; - final String value; - final bool showLabel; - const CardField({ - super.key, - required this.label, - required this.value, - this.showLabel = true, - }); - - @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.symmetric(vertical: 5.0, horizontal: 10), - child: Center( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (showLabel) ...[ - Text(label, style: const TextStyle(fontSize: 16)), - const SizedBox(width: 5), - ], - CopiabledText( - value, - maxLines: 1, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), - ], - ), - ), - ); - } -} diff --git a/lib/phonebook/ui/pages/association_page/member_card.dart b/lib/phonebook/ui/pages/association_page/member_card.dart deleted file mode 100644 index c1bc07159b..0000000000 --- a/lib/phonebook/ui/pages/association_page/member_card.dart +++ /dev/null @@ -1,148 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/phonebook/class/association.dart'; -import 'package:titan/phonebook/class/complete_member.dart'; -import 'package:titan/phonebook/class/membership.dart'; -import 'package:titan/phonebook/providers/member_pictures_provider.dart'; -import 'package:titan/phonebook/providers/profile_picture_provider.dart'; -import 'package:titan/phonebook/router.dart'; -import 'package:titan/phonebook/tools/constants.dart'; -import 'package:titan/phonebook/providers/complete_member_provider.dart'; -import 'package:titan/phonebook/tools/function.dart'; -import 'package:titan/tools/ui/builders/auto_loader_child.dart'; -import 'package:titan/tools/ui/layouts/card_layout.dart'; -import 'package:qlevar_router/qlevar_router.dart'; - -class MemberCard extends HookConsumerWidget { - const MemberCard({ - super.key, - required this.member, - required this.association, - }); - - final CompleteMember member; - final Association association; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final memberNotifier = ref.watch(completeMemberProvider.notifier); - - final memberPictures = ref.watch( - memberPicturesProvider.select((value) => value[member]), - ); - final memberPicturesNotifier = ref.watch(memberPicturesProvider.notifier); - final profilePictureNotifier = ref.watch(profilePictureProvider.notifier); - - Membership? assoMembership = member.memberships.firstWhereOrNull( - (memberships) => - memberships.associationId == association.id && - memberships.mandateYear == association.mandateYear, - ); - - return GestureDetector( - onTap: () { - memberNotifier.setCompleteMember(member); - QR.to(PhonebookRouter.root + PhonebookRouter.memberDetail); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 5), - child: CardLayout( - color: getColorFromTagList( - ref, - member.memberships - .firstWhere( - (element) => - element.associationId == association.id && - element.mandateYear == association.mandateYear, - ) - .rolesTags, - ), - margin: EdgeInsets.zero, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.1), - spreadRadius: 5, - blurRadius: 10, - offset: const Offset(2, 3), - ), - ], - ), - child: AutoLoaderChild( - group: memberPictures, - notifier: memberPicturesNotifier, - mapKey: member, - loader: (ref) => profilePictureNotifier.getProfilePicture( - member.member.id, - ), - loadingBuilder: (context) => const CircleAvatar( - radius: 20, - child: CircularProgressIndicator(), - ), - dataBuilder: (context, data) => CircleAvatar( - radius: 20, - backgroundImage: data.first.image, - ), - ), - ), - const SizedBox(width: 10), - if ((member.member.nickname != null) && - (member.member.nickname != "")) ...[ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - member.member.nickname!, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - Text( - "(${member.member.name} ${member.member.firstname})", - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: Color.fromARGB(255, 115, 115, 115), - ), - ), - ], - ), - ] else - Text( - "${member.member.name} ${member.member.firstname}", - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - ], - ), - Flexible( - child: Text( - textAlign: TextAlign.right, - assoMembership == null - ? PhonebookTextConstants.noMemberRole - : assoMembership.apparentName, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/phonebook/ui/pages/association_page/web_member_card.dart b/lib/phonebook/ui/pages/association_page/web_member_card.dart deleted file mode 100644 index d944ce67a8..0000000000 --- a/lib/phonebook/ui/pages/association_page/web_member_card.dart +++ /dev/null @@ -1,283 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/phonebook/class/association.dart'; -import 'package:titan/phonebook/class/complete_member.dart'; -import 'package:titan/phonebook/class/membership.dart'; -import 'package:titan/phonebook/providers/member_pictures_provider.dart'; -import 'package:titan/phonebook/providers/profile_picture_provider.dart'; -import 'package:titan/phonebook/providers/complete_member_provider.dart'; -import 'package:titan/phonebook/router.dart'; -import 'package:titan/phonebook/tools/constants.dart'; -import 'package:titan/phonebook/tools/function.dart'; -import 'package:titan/phonebook/ui/pages/association_page/card_field.dart'; -import 'package:titan/tools/ui/builders/auto_loader_child.dart'; -import 'package:titan/tools/ui/layouts/card_layout.dart'; -import 'package:qlevar_router/qlevar_router.dart'; - -class WebMemberCard extends HookConsumerWidget { - const WebMemberCard({ - super.key, - required this.member, - required this.association, - }); - - final CompleteMember member; - final Association association; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final memberNotifier = ref.watch(completeMemberProvider.notifier); - - final memberPictures = ref.watch( - memberPicturesProvider.select((value) => value[member]), - ); - final memberPicturesNotifier = ref.watch(memberPicturesProvider.notifier); - final profilePictureNotifier = ref.watch(profilePictureProvider.notifier); - - Membership? assoMembership = member.memberships.firstWhereOrNull( - (memberships) => - memberships.associationId == association.id && - memberships.mandateYear == association.mandateYear, - ); - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 5), - child: GestureDetector( - onTap: () { - memberNotifier.setCompleteMember(member); - QR.to(PhonebookRouter.root + PhonebookRouter.memberDetail); - }, - child: CardLayout( - color: getColorFromTagList( - ref, - member.memberships - .firstWhere( - (element) => - element.associationId == association.id && - element.mandateYear == association.mandateYear, - ) - .rolesTags, - ), - margin: EdgeInsets.zero, - child: LayoutBuilder( - builder: (context, constraints) { - if (constraints.maxWidth > 700) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.1), - spreadRadius: 5, - blurRadius: 10, - offset: const Offset(2, 3), - ), - ], - ), - child: AutoLoaderChild( - group: memberPictures, - notifier: memberPicturesNotifier, - mapKey: member, - loader: (ref) => profilePictureNotifier - .getProfilePicture(member.member.id), - loadingBuilder: (context) => const CircleAvatar( - radius: 40, - child: CircularProgressIndicator(), - ), - dataBuilder: (context, data) => CircleAvatar( - radius: 40, - backgroundImage: data.first.image, - ), - ), - ), - const SizedBox(width: 10), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (member.member.nickname != null) ...[ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SelectableText( - member.member.nickname!, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20, - ), - ), - const SizedBox(width: 10), - SelectableText( - "(${member.member.name} ${member.member.firstname})", - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20, - color: Color.fromARGB(255, 115, 115, 115), - ), - ), - ], - ), - ] else - SelectableText( - "${member.member.name} ${member.member.firstname}", - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20, - ), - ), - const SizedBox(height: 5), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - CardField( - label: PhonebookTextConstants.promotion, - value: member.member.promotion == 0 - ? PhonebookTextConstants.promoNotGiven - : member.member.promotion < 100 - ? "20${member.member.promotion}" - : member.member.promotion.toString(), - ), - CardField( - label: PhonebookTextConstants.email, - value: member.member.email, - ), - if (member.member.phone != null) - CardField( - label: PhonebookTextConstants.phone, - value: member.member.phone!, - ), - ], - ), - ), - ], - ), - ), - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - textAlign: TextAlign.right, - assoMembership == null - ? PhonebookTextConstants.noMemberRole - : assoMembership.apparentName, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20, - ), - ), - ], - ), - ], - ); - } - return Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (member.member.nickname != null) ...[ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SelectableText( - member.member.nickname!, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20, - ), - ), - const SizedBox(width: 10), - SelectableText( - "(${member.member.name} ${member.member.firstname})", - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20, - color: Color.fromARGB(255, 115, 115, 115), - ), - ), - ], - ), - ] else - SelectableText( - "${member.member.name} ${member.member.firstname}", - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20, - ), - ), - const SizedBox(height: 5), - CardField( - label: PhonebookTextConstants.promotion, - value: member.member.promotion == 0 - ? PhonebookTextConstants.promoNotGiven - : member.member.promotion < 100 - ? "20${member.member.promotion}" - : member.member.promotion.toString(), - ), - if (constraints.maxWidth > 500) - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - CardField( - label: PhonebookTextConstants.email, - value: member.member.email, - ), - if (member.member.phone != null) - CardField( - label: PhonebookTextConstants.phone, - value: member.member.phone!, - ), - ], - ), - ) - else - Column( - children: [ - CardField( - label: PhonebookTextConstants.email, - value: member.member.email, - showLabel: false, - ), - if (member.member.phone != null) - CardField( - label: PhonebookTextConstants.phone, - value: member.member.phone!, - showLabel: false, - ), - ], - ), - ], - ), - Column( - children: [ - Text( - textAlign: TextAlign.right, - assoMembership == null - ? PhonebookTextConstants.noMemberRole - : assoMembership.apparentName, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20, - ), - ), - ], - ), - ], - ); - }, - ), - ), - ), - ); - } -} diff --git a/lib/phonebook/ui/pages/main_page/association_card.dart b/lib/phonebook/ui/pages/main_page/association_card.dart index d263cb68ee..1eecc5b65c 100644 --- a/lib/phonebook/ui/pages/main_page/association_card.dart +++ b/lib/phonebook/ui/pages/main_page/association_card.dart @@ -1,42 +1,66 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:qlevar_router/qlevar_router.dart'; import 'package:titan/phonebook/class/association.dart'; -import 'package:titan/tools/ui/layouts/card_layout.dart'; +import 'package:titan/phonebook/class/association_groupement.dart'; +import 'package:titan/phonebook/providers/association_groupement_provider.dart'; +import 'package:titan/phonebook/providers/association_picture_provider.dart'; +import 'package:titan/phonebook/providers/association_provider.dart'; +import 'package:titan/phonebook/providers/associations_picture_map_provider.dart'; +import 'package:titan/phonebook/router.dart'; +import 'package:titan/tools/ui/builders/auto_loader_child.dart'; +import 'package:titan/tools/ui/styleguide/list_item.dart'; class AssociationCard extends HookConsumerWidget { const AssociationCard({ super.key, required this.association, - required this.onClicked, + required this.groupement, }); final Association association; - final VoidCallback onClicked; + final AssociationGroupement groupement; @override Widget build(BuildContext context, WidgetRef ref) { + final associationPicture = ref.watch( + associationPictureMapProvider.select((value) => value[association.id]), + ); + final associationPictureMapNotifier = ref.watch( + associationPictureMapProvider.notifier, + ); + final associationPictureNotifier = ref.watch( + associationPictureProvider.notifier, + ); + final associationGroupementNotifier = ref.watch( + associationGroupementProvider.notifier, + ); + final associationNotifier = ref.watch(associationProvider.notifier); return Padding( - padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 5), - child: GestureDetector( - onTap: onClicked, - child: CardLayout( - margin: EdgeInsets.zero, - child: Row( - children: [ - const SizedBox(width: 10), - Expanded( - child: Text( - association.name, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - Text(association.kind), - ], - ), + padding: const EdgeInsets.symmetric(vertical: 5), + child: ListItem( + title: association.name, + subtitle: groupement.name, + icon: AutoLoaderChild( + group: associationPicture, + notifier: associationPictureMapNotifier, + mapKey: association.id, + loader: (associationId) => + associationPictureNotifier.getAssociationPicture(associationId), + dataBuilder: (context, data) { + return CircleAvatar( + radius: 20, + backgroundColor: Colors.white, + backgroundImage: Image(image: data.first.image).image, + ); + }, ), + onTap: () { + associationNotifier.setAssociation(association); + associationPictureNotifier.getAssociationPicture(association.id); + associationGroupementNotifier.setAssociationGroupement(groupement); + QR.to(PhonebookRouter.root + PhonebookRouter.associationDetail); + }, ), ); } diff --git a/lib/phonebook/ui/pages/main_page/main_page.dart b/lib/phonebook/ui/pages/main_page/main_page.dart index 4be249d2de..892d1af527 100644 --- a/lib/phonebook/ui/pages/main_page/main_page.dart +++ b/lib/phonebook/ui/pages/main_page/main_page.dart @@ -1,22 +1,22 @@ import 'package:flutter/material.dart'; +import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/admin/providers/is_admin_provider.dart'; +import 'package:titan/advert/ui/components/special_action_button.dart'; import 'package:titan/phonebook/providers/association_filtered_list_provider.dart'; -import 'package:titan/phonebook/providers/association_kind_provider.dart'; -import 'package:titan/phonebook/providers/association_kinds_provider.dart'; +import 'package:titan/phonebook/providers/association_groupement_provider.dart'; +import 'package:titan/phonebook/providers/association_groupement_list_provider.dart'; import 'package:titan/phonebook/providers/association_list_provider.dart'; -import 'package:titan/phonebook/providers/association_provider.dart'; -import 'package:titan/phonebook/providers/phonebook_admin_provider.dart'; +import 'package:titan/phonebook/providers/is_phonebook_admin_provider.dart'; import 'package:titan/phonebook/router.dart'; -import 'package:titan/phonebook/tools/constants.dart'; -import 'package:titan/phonebook/ui/components/kinds_bar.dart'; import 'package:titan/phonebook/ui/pages/main_page/association_card.dart'; import 'package:titan/phonebook/ui/phonebook.dart'; -import 'package:titan/phonebook/ui/pages/main_page/research_bar.dart'; +import 'package:titan/phonebook/ui/components/association_research_bar.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/layouts/refresher.dart'; -import 'package:titan/tools/ui/widgets/admin_button.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:tuple/tuple.dart'; class PhonebookMainPage extends HookConsumerWidget { const PhonebookMainPage({super.key}); @@ -25,75 +25,81 @@ class PhonebookMainPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final isPhonebookAdmin = ref.watch(isPhonebookAdminProvider); final isAdmin = ref.watch(isAdminProvider); - final associationNotifier = ref.watch(associationProvider.notifier); final associationListNotifier = ref.watch(associationListProvider.notifier); final associationList = ref.watch(associationListProvider); + final associationGroupementList = ref.watch( + associationGroupementListProvider, + ); final associationFilteredList = ref.watch(associationFilteredListProvider); - final associationKindsNotifier = ref.watch( - associationKindsProvider.notifier, + final associationGroupementListNotifier = ref.watch( + associationGroupementListProvider.notifier, + ); + final associationGroupementNotifier = ref.watch( + associationGroupementProvider.notifier, ); - final kindNotifier = ref.watch(associationKindProvider.notifier); + + final localizeWithContext = AppLocalizations.of(context)!; return PhonebookTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { - await associationKindsNotifier.loadAssociationKinds(); + await associationGroupementListNotifier.loadAssociationGroupement(); await associationListNotifier.loadAssociations(); }, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(30.0), - child: Row( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + const SizedBox(height: 10), + Row( children: [ - const ResearchBar(), - if (isPhonebookAdmin || isAdmin) - Padding( - padding: const EdgeInsets.only(left: 20), - child: AdminButton( - onTap: () { - kindNotifier.setKind(''); - QR.to(PhonebookRouter.root + PhonebookRouter.admin); - }, - ), + Expanded(child: AssociationResearchBar()), + if (isPhonebookAdmin || isAdmin) ...[ + SizedBox(width: 10), + SpecialActionButton( + icon: HeroIcon(HeroIcons.userGroup, color: Colors.white), + name: localizeWithContext.phonebookAdmin, + onTap: () { + associationGroupementNotifier + .resetAssociationGroupement(); + QR.to(PhonebookRouter.root + PhonebookRouter.admin); + }, ), + ], ], ), - ), - const SizedBox(height: 10), - AsyncChild( - value: associationList, - builder: (context, associations) { - return Column( - children: [ - KindsBar(), - const SizedBox(height: 30), - if (associations.isEmpty) - const Center( - child: Text(PhonebookTextConstants.noAssociationFound), - ) - else + const SizedBox(height: 10), + Async2Children( + values: Tuple2(associationList, associationGroupementList), + builder: (context, associations, associationGroupements) { + if (associations.isEmpty) { + return Center( + child: Text( + localizeWithContext.phonebookNoAssociationFound, + ), + ); + } + return Column( + children: [ ...associationFilteredList.map( (association) => !association.deactivated ? AssociationCard( association: association, - onClicked: () { - associationNotifier.setAssociation( - association, - ); - QR.to( - PhonebookRouter.root + - PhonebookRouter.associationDetail, - ); - }, + groupement: associationGroupements.firstWhere( + (groupement) => + groupement.id == association.groupementId, + ), ) : const SizedBox.shrink(), ), - ], - ); - }, - ), - ], + ], + ); + }, + ), + const SizedBox(height: 20), + ], + ), ), ), ); diff --git a/lib/phonebook/ui/pages/main_page/research_bar.dart b/lib/phonebook/ui/pages/main_page/research_bar.dart deleted file mode 100644 index a2bde51a97..0000000000 --- a/lib/phonebook/ui/pages/main_page/research_bar.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/phonebook/providers/research_filter_provider.dart'; -import 'package:titan/phonebook/tools/constants.dart'; -import 'package:titan/tools/constants.dart'; - -class ResearchBar extends HookConsumerWidget { - const ResearchBar({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final focusNode = useFocusNode(); - final editingController = useTextEditingController(); - final filterNotifier = ref.watch(filterProvider.notifier); - - return Expanded( - child: TextField( - onChanged: (value) { - filterNotifier.setFilter(value); - }, - focusNode: focusNode, - controller: editingController, - cursorColor: PhonebookColorConstants.textDark, - decoration: const InputDecoration( - isDense: true, - suffixIcon: Icon( - Icons.search, - color: PhonebookColorConstants.textDark, - size: 30, - ), - label: Text( - PhonebookTextConstants.research, - style: TextStyle(color: PhonebookColorConstants.textDark), - ), - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide(color: ColorConstants.gradient1), - ), - ), - ), - ); - } -} diff --git a/lib/phonebook/ui/pages/member_detail_page/element_field.dart b/lib/phonebook/ui/pages/member_detail_page/element_field.dart deleted file mode 100644 index 80b0471bab..0000000000 --- a/lib/phonebook/ui/pages/member_detail_page/element_field.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:titan/phonebook/tools/constants.dart'; -import 'package:titan/tools/functions.dart'; - -class ElementField extends StatelessWidget { - final String label; - final String value; - const ElementField({super.key, required this.label, required this.value}); - - @override - Widget build(BuildContext context) { - void displayToastWithContext(TypeMsg type, String msg) { - displayToast(context, type, msg); - } - - return Container( - margin: const EdgeInsets.symmetric(vertical: 5.0), - child: Column( - children: [ - Center(child: Text(label, style: const TextStyle(fontSize: 16))), - Center( - child: SelectableText( - value, - maxLines: 1, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - onTap: () { - Clipboard.setData(ClipboardData(text: value)); - displayToastWithContext( - TypeMsg.msg, - PhonebookTextConstants.copied, - ); - }, - ), - ), - ], - ), - ); - } -} diff --git a/lib/phonebook/ui/pages/member_detail_page/member_detail_page.dart b/lib/phonebook/ui/pages/member_detail_page/member_detail_page.dart index 1cc88e0362..71dc5cc3bc 100644 --- a/lib/phonebook/ui/pages/member_detail_page/member_detail_page.dart +++ b/lib/phonebook/ui/pages/member_detail_page/member_detail_page.dart @@ -1,108 +1,138 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/phonebook/providers/association_list_provider.dart'; -import 'package:titan/phonebook/providers/association_provider.dart'; import 'package:titan/phonebook/providers/complete_member_provider.dart'; -import 'package:titan/phonebook/router.dart'; -import 'package:titan/phonebook/tools/constants.dart'; -import 'package:titan/phonebook/ui/pages/member_detail_page/element_field.dart'; +import 'package:titan/phonebook/providers/member_pictures_provider.dart'; +import 'package:titan/phonebook/providers/profile_picture_provider.dart'; import 'package:titan/phonebook/ui/pages/member_detail_page/membership_card.dart'; import 'package:titan/phonebook/ui/phonebook.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; -import 'package:titan/tools/ui/layouts/card_layout.dart'; -import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/ui/builders/auto_loader_child.dart'; class MemberDetailPage extends HookConsumerWidget { const MemberDetailPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final memberProvider = ref.watch(completeMemberProvider); - final associationNotifier = ref.watch(associationProvider.notifier); + final member = ref.watch(completeMemberProvider); final associationList = ref.watch(associationListProvider); + final memberPictures = ref.watch( + memberPicturesProvider.select((value) => value[member]), + ); + final memberPicturesNotifier = ref.watch(memberPicturesProvider.notifier); + final profilePictureNotifier = ref.watch(profilePictureProvider.notifier); + + AppLocalizations localizeWithContext = AppLocalizations.of(context)!; + + final sortedMemberships = [...member.memberships]; + sortedMemberships.sort((a, b) => a.mandateYear.compareTo(b.mandateYear)); + return PhonebookTemplate( - child: SingleChildScrollView( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 30), - child: CardLayout( - margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + Center( child: Column( children: [ - ElementField( - label: PhonebookTextConstants.name, - value: memberProvider.member.name, - ), - ElementField( - label: PhonebookTextConstants.firstname, - value: memberProvider.member.firstname, - ), - if (memberProvider.member.nickname != null) - ElementField( - label: PhonebookTextConstants.nickname, - value: memberProvider.member.nickname!, + AutoLoaderChild( + group: memberPictures, + notifier: memberPicturesNotifier, + mapKey: member, + loader: (ref) => profilePictureNotifier.getProfilePicture( + member.member.id, + ), + loadingBuilder: (context) => const CircleAvatar( + radius: 80, + child: CircularProgressIndicator(), + ), + dataBuilder: (context, data) => CircleAvatar( + radius: 80, + backgroundColor: Colors.white, + backgroundImage: Image(image: data.first.image).image, ), - ElementField( - label: PhonebookTextConstants.email, - value: memberProvider.member.email, ), - if (memberProvider.member.phone != null) - ElementField( - label: PhonebookTextConstants.phone, - value: memberProvider.member.phone!, + if (member.member.nickname != null) ...[ + Text( + member.member.nickname!, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), ), - ElementField( - label: PhonebookTextConstants.promotion, - value: memberProvider.member.promotion == 0 - ? PhonebookTextConstants.promoNotGiven - : memberProvider.member.promotion < 100 - ? "20${memberProvider.member.promotion}" - : memberProvider.member.promotion.toString(), + const SizedBox(height: 5), + Text( + member.getName(), + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ] else + Text( + member.getName(), + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 5), + if (member.member.promotion != 0) + Text( + localizeWithContext.phonebookPromotion( + member.member.promotion < 100 + ? member.member.promotion + 2000 + : member.member.promotion, + ), + style: const TextStyle(fontSize: 16), + ), + const SizedBox(height: 20), + Text( + member.member.email, + style: const TextStyle(fontSize: 16), ), + const SizedBox(height: 5), + if (member.member.phone != null) + Text( + member.member.phone!, + style: const TextStyle(fontSize: 16), + ), ], ), ), - ), - const SizedBox(height: 20), - if (memberProvider.memberships.isNotEmpty) - Text( - memberProvider.memberships.length == 1 - ? PhonebookTextConstants.association - : PhonebookTextConstants.associations, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20, + const SizedBox(height: 20), + if (member.memberships.isNotEmpty) + Text( + member.memberships.length == 1 + ? localizeWithContext.phonebookAssociation + : localizeWithContext.phonebookAssociations, + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + AsyncChild( + value: associationList, + builder: (context, associations) => Column( + children: [ + ...sortedMemberships.map((membership) { + final membershipAssociation = associations.firstWhere( + (association) => + association.id == membership.associationId, + ); + return MembershipCard( + association: membershipAssociation, + membership: membership, + ); + }), + ], ), ), - const SizedBox(height: 20), - AsyncChild( - value: associationList, - builder: (context, associations) => Column( - children: [ - ...memberProvider.memberships.map((membership) { - final associationMembership = associations.firstWhere( - (association) => - association.id == membership.associationId, - ); - return MembershipCard( - association: associationMembership, - onClicked: () { - associationNotifier.setAssociation( - associationMembership, - ); - QR.to( - PhonebookRouter.root + - PhonebookRouter.associationDetail, - ); - }, - membership: membership, - ); - }), - ], - ), - ), - ], + const SizedBox(height: 20), + ], + ), ), ), ); diff --git a/lib/phonebook/ui/pages/member_detail_page/membership_card.dart b/lib/phonebook/ui/pages/member_detail_page/membership_card.dart index 1775c1ccb8..5cc71caa65 100644 --- a/lib/phonebook/ui/pages/member_detail_page/membership_card.dart +++ b/lib/phonebook/ui/pages/member_detail_page/membership_card.dart @@ -1,44 +1,60 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:qlevar_router/qlevar_router.dart'; import 'package:titan/phonebook/class/association.dart'; import 'package:titan/phonebook/class/membership.dart'; -import 'package:titan/tools/ui/layouts/card_layout.dart'; +import 'package:titan/phonebook/providers/association_picture_provider.dart'; +import 'package:titan/phonebook/providers/association_provider.dart'; +import 'package:titan/phonebook/providers/associations_picture_map_provider.dart'; +import 'package:titan/phonebook/router.dart'; +import 'package:titan/tools/ui/builders/auto_loader_child.dart'; +import 'package:titan/tools/ui/styleguide/list_item.dart'; class MembershipCard extends HookConsumerWidget { const MembershipCard({ super.key, required this.association, required this.membership, - required this.onClicked, }); final Association association; - final VoidCallback onClicked; final Membership membership; @override Widget build(BuildContext context, WidgetRef ref) { + final associationNotifier = ref.watch(associationProvider.notifier); + final associationPicture = ref.watch( + associationPictureMapProvider.select((value) => value[association.id]), + ); + final associationPictureMapNotifier = ref.watch( + associationPictureMapProvider.notifier, + ); + final associationPictureNotifier = ref.watch( + associationPictureProvider.notifier, + ); return Padding( - padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 5), - child: GestureDetector( - onTap: onClicked, - child: CardLayout( - margin: EdgeInsets.zero, - child: Row( - children: [ - const SizedBox(width: 10), - Text( - "${association.name} - ${membership.mandateYear}", - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const Spacer(flex: 1), - Text(membership.apparentName), - ], - ), + padding: const EdgeInsets.symmetric(vertical: 5.0), + child: ListItem( + title: "${association.name} - ${membership.apparentName}", + subtitle: membership.mandateYear.toString(), + icon: AutoLoaderChild( + group: associationPicture, + notifier: associationPictureMapNotifier, + mapKey: association.id, + loader: (associationId) => + associationPictureNotifier.getAssociationPicture(associationId), + dataBuilder: (context, data) { + return CircleAvatar( + radius: 20, + backgroundColor: Colors.white, + backgroundImage: Image(image: data.first.image).image, + ); + }, ), + onTap: () { + associationNotifier.setAssociation(association); + QR.to(PhonebookRouter.root + PhonebookRouter.associationDetail); + }, ), ); } diff --git a/lib/phonebook/ui/pages/membership_editor_page/membership_editor_page.dart b/lib/phonebook/ui/pages/membership_editor_page/membership_editor_page.dart index aeef4dbc7f..86eeba0b48 100644 --- a/lib/phonebook/ui/pages/membership_editor_page/membership_editor_page.dart +++ b/lib/phonebook/ui/pages/membership_editor_page/membership_editor_page.dart @@ -1,29 +1,25 @@ -import 'dart:math'; - import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/phonebook/class/membership.dart'; import 'package:titan/phonebook/providers/association_member_list_provider.dart'; import 'package:titan/phonebook/providers/association_provider.dart'; -import 'package:titan/phonebook/providers/member_role_tags_provider.dart'; import 'package:titan/phonebook/providers/membership_provider.dart'; -import 'package:titan/phonebook/providers/phonebook_admin_provider.dart'; +import 'package:titan/phonebook/providers/is_phonebook_admin_provider.dart'; import 'package:titan/phonebook/providers/roles_tags_provider.dart'; -import 'package:titan/phonebook/tools/constants.dart'; +import 'package:titan/phonebook/ui/pages/membership_editor_page/user_search_modal.dart'; import 'package:titan/phonebook/ui/phonebook.dart'; import 'package:titan/tools/constants.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; -import 'package:titan/tools/ui/builders/waiting_button.dart'; -import 'package:titan/tools/ui/layouts/add_edit_button_layout.dart'; -import 'package:titan/tools/ui/widgets/align_left_text.dart'; -import 'package:titan/tools/ui/widgets/styled_search_bar.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; +import 'package:titan/tools/ui/styleguide/list_item.dart'; +import 'package:titan/tools/ui/styleguide/list_item_toggle.dart'; import 'package:titan/tools/ui/widgets/text_entry.dart'; -import 'package:titan/user/providers/user_list_provider.dart'; import 'package:qlevar_router/qlevar_router.dart'; import 'package:titan/phonebook/providers/complete_member_provider.dart'; -import 'package:titan/phonebook/ui/pages/membership_editor_page/search_result.dart'; +import 'package:titan/l10n/app_localizations.dart'; class MembershipEditorPage extends HookConsumerWidget { const MembershipEditorPage({super.key}); @@ -31,9 +27,6 @@ class MembershipEditorPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final rolesTagList = ref.watch(rolesTagsProvider); - final queryController = useTextEditingController(text: ''); - final usersNotifier = ref.watch(userList.notifier); - final rolesTagsNotifier = ref.watch(rolesTagsProvider.notifier); final member = ref.watch(completeMemberProvider); final membership = ref.watch(membershipProvider); final association = ref.watch(associationProvider); @@ -41,8 +34,6 @@ class MembershipEditorPage extends HookConsumerWidget { final associationMemberListNotifier = ref.watch( associationMemberListProvider.notifier, ); - final memberRoleTagsNotifier = ref.watch(memberRoleTagsProvider.notifier); - final memberRoleTags = ref.watch(memberRoleTagsProvider); final apparentNameController = useTextEditingController( text: membership.apparentName, ); @@ -53,221 +44,205 @@ class MembershipEditorPage extends HookConsumerWidget { displayToast(context, type, msg); } + final selectedTags = useState>( + List.from(membership.rolesTags), + ); + + final localizeWithContext = AppLocalizations.of(context)!; + + Future addMember() async { + final memberAssociationMemberships = member.memberships.where( + (membership) => membership.associationId == association.id, + ); + + if (memberAssociationMemberships + .where( + (membership) => membership.mandateYear == association.mandateYear, + ) + .isNotEmpty) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.phonebookExistingMembership, + ); + return; + } + + final membershipAdd = Membership( + id: "", + memberId: member.member.id, + associationId: association.id, + rolesTags: selectedTags.value, + apparentName: apparentNameController.text, + mandateYear: association.mandateYear, + order: associationMembers.maybeWhen( + data: (members) => members.length, + orElse: () => 0, + ), + ); + final value = await associationMemberListNotifier.addMember( + member, + membershipAdd, + ); + if (value) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.phonebookAddedMember, + ); + QR.back(); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.phonebookAddingError, + ); + } + } + + Future updateMember() async { + final membershipEdit = Membership( + id: membership.id, + memberId: membership.memberId, + associationId: membership.associationId, + rolesTags: selectedTags.value, + apparentName: apparentNameController.text, + mandateYear: membership.mandateYear, + order: membership.order, + ); + member.memberships[member.memberships.indexWhere( + (membership) => membership.id == membershipEdit.id, + )] = + membershipEdit; + final value = await associationMemberListNotifier.updateMember( + member, + membershipEdit, + ); + if (value) { + associationMemberListNotifier.loadMembers( + association.id, + association.mandateYear, + ); + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.phonebookUpdatedMember, + ); + QR.back(); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.phonebookUpdatingError, + ); + } + } + return PhonebookTemplate( child: Padding( - padding: const EdgeInsets.all(30.0), + padding: const EdgeInsets.symmetric(horizontal: 20.0), child: SingleChildScrollView( child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - AlignLeftText( - isEdit - ? PhonebookTextConstants.editMembership - : PhonebookTextConstants.addMember, - ), + const SizedBox(height: 20), if (!isEdit) ...[ - StyledSearchBar( - padding: EdgeInsets.zero, - label: PhonebookTextConstants.member, - editingController: queryController, - onChanged: (value) async { - tokenExpireWrapper(ref, () async { - if (value.isNotEmpty) { - await usersNotifier.filterUsers(value); - } else { - usersNotifier.clear(); - } - }); + Text( + localizeWithContext.phonebookAddMember, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorConstants.title, + ), + ), + const SizedBox(height: 20), + ListItem( + title: member.member.id == "" + ? localizeWithContext.phonebookSearchUser + : member.member.getName(), + onTap: () async { + showCustomBottomModal( + context: context, + modal: UserSearchModal(), + ref: ref, + ); }, ), - SearchResult(queryController: queryController), ] else - member.member.nickname == null - ? Text( - "${member.member.firstname} ${member.member.name}", - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ) - : Text( - "${member.member.nickname} (${member.member.firstname} ${member.member.name})", - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), + Text( + localizeWithContext.phonebookModifyMembership( + member.member.nickname ?? member.getName(), + ), + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorConstants.title, + ), + ), const SizedBox(height: 10), - SizedBox( - width: min(MediaQuery.of(context).size.width, 300), - child: Column( - children: [ - ...rolesTagList.keys.map( - (tagKey) => Row( - children: [ - Text(tagKey), - const Spacer(), - Checkbox( - value: rolesTagList[tagKey]!.maybeWhen( - data: (rolesTag) => rolesTag[0], - orElse: () => false, - ), - fillColor: - rolesTagList.keys.first == tagKey && - !isPhonebookAdmin - ? WidgetStateProperty.all(Colors.black) - : WidgetStateProperty.all(Colors.grey), - onChanged: - rolesTagList.keys.first == tagKey && - !isPhonebookAdmin - ? null - : (value) { - rolesTagList[tagKey] = AsyncData([value!]); - memberRoleTagsNotifier - .setRoleTagsWithFilter(rolesTagList); - rolesTagsNotifier.setTData( - tagKey, - AsyncData([value]), - ); - if (value && - apparentNameController.text == "") { - apparentNameController.text = tagKey; - } else if (!value && - apparentNameController.text == tagKey) { - apparentNameController.text = ""; + rolesTagList.maybeWhen( + orElse: () => Text(localizeWithContext.phonebookNoRoleTags), + data: (tagList) { + return Column( + children: tagList + .map( + (tag) => ToggleListItem( + title: tag, + onTap: tagList.first == tag && !isPhonebookAdmin + ? () {} + : () { + final tags = [...selectedTags.value]; + final changeApparentName = + apparentNameController.text == + tags.join(", "); + tags.contains(tag) + ? tags.remove(tag) + : tags.add(tag); + if (changeApparentName) { + apparentNameController.text = tags.join( + ", ", + ); } + selectedTags.value = tags; }, + selected: selectedTags.value.contains(tag), ), - ], - ), - ), - ], - ), + ) + .toList(), + ); + }, ), - const SizedBox(height: 30), + const SizedBox(height: 20), TextEntry( controller: apparentNameController, - label: PhonebookTextConstants.apparentName, + label: localizeWithContext.phonebookApparentName, ), - const SizedBox(height: 50), - WaitingButton( - builder: (child) => AddEditButtonLayout( - colors: const [ - ColorConstants.gradient1, - ColorConstants.gradient2, - ], - child: child, - ), - child: Text( - !isEdit - ? PhonebookTextConstants.add - : PhonebookTextConstants.edit, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Color.fromARGB(255, 255, 255, 255), - ), - ), - onTap: () async { + const SizedBox(height: 30), + Button( + text: isEdit + ? localizeWithContext.phonebookEdit + : localizeWithContext.phonebookAdd, + onPressed: () async { if (member.member.id == "") { displayToastWithContext( TypeMsg.msg, - PhonebookTextConstants.emptyMember, + localizeWithContext.phonebookEmptyMember, ); return; } if (apparentNameController.text == "") { displayToastWithContext( TypeMsg.msg, - PhonebookTextConstants.emptyApparentName, + localizeWithContext.phonebookEmptyApparentName, ); return; } tokenExpireWrapper(ref, () async { if (isEdit) { - final membershipEdit = Membership( - id: membership.id, - memberId: membership.memberId, - associationId: membership.associationId, - rolesTags: memberRoleTags, - apparentName: apparentNameController.text, - mandateYear: membership.mandateYear, - order: membership.order, - ); - member.memberships[member.memberships.indexWhere( - (membership) => membership.id == membershipEdit.id, - )] = - membershipEdit; - final value = await associationMemberListNotifier - .updateMember(member, membershipEdit); - if (value) { - associationMemberListNotifier.loadMembers( - association.id, - association.mandateYear.toString(), - ); - displayToastWithContext( - TypeMsg.msg, - PhonebookTextConstants.updatedMember, - ); - QR.back(); - } else { - displayToastWithContext( - TypeMsg.error, - PhonebookTextConstants.updatingError, - ); - } + await updateMember(); } else { - // Test if the membership already exists with (association_id,member_id,mandate_year) - final memberAssociationMemberships = member.memberships - .where( - (membership) => - membership.associationId == association.id, - ); - - if (memberAssociationMemberships - .where( - (membership) => - membership.mandateYear == - association.mandateYear, - ) - .isNotEmpty) { - displayToastWithContext( - TypeMsg.msg, - PhonebookTextConstants.existingMembership, - ); - return; - } - - final membershipAdd = Membership( - id: "", - memberId: member.member.id, - associationId: association.id, - rolesTags: memberRoleTags, - apparentName: apparentNameController.text, - mandateYear: association.mandateYear, - order: associationMembers.maybeWhen( - data: (members) => members.length, - orElse: () => 0, - ), - ); - final value = await associationMemberListNotifier - .addMember(member, membershipAdd); - if (value) { - displayToastWithContext( - TypeMsg.msg, - PhonebookTextConstants.addedMember, - ); - QR.back(); - } else { - displayToastWithContext( - TypeMsg.error, - PhonebookTextConstants.addingError, - ); - } + await addMember(); } }); }, ), + const SizedBox(height: 20), ], ), ), diff --git a/lib/phonebook/ui/pages/membership_editor_page/search_result.dart b/lib/phonebook/ui/pages/membership_editor_page/search_result.dart index d4b0244735..166599fbd5 100644 --- a/lib/phonebook/ui/pages/membership_editor_page/search_result.dart +++ b/lib/phonebook/ui/pages/membership_editor_page/search_result.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/phonebook/class/member.dart'; import 'package:titan/phonebook/providers/complete_member_provider.dart'; +import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/tools/ui/styleguide/list_item_template.dart'; import 'package:titan/user/providers/user_list_provider.dart'; class SearchResult extends HookConsumerWidget { @@ -14,59 +17,30 @@ class SearchResult extends HookConsumerWidget { final usersNotifier = ref.watch(userList.notifier); final memberNotifier = ref.watch(completeMemberProvider.notifier); - return users.when( - data: (usersData) { + return AsyncChild( + value: users, + builder: (context, usersData) { return Column( children: usersData .map( - (user) => GestureDetector( - behavior: HitTestBehavior.opaque, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.1), - offset: const Offset(0, 1), - blurRadius: 4, - spreadRadius: 2, - ), - ], - ), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 14), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container(width: 20), - Expanded( - child: Text( - user.getName(), - style: const TextStyle(fontSize: 13), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - ), + (user) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: ListItemTemplate( + title: user.getName(), + trailing: const HeroIcon(HeroIcons.plus), + onTap: () { + memberNotifier.setMember(Member.fromUser(user)); + queryController.text = user.getName(); + usersNotifier.clear(); + memberNotifier.loadMemberComplete(); + Navigator.of(context).pop(); + }, ), - onTap: () { - memberNotifier.setMember(Member.fromUser(user)); - queryController.text = user.getName(); - usersNotifier.clear(); - memberNotifier.loadMemberComplete(); - }, ), ) .toList(), ); }, - loading: () => const Center(child: CircularProgressIndicator()), - error: (e, s) => Text(e.toString()), ); } } diff --git a/lib/phonebook/ui/pages/membership_editor_page/user_search_modal.dart b/lib/phonebook/ui/pages/membership_editor_page/user_search_modal.dart new file mode 100644 index 0000000000..2148f64414 --- /dev/null +++ b/lib/phonebook/ui/pages/membership_editor_page/user_search_modal.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/phonebook/ui/pages/membership_editor_page/search_result.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/searchbar.dart'; +import 'package:titan/user/providers/user_list_provider.dart'; + +class UserSearchModal extends HookConsumerWidget { + const UserSearchModal({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final usersNotifier = ref.watch(userList.notifier); + final textController = useTextEditingController(); + + final localizeWithContext = AppLocalizations.of(context)!; + + return BottomModalTemplate( + title: localizeWithContext.phonebookSearchUser, + type: BottomModalType.main, + child: SingleChildScrollView( + child: Column( + children: [ + CustomSearchBar( + autofocus: true, + onSearch: (value) => tokenExpireWrapper(ref, () async { + if (value.isNotEmpty) { + await usersNotifier.filterUsers(value); + textController.text = value; + } else { + usersNotifier.clear(); + textController.clear(); + } + }), + ), + SizedBox(height: 10), + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 280), + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: SearchResult(queryController: textController), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/phonebook/ui/phonebook.dart b/lib/phonebook/ui/phonebook.dart index 4921564af0..628d3c1a43 100644 --- a/lib/phonebook/ui/phonebook.dart +++ b/lib/phonebook/ui/phonebook.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/phonebook/providers/association_kind_provider.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/phonebook/providers/association_groupement_provider.dart'; import 'package:titan/phonebook/router.dart'; -import 'package:titan/phonebook/tools/constants.dart'; +import 'package:titan/tools/constants.dart'; import 'package:titan/tools/ui/widgets/top_bar.dart'; -import 'package:qlevar_router/qlevar_router.dart'; class PhonebookTemplate extends HookConsumerWidget { final Widget child; @@ -12,33 +12,40 @@ class PhonebookTemplate extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final kindNotifier = ref.watch(associationKindProvider.notifier); - return SafeArea( - child: Column( - children: [ - TopBar( - title: PhonebookTextConstants.phonebook, - root: PhonebookRouter.root, - onBack: () { - if (QR.currentPath != - PhonebookRouter.root + - PhonebookRouter.admin + - PhonebookRouter.editAssociation + - PhonebookRouter.addEditMember) { - kindNotifier.setKind(''); - } - if (QR.currentPath == - PhonebookRouter.root + - PhonebookRouter.admin + - PhonebookRouter.editAssociation) { - QR.to( - PhonebookRouter.root + PhonebookRouter.admin, - ); // Used on back after adding an association - } - }, - ), - Expanded(child: child), - ], + final associationGroupementNotifer = ref.watch( + associationGroupementProvider.notifier, + ); + + final pathGroupementClearing = [ + PhonebookRouter.root + PhonebookRouter.admin, + PhonebookRouter.root + PhonebookRouter.associationDetail, + PhonebookRouter.root + + PhonebookRouter.admin + + PhonebookRouter.addEditAssociation, + PhonebookRouter.root + + PhonebookRouter.admin + + PhonebookRouter.editAssociationGroups, + PhonebookRouter.root + + PhonebookRouter.admin + + PhonebookRouter.editAssociationMembers, + ]; + + return Container( + color: ColorConstants.background, + child: SafeArea( + child: Column( + children: [ + TopBar( + root: PhonebookRouter.root, + onBack: () { + if (pathGroupementClearing.contains(QR.currentPath)) { + associationGroupementNotifer.resetAssociationGroupement(); + } + }, + ), + Expanded(child: child), + ], + ), ), ); } diff --git a/lib/purchases/router.dart b/lib/purchases/router.dart index fa3de16eb5..ac7588c5c6 100644 --- a/lib/purchases/router.dart +++ b/lib/purchases/router.dart @@ -1,7 +1,7 @@ -import 'package:either_dart/either.dart'; -import 'package:heroicons/heroicons.dart'; +import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/drawer/class/module.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/class/module.dart'; import 'package:titan/purchases/providers/purchases_admin_provider.dart'; import 'package:titan/purchases/ui/pages/history_page/history_page.dart'; import 'package:titan/purchases/ui/pages/main_page/main_page.dart'; @@ -22,10 +22,10 @@ class PurchasesRouter { static const String ticket = '/ticket'; static const String purchase = '/purchase'; static final Module module = Module( - name: "Achats", - icon: const Left(HeroIcons.shoppingBag), + getName: (context) => AppLocalizations.of(context)!.modulePurchases, + getDescription: (context) => + AppLocalizations.of(context)!.modulePurchasesDescription, root: PurchasesRouter.root, - selected: false, ); PurchasesRouter(this.ref); @@ -34,6 +34,10 @@ class PurchasesRouter { path: PurchasesRouter.root, builder: () => const PurchasesMainPage(), middleware: [AuthenticatedMiddleware(ref)], + pageType: QCustomPage( + transitionsBuilder: (_, animation, _, child) => + FadeTransition(opacity: animation, child: child), + ), children: [ QRoute( path: scan, diff --git a/lib/purchases/tools/constants.dart b/lib/purchases/tools/constants.dart index db9af33c35..b4a56f429e 100644 --- a/lib/purchases/tools/constants.dart +++ b/lib/purchases/tools/constants.dart @@ -1,51 +1,5 @@ import 'package:flutter/material.dart'; -class PurchasesTextConstants { - static const String purchases = "Achats"; - - static const String research = "Rechercher"; - - static const String noPurchasesFound = "Aucun achat trouvé"; - - static const String noTickets = "Aucun ticket"; - - static const String ticketsError = "Erreur lors du chargement des tickets"; - static const String purchasesError = "Erreur lors du chargement des achats"; - - static const String noPurchases = "Aucun achat"; - - static const String times = "fois"; - - static const String alreadyUsed = "Déjà utilisé"; - - static const String notPaid = "Non validé"; - - static const String pleaseSelectProduct = "Veuillez sélectionner un produit"; - - static const String products = "Produits"; - - static const String cancel = "Annuler"; - - static const String validate = "Valider"; - - static const String leftScan = "Scans restants"; - static const String tag = "Tag"; - - static const String history = "Historique"; - - static const String pleaseSelectSeller = "Veuillez sélectionner un vendeur"; - - static const String noTagGiven = "Attention, aucun tag n'a été entré"; - - static const String tickets = "Tickets"; - - static const String noScannableProducts = "Aucun produit scannable"; - - static const String loading = "En attente de scan"; - - static const String scan = "Scanner"; -} - class PurchasesColorConstants { static const Color textDark = Color(0xFF1D1D1D); } diff --git a/lib/purchases/ui/pages/history_page/history_page.dart b/lib/purchases/ui/pages/history_page/history_page.dart index e27a5cd7bc..a75b81d0ff 100644 --- a/lib/purchases/ui/pages/history_page/history_page.dart +++ b/lib/purchases/ui/pages/history_page/history_page.dart @@ -4,7 +4,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/purchases/providers/purchase_list_provider.dart'; import 'package:titan/purchases/providers/purchase_provider.dart'; import 'package:titan/purchases/router.dart'; -import 'package:titan/purchases/tools/constants.dart'; import 'package:titan/purchases/ui/pages/history_page/purchase_card.dart'; import 'package:titan/purchases/ui/purchases.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; @@ -12,6 +11,7 @@ import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; import 'package:titan/tools/ui/layouts/item_chip.dart'; import 'package:titan/tools/ui/layouts/refresher.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class HistoryPage extends HookConsumerWidget { const HistoryPage({super.key}); @@ -26,6 +26,7 @@ class HistoryPage extends HookConsumerWidget { return PurchasesTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await purchasesListNotifier.loadPurchases(); }, @@ -55,7 +56,11 @@ class HistoryPage extends HookConsumerWidget { if (children.isEmpty) { children.add( - const Center(child: Text(PurchasesTextConstants.noPurchases)), + Center( + child: Text( + AppLocalizations.of(context)!.purchasesNoPurchases, + ), + ), ); } return Column( @@ -86,8 +91,9 @@ class HistoryPage extends HookConsumerWidget { ], ); }, - errorBuilder: (error, stack) => - const Center(child: Text(PurchasesTextConstants.purchasesError)), + errorBuilder: (error, stack) => Center( + child: Text(AppLocalizations.of(context)!.purchasesPurchasesError), + ), ), ), ); diff --git a/lib/purchases/ui/pages/history_page/purchase_card.dart b/lib/purchases/ui/pages/history_page/purchase_card.dart index 8cf07a0161..976eb85260 100644 --- a/lib/purchases/ui/pages/history_page/purchase_card.dart +++ b/lib/purchases/ui/pages/history_page/purchase_card.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/purchases/class/purchase.dart'; -import 'package:titan/purchases/tools/constants.dart'; import 'package:titan/tools/ui/layouts/card_layout.dart'; +import 'package:titan/l10n/app_localizations.dart'; class PurchaseCard extends HookConsumerWidget { const PurchaseCard({ @@ -42,9 +42,9 @@ class PurchaseCard extends HookConsumerWidget { fontWeight: FontWeight.bold, ), ) - : const Text( - PurchasesTextConstants.notPaid, - style: TextStyle( + : Text( + AppLocalizations.of(context)!.purchasesNotPaid, + style: const TextStyle( color: Colors.red, fontSize: 16, fontWeight: FontWeight.bold, diff --git a/lib/purchases/ui/pages/history_page/research_bar.dart b/lib/purchases/ui/pages/history_page/research_bar.dart index 2e8dfe2ecf..4207355a01 100644 --- a/lib/purchases/ui/pages/history_page/research_bar.dart +++ b/lib/purchases/ui/pages/history_page/research_bar.dart @@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/phonebook/providers/research_filter_provider.dart'; import 'package:titan/purchases/tools/constants.dart'; import 'package:titan/tools/constants.dart'; +import 'package:titan/l10n/app_localizations.dart'; class ResearchBar extends HookConsumerWidget { const ResearchBar({super.key}); @@ -22,18 +23,18 @@ class ResearchBar extends HookConsumerWidget { focusNode: focusNode, controller: editingController, cursorColor: PurchasesColorConstants.textDark, - decoration: const InputDecoration( + decoration: InputDecoration( isDense: true, - suffixIcon: Icon( + suffixIcon: const Icon( Icons.search, color: PurchasesColorConstants.textDark, size: 30, ), label: Text( - PurchasesTextConstants.research, - style: TextStyle(color: PurchasesColorConstants.textDark), + AppLocalizations.of(context)!.purchasesResearch, + style: const TextStyle(color: PurchasesColorConstants.textDark), ), - focusedBorder: UnderlineInputBorder( + focusedBorder: const UnderlineInputBorder( borderSide: BorderSide(color: ColorConstants.gradient1), ), ), diff --git a/lib/purchases/ui/pages/main_page/custom_button.dart b/lib/purchases/ui/pages/main_page/custom_button.dart index 518fd379eb..b53f37d723 100644 --- a/lib/purchases/ui/pages/main_page/custom_button.dart +++ b/lib/purchases/ui/pages/main_page/custom_button.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; -import 'package:titan/tools/constants.dart'; class CustomButton extends StatelessWidget { final VoidCallback onTap; @@ -14,7 +13,7 @@ class CustomButton extends StatelessWidget { required this.onTap, this.textColor = Colors.white, this.color = Colors.black, - this.text = TextConstants.admin, + this.text = 'Admin', this.colors, required this.icon, }); diff --git a/lib/purchases/ui/pages/main_page/main_page.dart b/lib/purchases/ui/pages/main_page/main_page.dart index 1ccc2047e5..0eac73f0d2 100644 --- a/lib/purchases/ui/pages/main_page/main_page.dart +++ b/lib/purchases/ui/pages/main_page/main_page.dart @@ -5,7 +5,6 @@ import 'package:titan/purchases/providers/purchases_admin_provider.dart'; import 'package:titan/purchases/providers/ticket_list_provider.dart'; import 'package:titan/purchases/providers/ticket_provider.dart'; import 'package:titan/purchases/router.dart'; -import 'package:titan/purchases/tools/constants.dart'; import 'package:titan/purchases/ui/pages/main_page/custom_button.dart'; import 'package:titan/purchases/ui/pages/main_page/ticket_card.dart'; import 'package:titan/purchases/ui/purchases.dart'; @@ -14,6 +13,7 @@ import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/layouts/refresher.dart'; import 'package:titan/tools/ui/widgets/align_left_text.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class PurchasesMainPage extends HookConsumerWidget { const PurchasesMainPage({super.key}); @@ -27,6 +27,7 @@ class PurchasesMainPage extends HookConsumerWidget { return PurchasesTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await ticketListNotifier.loadTickets(); }, @@ -39,7 +40,7 @@ class PurchasesMainPage extends HookConsumerWidget { children: [ CustomButton( icon: HeroIcons.clock, - text: PurchasesTextConstants.history, + text: AppLocalizations.of(context)!.purchasesHistory, onTap: () { QR.to(PurchasesRouter.root + PurchasesRouter.history); }, @@ -47,7 +48,7 @@ class PurchasesMainPage extends HookConsumerWidget { if (isAdmin) CustomButton( icon: HeroIcons.viewfinderCircle, - text: PurchasesTextConstants.scan, + text: AppLocalizations.of(context)!.purchasesScan, onTap: () { QR.to(PurchasesRouter.root + PurchasesRouter.scan); }, @@ -55,9 +56,9 @@ class PurchasesMainPage extends HookConsumerWidget { ], ), ), - const AlignLeftText( - padding: EdgeInsets.only(left: 30), - PurchasesTextConstants.tickets, + AlignLeftText( + padding: const EdgeInsets.only(left: 30), + AppLocalizations.of(context)!.purchasesTickets, fontSize: 20, ), AsyncChild( @@ -66,8 +67,10 @@ class PurchasesMainPage extends HookConsumerWidget { return Column( children: [ if (tickets.isEmpty) - const Center( - child: Text(PurchasesTextConstants.noTickets), + Center( + child: Text( + AppLocalizations.of(context)!.purchasesNoTickets, + ), ) else ...ticketList.maybeWhen( @@ -86,7 +89,9 @@ class PurchasesMainPage extends HookConsumerWidget { ), ), orElse: () => [ - const Text(PurchasesTextConstants.ticketsError), + Text( + AppLocalizations.of(context)!.purchasesTicketsError, + ), ], ), ], diff --git a/lib/purchases/ui/pages/main_page/ticket_card.dart b/lib/purchases/ui/pages/main_page/ticket_card.dart index d0933c2821..31590a686b 100644 --- a/lib/purchases/ui/pages/main_page/ticket_card.dart +++ b/lib/purchases/ui/pages/main_page/ticket_card.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:titan/purchases/class/ticket.dart'; -import 'package:titan/tools/functions.dart'; import 'package:titan/tools/ui/layouts/card_layout.dart'; class TicketCard extends HookConsumerWidget { @@ -12,6 +12,7 @@ class TicketCard extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final locale = Localizations.localeOf(context); return Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), child: GestureDetector( @@ -32,7 +33,7 @@ class TicketCard extends HookConsumerWidget { ), ), Text( - "${ticket.scanLeft} scan${ticket.scanLeft > 1 ? 's' : ""} restant${ticket.scanLeft > 1 ? 's' : ""} - Valide jusqu'au ${processDate(ticket.expirationDate)}", + "${ticket.scanLeft} scan${ticket.scanLeft > 1 ? 's' : ""} restant${ticket.scanLeft > 1 ? 's' : ""} - Valide jusqu'au ${DateFormat.yMd(locale).format(ticket.expirationDate)}", style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, diff --git a/lib/purchases/ui/pages/purchase_page/purchase_page.dart b/lib/purchases/ui/pages/purchase_page/purchase_page.dart index 472bf6d20b..55cf8ced1d 100644 --- a/lib/purchases/ui/pages/purchase_page/purchase_page.dart +++ b/lib/purchases/ui/pages/purchase_page/purchase_page.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/purchases/providers/purchase_provider.dart'; -import 'package:titan/purchases/tools/constants.dart'; import 'package:titan/purchases/ui/purchases.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/layouts/refresher.dart'; +import 'package:titan/l10n/app_localizations.dart'; class PurchasePage extends HookConsumerWidget { const PurchasePage({super.key}); @@ -15,6 +15,7 @@ class PurchasePage extends HookConsumerWidget { return PurchasesTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async {}, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 30), @@ -31,9 +32,12 @@ class PurchasePage extends HookConsumerWidget { ...!data.validated ? [ const SizedBox(height: 10), - const Text( - PurchasesTextConstants.notPaid, - style: TextStyle(fontSize: 20, color: Colors.red), + Text( + AppLocalizations.of(context)!.purchasesNotPaid, + style: const TextStyle( + fontSize: 20, + color: Colors.red, + ), ), ] : [], diff --git a/lib/purchases/ui/pages/scan_page/scan_dialog.dart b/lib/purchases/ui/pages/scan_page/scan_dialog.dart index 02066616da..88d64a70cb 100644 --- a/lib/purchases/ui/pages/scan_page/scan_dialog.dart +++ b/lib/purchases/ui/pages/scan_page/scan_dialog.dart @@ -14,6 +14,7 @@ import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/tools/ui/layouts/add_edit_button_layout.dart'; import 'package:titan/tools/ui/layouts/card_button.dart'; import 'package:titan/tools/ui/layouts/card_layout.dart'; +import 'package:titan/l10n/app_localizations.dart'; class ScanDialog extends HookConsumerWidget { final String sellerId; @@ -57,15 +58,15 @@ class ScanDialog extends HookConsumerWidget { tagNotifier.setTag(value); }, cursorColor: PurchasesColorConstants.textDark, - decoration: const InputDecoration( + decoration: InputDecoration( isDense: true, label: Text( - PurchasesTextConstants.tag, - style: TextStyle( + AppLocalizations.of(context)!.purchasesTag, + style: const TextStyle( color: PurchasesColorConstants.textDark, ), ), - focusedBorder: UnderlineInputBorder( + focusedBorder: const UnderlineInputBorder( borderSide: BorderSide(color: ColorConstants.gradient1), ), ), @@ -188,7 +189,7 @@ class ScanDialog extends HookConsumerWidget { ), const SizedBox(height: 10), Text( - "${data.scanLeft.toString()} / ${ticket.maxUse} ${PurchasesTextConstants.leftScan}", + "${data.scanLeft.toString()} / ${ticket.maxUse} ${AppLocalizations.of(context)!.purchasesLeftScan}", style: const TextStyle( fontSize: 16, color: Colors.black, @@ -264,9 +265,9 @@ class ScanDialog extends HookConsumerWidget { ], ); }, - loading: () => const Text( - PurchasesTextConstants.loading, - style: TextStyle(fontSize: 20, color: Colors.black), + loading: () => Text( + AppLocalizations.of(context)!.purchasesLoading, + style: const TextStyle(fontSize: 20, color: Colors.black), ), error: (error, stack) => Column( children: [ diff --git a/lib/purchases/ui/pages/scan_page/scan_page.dart b/lib/purchases/ui/pages/scan_page/scan_page.dart index c1dd743f69..375af4fa0a 100644 --- a/lib/purchases/ui/pages/scan_page/scan_page.dart +++ b/lib/purchases/ui/pages/scan_page/scan_page.dart @@ -6,7 +6,6 @@ import 'package:titan/purchases/providers/generated_ticket_provider.dart'; import 'package:titan/purchases/providers/scanner_provider.dart'; import 'package:titan/purchases/providers/seller_list_provider.dart'; import 'package:titan/purchases/providers/seller_provider.dart'; -import 'package:titan/purchases/tools/constants.dart'; import 'package:titan/purchases/ui/pages/scan_page/ticket_card.dart'; import 'package:titan/purchases/ui/pages/scan_page/scan_dialog.dart'; import 'package:titan/purchases/ui/purchases.dart'; @@ -15,6 +14,7 @@ import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; import 'package:titan/tools/ui/layouts/item_chip.dart'; import 'package:titan/tools/ui/layouts/refresher.dart'; +import 'package:titan/l10n/app_localizations.dart'; class ScanPage extends HookConsumerWidget { const ScanPage({super.key}); @@ -32,6 +32,7 @@ class ScanPage extends HookConsumerWidget { return PurchasesTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await sellersNotifier.loadSellers(); if (seller != Seller.empty()) { @@ -74,7 +75,11 @@ class ScanPage extends HookConsumerWidget { ), const SizedBox(height: 10), seller.id == "" - ? const Text(PurchasesTextConstants.pleaseSelectSeller) + ? Text( + AppLocalizations.of( + context, + )!.purchasesPleaseSelectSeller, + ) : AsyncChild( value: products, builder: (context, products) { @@ -83,8 +88,10 @@ class ScanPage extends HookConsumerWidget { product.ticketGenerators.isNotEmpty, ); if (scannableProducts.isEmpty) { - return const Text( - PurchasesTextConstants.noScannableProducts, + return Text( + AppLocalizations.of( + context, + )!.purchasesNoScannableProducts, ); } return Column( @@ -127,7 +134,7 @@ class ScanPage extends HookConsumerWidget { // decoration: const InputDecoration( // isDense: true, // label: Text( - // PurchasesTextConstants.tag, + // AppLocalizations.of(context)!.purchasesTag, // style: TextStyle( // color: PurchasesColorConstants.textDark, // ), @@ -139,12 +146,12 @@ class ScanPage extends HookConsumerWidget { // ), // tag == "" // ? const Text( - // PurchasesTextConstants.noTagGiven, + // AppLocalizations.of(context)!.purchasesNoTagGiven, // style: TextStyle(color: Colors.red), // ) // : const SizedBox(), // product.id == "" - // ? const Text(PurchasesTextConstants.pleaseSelectProduct) + // ? const Text(AppLocalizations.of(context)!.purchasesPleaseSelectProduct) // : Padding( // padding: const EdgeInsets.all(30), // child: SizedBox( diff --git a/lib/purchases/ui/pages/scan_page/ticket_card.dart b/lib/purchases/ui/pages/scan_page/ticket_card.dart index e68d0206ee..8132ed98b2 100644 --- a/lib/purchases/ui/pages/scan_page/ticket_card.dart +++ b/lib/purchases/ui/pages/scan_page/ticket_card.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:titan/purchases/class/product.dart'; import 'package:titan/purchases/class/ticket_generator.dart'; import 'package:titan/purchases/providers/product_id_provider.dart'; @@ -8,7 +9,6 @@ import 'package:titan/purchases/providers/seller_provider.dart'; import 'package:titan/purchases/providers/tag_list_provider.dart'; import 'package:titan/purchases/providers/ticket_id_provider.dart'; import 'package:titan/purchases/router.dart'; -import 'package:titan/tools/functions.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/tools/ui/layouts/card_layout.dart'; import 'package:qlevar_router/qlevar_router.dart'; @@ -27,6 +27,7 @@ class TicketCard extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final locale = Localizations.localeOf(context); final seller = ref.watch(sellerProvider); final ticketIdNotifier = ref.read(ticketIdProvider.notifier); final productIdNotifier = ref.read(productIdProvider.notifier); @@ -52,7 +53,7 @@ class TicketCard extends HookConsumerWidget { ), ), Text( - "${ticket.maxUse} scan${ticket.maxUse > 1 ? 's' : ""} maximun - Valide jusqu'au ${processDate(ticket.expiration)}", + "${ticket.maxUse} scan${ticket.maxUse > 1 ? 's' : ""} maximun - Valide jusqu'au ${DateFormat.yMd(locale).format(ticket.expiration)}", style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, diff --git a/lib/purchases/ui/pages/ticket_page/ticket_page.dart b/lib/purchases/ui/pages/ticket_page/ticket_page.dart index 80d9eb9115..1e0b2d4e64 100644 --- a/lib/purchases/ui/pages/ticket_page/ticket_page.dart +++ b/lib/purchases/ui/pages/ticket_page/ticket_page.dart @@ -3,12 +3,12 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/purchases/providers/ticket_provider.dart'; -import 'package:titan/purchases/tools/constants.dart'; import 'package:titan/purchases/ui/purchases.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/layouts/refresher.dart'; import 'package:titan/tools/ui/widgets/loader.dart'; import 'package:qr_flutter/qr_flutter.dart'; +import 'package:titan/l10n/app_localizations.dart'; class TicketPage extends HookConsumerWidget { const TicketPage({super.key}); @@ -20,6 +20,7 @@ class TicketPage extends HookConsumerWidget { return PurchasesTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await ticketNotifier.loadTicketSecret(); }, @@ -63,7 +64,7 @@ class TicketPage extends HookConsumerWidget { ), const SizedBox(height: 10), Text( - "${PurchasesTextConstants.leftScan}: ${data.scanLeft.toString()}", + "${AppLocalizations.of(context)!.purchasesLeftScan}: ${data.scanLeft.toString()}", style: const TextStyle(fontSize: 20, color: Colors.black), ), const SizedBox(height: 10), diff --git a/lib/purchases/ui/pages/user_list_page/user_list_page.dart b/lib/purchases/ui/pages/user_list_page/user_list_page.dart index e8402d99ed..a105c05405 100644 --- a/lib/purchases/ui/pages/user_list_page/user_list_page.dart +++ b/lib/purchases/ui/pages/user_list_page/user_list_page.dart @@ -30,6 +30,7 @@ class UserListPage extends HookConsumerWidget { final selectedTag = useState(null); return PurchasesTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { productId.maybeWhen( orElse: () {}, diff --git a/lib/purchases/ui/purchases.dart b/lib/purchases/ui/purchases.dart index 8e10186783..705ca3e84d 100644 --- a/lib/purchases/ui/purchases.dart +++ b/lib/purchases/ui/purchases.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/purchases/router.dart'; -import 'package:titan/purchases/tools/constants.dart'; import 'package:titan/tools/ui/widgets/top_bar.dart'; +import 'package:titan/tools/constants.dart'; class PurchasesTemplate extends HookConsumerWidget { final Widget child; @@ -10,15 +10,18 @@ class PurchasesTemplate extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return SafeArea( - child: Column( - children: [ - const TopBar( - title: PurchasesTextConstants.purchases, - root: PurchasesRouter.root, + return Scaffold( + body: Container( + decoration: const BoxDecoration(color: ColorConstants.background), + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const TopBar(root: PurchasesRouter.root), + Expanded(child: child), + ], ), - Expanded(child: child), - ], + ), ), ); } diff --git a/lib/raffle/router.dart b/lib/raffle/router.dart index 2f0797698b..600bfcb165 100644 --- a/lib/raffle/router.dart +++ b/lib/raffle/router.dart @@ -1,7 +1,7 @@ -import 'package:either_dart/either.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:titan/drawer/class/module.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/class/module.dart'; import 'package:titan/raffle/providers/is_raffle_admin.dart'; import 'package:titan/raffle/ui/pages/admin_module_page/admin_module_page.dart' deferred as admin_module_page; @@ -29,10 +29,10 @@ class RaffleRouter { static const String addEditPackTicket = '/add_edit_pack_ticket'; static const String creation = '/creation'; static final Module module = Module( - name: "Tombola", - icon: const Left(HeroIcons.gift), + getName: (context) => AppLocalizations.of(context)!.moduleRaffle, + getDescription: (context) => + AppLocalizations.of(context)!.moduleRaffleDescription, root: RaffleRouter.root, - selected: false, ); RaffleRouter(this.ref); QRoute route() => QRoute( @@ -43,6 +43,10 @@ class RaffleRouter { AuthenticatedMiddleware(ref), DeferredLoadingMiddleware(main_page.loadLibrary), ], + pageType: QCustomPage( + transitionsBuilder: (_, animation, _, child) => + FadeTransition(opacity: animation, child: child), + ), children: [ QRoute( path: admin, diff --git a/lib/raffle/tools/constants.dart b/lib/raffle/tools/constants.dart index 35948fd84e..64eea5565f 100644 --- a/lib/raffle/tools/constants.dart +++ b/lib/raffle/tools/constants.dart @@ -15,109 +15,3 @@ class RaffleColorConstants extends ColorConstants { static const Color redGradient3 = Color.fromARGB(255, 255, 34, 34); static const Color ticketBack = Color(0xff000031); } - -class RaffleTextConstants { - //general - static const String raffle = "Tombola"; - static const String prize = "Lot"; - static const String prizes = "Lots"; - //Home page - static const String actualRaffles = "Tombola en cours"; - static const String pastRaffles = "Tombola passés"; - static const String yourTickets = "Tous vos tickets"; - static const String createMenu = "Menu de Création"; - static const String nextRaffles = "Prochaines tombolas"; - static const String noTicket = "Vous n'avez pas de ticket"; - static const String seeRaffleDetail = "Voir lots/tickets"; - - //Tombola page - static const String actualPrize = "Lots actuels"; - static const String majorPrize = "Lot Majeurs"; - static const String takeTickets = "Prendre vos tickets"; - static const String noTicketBuyable = - "Vous ne pouvez pas achetez de billets pour l'instant"; - static const String noCurrentPrize = "Il n'y a aucun lots actuellement"; - //Create Home - static const String modifTombola = - "Vous pouvez modifiez vos tombolas ou en créer de nouvelles, toute décision doit ensuite être prise par les admins"; - static const String createYourRaffle = "Votre menu de création de tombolas"; - - //Add Edit Page - static const String possiblePrice = "Prix possible"; - static const String information = "Information et Statistiques"; - - //Admin page - static const String accounts = "Comptes"; - static const String add = "Ajouter"; - static const String updatedAmount = "Montant mis à jour"; - static const String updatingError = "Erreur lors de la mise à jour"; - static const String deletedPrize = "Lot supprimé"; - static const String deletingError = "Erreur lors de la suppression"; - static const String quantity = "Quantité"; - static const String close = "Fermer"; - static const String open = "Ouvrir"; - - // Add Edit type ticket - static const String addTypeTicketSimple = "Ajouter"; - static const String addingError = "Erreur lors de l'ajout"; - static const String editTypeTicketSimple = "Modifier"; - static const String fillField = "Le champ ne peut pas être vide"; - static const String waiting = "Chargement"; - static const String editingError = "Erreur lors de la modification"; - static const String addedTicket = "Ticket ajouté"; - static const String editedTicket = "Ticket modifié"; - static const String alreadyExistTicket = "Le ticket existe déjà"; - static const String numberExpected = "Un entier est attendu"; - static const String deletedTicket = "Ticket supprimé"; - static const String addPrize = "Ajouter"; - static const String editPrize = "Modifier"; - static const String openRaffle = "Ouvrir la tombola"; - static const String closeRaffle = "Fermer la tombola"; - static const String openRaffleDescription = - "Vous allez ouvrir la tombola, les utilisateurs pourront acheter des tickets. Vous ne pourrez plus modifier la tombola. Êtes-vous sûr de vouloir continuer ?"; - static const String closeRaffleDescription = - "Vous allez fermer la tombola, les utilisateurs ne pourront plus acheter de tickets. Êtes-vous sûr de vouloir continuer ?"; - static const String noCurrentRaffle = "Il n'y a aucune tombola en cours"; - static const String boughtTicket = "Ticket acheté"; - static const String drawingError = "Erreur lors du tirage"; - static const String invalidPrice = "Le prix doit être supérieur à 0"; - static const String mustBePositive = - "Le nombre doit être strictement positif"; - static const String draw = "Tirer"; - static const String drawn = "Tiré"; - static const String error = "Erreur"; - static const String gathered = "Récolté"; - static const String tickets = "Tickets"; - static const String ticket = "ticket"; - static const String winner = "Gagnant"; - static const String noPrize = "Aucun lot"; - static const String deletePrize = "Supprimer le lot"; - static const String deletePrizeDescription = - "Vous allez supprimer le lot, êtes-vous sûr de vouloir continuer ?"; - static const String drawing = "Tirage"; - static const String drawingDescription = "Tirer le gagnant du lot ?"; - static const String deleteTicket = "Supprimer le ticket"; - static const String deleteTicketDescription = - "Vous allez supprimer le ticket, êtes-vous sûr de vouloir continuer ?"; - static const String winningTickets = "Tickets gagnants"; - static const String noWinningTicketYet = - "Les tickets gagnants seront affichés ici"; - static const String name = "Nom"; - static const String description = "Description"; - static const String buyThisTicket = "Acheter ce ticket"; - static const String lockedRaffle = "Tombola verrouillée"; - static const String unavailableRaffle = "Tombola indisponible"; - static const String notEnoughMoney = "Vous n'avez pas assez d'argent"; - static const String winnable = "gagnable"; - static const String noDescription = "Aucune description"; - static const String amount = "Solde"; - static const String loading = "Chargement"; - static const String ticketNumber = "Nombre de ticket"; - static const String price = "Prix"; - - static const String editRaffle = "Modifier la tombola"; - - static const String edit = "Modifier"; - - static const String addPackTicket = "Ajouter un pack de ticket"; -} diff --git a/lib/raffle/ui/pages/admin_module_page/account_handler.dart b/lib/raffle/ui/pages/admin_module_page/account_handler.dart index 8a3a0b9a64..27c9a4c32f 100644 --- a/lib/raffle/ui/pages/admin_module_page/account_handler.dart +++ b/lib/raffle/ui/pages/admin_module_page/account_handler.dart @@ -10,6 +10,7 @@ import 'package:titan/raffle/ui/pages/admin_module_page/adding_user_container.da import 'package:titan/raffle/ui/pages/admin_module_page/cash_container.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/user/providers/user_list_provider.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AccountHandler extends HookConsumerWidget { const AccountHandler({super.key}); @@ -55,22 +56,22 @@ class AccountHandler extends HookConsumerWidget { focusNode: focusNode, controller: editingController, cursorColor: RaffleColorConstants.textDark, - decoration: const InputDecoration( - labelText: RaffleTextConstants.accounts, - labelStyle: TextStyle( + decoration: InputDecoration( + labelText: AppLocalizations.of(context)!.raffleAccounts, + labelStyle: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: RaffleColorConstants.textDark, ), - suffixIcon: Icon( + suffixIcon: const Icon( Icons.search, color: RaffleColorConstants.textDark, size: 30, ), - enabledBorder: UnderlineInputBorder( + enabledBorder: const UnderlineInputBorder( borderSide: BorderSide(color: Colors.transparent), ), - focusedBorder: UnderlineInputBorder( + focusedBorder: const UnderlineInputBorder( borderSide: BorderSide(color: RaffleColorConstants.textDark), ), ), diff --git a/lib/raffle/ui/pages/admin_module_page/admin_module_page.dart b/lib/raffle/ui/pages/admin_module_page/admin_module_page.dart index ffe4e75bcb..acad16d56d 100644 --- a/lib/raffle/ui/pages/admin_module_page/admin_module_page.dart +++ b/lib/raffle/ui/pages/admin_module_page/admin_module_page.dart @@ -14,6 +14,7 @@ class AdminModulePage extends HookConsumerWidget { final tombolaLogosNotifier = ref.watch(tombolaLogosProvider.notifier); return RaffleTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { tombolaLogosNotifier.resetTData(); }, diff --git a/lib/raffle/ui/pages/admin_module_page/tombola_handler.dart b/lib/raffle/ui/pages/admin_module_page/tombola_handler.dart index 88d19687df..4af0d41b0e 100644 --- a/lib/raffle/ui/pages/admin_module_page/tombola_handler.dart +++ b/lib/raffle/ui/pages/admin_module_page/tombola_handler.dart @@ -8,6 +8,7 @@ import 'package:titan/raffle/providers/raffle_list_provider.dart'; import 'package:titan/raffle/tools/constants.dart'; import 'package:titan/raffle/ui/pages/admin_module_page/confirm_creation.dart'; import 'package:titan/raffle/ui/pages/admin_module_page/tombola_card.dart'; +import 'package:titan/l10n/app_localizations.dart'; class TombolaHandler extends HookConsumerWidget { const TombolaHandler({super.key}); @@ -77,9 +78,9 @@ class TombolaHandler extends HookConsumerWidget { Container( padding: const EdgeInsets.symmetric(horizontal: 30), alignment: Alignment.centerLeft, - child: const Text( - RaffleTextConstants.raffle, - style: TextStyle( + child: Text( + AppLocalizations.of(context)!.raffleRaffle, + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: RaffleColorConstants.textDark, diff --git a/lib/raffle/ui/pages/creation_edit_page/creation_edit_page.dart b/lib/raffle/ui/pages/creation_edit_page/creation_edit_page.dart index 1e823017ed..2bd4de4fd1 100644 --- a/lib/raffle/ui/pages/creation_edit_page/creation_edit_page.dart +++ b/lib/raffle/ui/pages/creation_edit_page/creation_edit_page.dart @@ -30,6 +30,7 @@ import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; import 'package:titan/tools/ui/widgets/image_picker_on_tap.dart'; import 'package:titan/tools/ui/widgets/text_entry.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class CreationPage extends HookConsumerWidget { const CreationPage({super.key}); @@ -72,6 +73,7 @@ class CreationPage extends HookConsumerWidget { return RaffleTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await cashNotifier.loadCashList(); await packTicketListNotifier.loadPackTicketList(); @@ -80,13 +82,13 @@ class CreationPage extends HookConsumerWidget { child: Column( children: [ const SizedBox(height: 30), - const Padding( - padding: EdgeInsets.only(top: 10, left: 30, right: 30), + Padding( + padding: const EdgeInsets.only(top: 10, left: 30, right: 30), child: Align( alignment: Alignment.centerLeft, child: Text( - RaffleTextConstants.editRaffle, - style: TextStyle( + AppLocalizations.of(context)!.raffleEditRaffle, + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Color.fromARGB(255, 149, 149, 149), @@ -186,7 +188,7 @@ class CreationPage extends HookConsumerWidget { child: Form( key: formKey, child: TextEntry( - label: RaffleTextConstants.name, + label: AppLocalizations.of(context)!.raffleName, enabled: raffle.raffleStatusType == RaffleStatusType.creation, controller: name, @@ -223,7 +225,7 @@ class CreationPage extends HookConsumerWidget { ); } }, - child: const Text(RaffleTextConstants.edit), + child: Text(AppLocalizations.of(context)!.raffleEdit), ), ), const SizedBox(height: 40), @@ -235,9 +237,9 @@ class CreationPage extends HookConsumerWidget { Container( padding: const EdgeInsets.symmetric(horizontal: 30), alignment: Alignment.centerLeft, - child: const Text( - RaffleTextConstants.editRaffle, - style: TextStyle( + child: Text( + AppLocalizations.of(context)!.raffleEditRaffle, + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: RaffleColorConstants.textDark, @@ -260,13 +262,21 @@ class CreationPage extends HookConsumerWidget { title: raffle.raffleStatusType == RaffleStatusType.creation - ? RaffleTextConstants.openRaffle - : RaffleTextConstants.closeRaffle, + ? AppLocalizations.of( + context, + )!.raffleOpenRaffle + : AppLocalizations.of( + context, + )!.raffleCloseRaffle, descriptions: raffle.raffleStatusType == RaffleStatusType.creation - ? RaffleTextConstants.openRaffleDescription - : RaffleTextConstants.closeRaffleDescription, + ? AppLocalizations.of( + context, + )!.raffleOpenRaffleDescription + : AppLocalizations.of( + context, + )!.raffleCloseRaffleDescription, onYes: () async { switch (raffle.raffleStatusType) { case RaffleStatusType.creation: @@ -306,8 +316,8 @@ class CreationPage extends HookConsumerWidget { child: BlueBtn( child: Text( raffle.raffleStatusType == RaffleStatusType.open - ? RaffleTextConstants.close - : RaffleTextConstants.open, + ? AppLocalizations.of(context)!.raffleClose + : AppLocalizations.of(context)!.raffleOpen, ), ), ), diff --git a/lib/raffle/ui/pages/creation_edit_page/prize_card.dart b/lib/raffle/ui/pages/creation_edit_page/prize_card.dart index e5ce99d300..5b4ab81152 100644 --- a/lib/raffle/ui/pages/creation_edit_page/prize_card.dart +++ b/lib/raffle/ui/pages/creation_edit_page/prize_card.dart @@ -5,6 +5,7 @@ import 'package:titan/raffle/class/prize.dart'; import 'package:titan/raffle/class/raffle_status_type.dart'; import 'package:titan/raffle/tools/constants.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; +import 'package:titan/l10n/app_localizations.dart'; class PrizeCard extends StatelessWidget { final Prize lot; @@ -63,7 +64,7 @@ class PrizeCard extends StatelessWidget { const SizedBox(height: 4), AutoSizeText( lot.quantity > 0 - ? "${RaffleTextConstants.quantity} : ${lot.quantity}" + ? "${AppLocalizations.of(context)!.raffleQuantity} : ${lot.quantity}" : "", maxLines: 2, overflow: TextOverflow.ellipsis, diff --git a/lib/raffle/ui/pages/creation_edit_page/prize_handler.dart b/lib/raffle/ui/pages/creation_edit_page/prize_handler.dart index 7d19a50a5c..e53b2667ca 100644 --- a/lib/raffle/ui/pages/creation_edit_page/prize_handler.dart +++ b/lib/raffle/ui/pages/creation_edit_page/prize_handler.dart @@ -15,6 +15,7 @@ import 'package:titan/tools/functions.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class PrizeHandler extends HookConsumerWidget { const PrizeHandler({super.key}); @@ -169,18 +170,25 @@ class PrizeHandler extends HookConsumerWidget { "Voulez-vous vraiment supprimer ce lot?", onYes: () { tokenExpireWrapper(ref, () async { + final deletePriceMsg = + AppLocalizations.of( + context, + )!.raffleDeletePrize; + final deletingErrorMsg = + AppLocalizations.of( + context, + )!.raffleDeletingError; final value = await prizesNotifier .deletePrize(e); if (value) { displayToastWithContext( TypeMsg.msg, - RaffleTextConstants.deletePrize, + deletePriceMsg, ); } else { displayToastWithContext( TypeMsg.error, - RaffleTextConstants - .deletingError, + deletingErrorMsg, ); } }); @@ -223,8 +231,9 @@ class PrizeHandler extends HookConsumerWidget { error: (e, s) { displayToastWithContext( TypeMsg.error, - RaffleTextConstants - .drawingError, + AppLocalizations.of( + context, + )!.raffleDrawingError, ); }, loading: () {}, diff --git a/lib/raffle/ui/pages/creation_edit_page/ticket_handler.dart b/lib/raffle/ui/pages/creation_edit_page/ticket_handler.dart index a901cb168b..81026195d0 100644 --- a/lib/raffle/ui/pages/creation_edit_page/ticket_handler.dart +++ b/lib/raffle/ui/pages/creation_edit_page/ticket_handler.dart @@ -13,6 +13,7 @@ import 'package:titan/tools/functions.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class TicketHandler extends HookConsumerWidget { const TicketHandler({super.key}); @@ -119,17 +120,25 @@ class TicketHandler extends HookConsumerWidget { "Voulez-vous vraiment supprimer ce ticket?", onYes: () { tokenExpireWrapper(ref, () async { + final deletedTicketMsg = + AppLocalizations.of( + context, + )!.raffleDeletedTicket; + final deletingErrorMsg = + AppLocalizations.of( + context, + )!.raffleDeletingError; final value = await packTicketsNotifier .deletePackTicket(e); if (value) { displayToastWithContext( TypeMsg.msg, - RaffleTextConstants.deletedTicket, + deletedTicketMsg, ); } else { displayToastWithContext( TypeMsg.error, - RaffleTextConstants.deletingError, + deletingErrorMsg, ); } }); diff --git a/lib/raffle/ui/pages/creation_edit_page/user_cash_ui.dart b/lib/raffle/ui/pages/creation_edit_page/user_cash_ui.dart index f56b0634f2..bd43f707a6 100644 --- a/lib/raffle/ui/pages/creation_edit_page/user_cash_ui.dart +++ b/lib/raffle/ui/pages/creation_edit_page/user_cash_ui.dart @@ -11,6 +11,7 @@ import 'package:titan/raffle/tools/constants.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; +import 'package:titan/l10n/app_localizations.dart'; class UserCashUi extends HookConsumerWidget { final Cash cash; @@ -195,7 +196,9 @@ class UserCashUi extends HookConsumerWidget { controller: amount, keyboardType: TextInputType.number, validator: (value) => value!.isEmpty - ? RaffleTextConstants.add + ? AppLocalizations.of( + context, + )!.raffleAdd : null, cursorColor: RaffleColorConstants.textDark, decoration: const InputDecoration( @@ -230,6 +233,14 @@ class UserCashUi extends HookConsumerWidget { } if (key.currentState!.validate()) { await tokenExpireWrapper(ref, () async { + final raffleUpdatedAmountMsg = + AppLocalizations.of( + context, + )!.raffleUpdatedAmount; + final raffleUpdatingErrorMsg = + AppLocalizations.of( + context, + )!.raffleUpdatingError; await ref .read(cashProvider.notifier) .updateCash( @@ -242,14 +253,12 @@ class UserCashUi extends HookConsumerWidget { toggle(); displayVoteWithContext( TypeMsg.msg, - RaffleTextConstants - .updatedAmount, + raffleUpdatedAmountMsg, ); } else { displayVoteWithContext( TypeMsg.error, - RaffleTextConstants - .updatingError, + raffleUpdatingErrorMsg, ); } }); diff --git a/lib/raffle/ui/pages/main_page/main_page.dart b/lib/raffle/ui/pages/main_page/main_page.dart index c030122b10..da1d6f768a 100644 --- a/lib/raffle/ui/pages/main_page/main_page.dart +++ b/lib/raffle/ui/pages/main_page/main_page.dart @@ -9,7 +9,6 @@ import 'package:titan/raffle/providers/raffle_list_provider.dart'; import 'package:titan/raffle/providers/tombola_logos_provider.dart'; import 'package:titan/raffle/providers/user_tickets_provider.dart'; import 'package:titan/raffle/router.dart'; -import 'package:titan/raffle/tools/constants.dart'; import 'package:titan/raffle/ui/components/section_title.dart'; import 'package:titan/raffle/ui/pages/main_page/raffle_card.dart'; import 'package:titan/raffle/ui/pages/main_page/ticket_card.dart'; @@ -19,6 +18,7 @@ import 'package:titan/tools/ui/widgets/admin_button.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/layouts/refresher.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class RaffleMainPage extends HookConsumerWidget { const RaffleMainPage({super.key}); @@ -41,6 +41,7 @@ class RaffleMainPage extends HookConsumerWidget { return RaffleTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await userTicketListNotifier.loadTicketList(); await raffleListNotifier.loadRaffleList(); @@ -54,7 +55,9 @@ class RaffleMainPage extends HookConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const SectionTitle(text: RaffleTextConstants.tickets), + SectionTitle( + text: AppLocalizations.of(context)!.raffleTickets, + ), if (isAdmin) AdminButton( onTap: () { @@ -104,7 +107,11 @@ class RaffleMainPage extends HookConsumerWidget { } } return ticketSum.isEmpty - ? const Center(child: Text(RaffleTextConstants.noTicket)) + ? Center( + child: Text( + AppLocalizations.of(context)!.raffleNoTicket, + ), + ) : HorizontalListView.builder( height: 135, items: ticketSum.keys.toList(), @@ -153,8 +160,10 @@ class RaffleMainPage extends HookConsumerWidget { top: 20, left: 5, ), - child: const SectionTitle( - text: RaffleTextConstants.actualRaffles, + child: SectionTitle( + text: AppLocalizations.of( + context, + )!.raffleActualRaffles, ), ), ...onGoingRaffles.map((e) => RaffleWidget(raffle: e)), @@ -165,8 +174,10 @@ class RaffleMainPage extends HookConsumerWidget { top: 20, left: 5, ), - child: const SectionTitle( - text: RaffleTextConstants.nextRaffles, + child: SectionTitle( + text: AppLocalizations.of( + context, + )!.raffleNextRaffles, ), ), ...incomingRaffles.map((e) => RaffleWidget(raffle: e)), @@ -177,20 +188,24 @@ class RaffleMainPage extends HookConsumerWidget { top: 20, left: 5, ), - child: const SectionTitle( - text: RaffleTextConstants.pastRaffles, + child: SectionTitle( + text: AppLocalizations.of( + context, + )!.rafflePastRaffles, ), ), ...pastRaffles.map((e) => RaffleWidget(raffle: e)), if (onGoingRaffles.isEmpty && incomingRaffles.isEmpty && pastRaffles.isEmpty) - const SizedBox( + SizedBox( height: 100, child: Center( child: Text( - RaffleTextConstants.noCurrentRaffle, - style: TextStyle(fontSize: 20), + AppLocalizations.of( + context, + )!.raffleNoCurrentRaffle, + style: const TextStyle(fontSize: 20), ), ), ), diff --git a/lib/raffle/ui/pages/main_page/raffle_card.dart b/lib/raffle/ui/pages/main_page/raffle_card.dart index 19ff9584d0..2e358e908b 100644 --- a/lib/raffle/ui/pages/main_page/raffle_card.dart +++ b/lib/raffle/ui/pages/main_page/raffle_card.dart @@ -14,6 +14,7 @@ import 'package:titan/raffle/tools/constants.dart'; import 'package:titan/raffle/ui/raffle.dart'; import 'package:titan/tools/ui/builders/auto_loader_child.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class RaffleWidget extends HookConsumerWidget { final Raffle raffle; @@ -107,10 +108,10 @@ class RaffleWidget extends HookConsumerWidget { fontSize: 30, ), ), - const Text( - RaffleTextConstants.tickets, + Text( + AppLocalizations.of(context)!.raffleTickets, textAlign: TextAlign.center, - style: TextStyle( + style: const TextStyle( color: RaffleColorConstants.textDark, fontSize: 20, ), @@ -127,10 +128,10 @@ class RaffleWidget extends HookConsumerWidget { fontSize: 30, ), ), - const Text( - RaffleTextConstants.gathered, + Text( + AppLocalizations.of(context)!.raffleGathered, textAlign: TextAlign.center, - style: TextStyle( + style: const TextStyle( color: RaffleColorConstants.textDark, fontSize: 20, ), diff --git a/lib/raffle/ui/pages/main_page/ticket_card.dart b/lib/raffle/ui/pages/main_page/ticket_card.dart index 5f4c09e53f..e9df498b86 100644 --- a/lib/raffle/ui/pages/main_page/ticket_card.dart +++ b/lib/raffle/ui/pages/main_page/ticket_card.dart @@ -7,9 +7,9 @@ import 'package:titan/raffle/class/tickets.dart'; import 'package:titan/raffle/providers/raffle_list_provider.dart'; import 'package:titan/raffle/providers/tombola_logo_provider.dart'; import 'package:titan/raffle/providers/tombola_logos_provider.dart'; -import 'package:titan/raffle/tools/constants.dart'; import 'package:titan/raffle/ui/pages/main_page/ticket_card_background.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; +import 'package:titan/l10n/app_localizations.dart'; class TicketWidget extends HookConsumerWidget { final List ticket; @@ -108,7 +108,7 @@ class TicketWidget extends HookConsumerWidget { Expanded( child: AutoSizeText( isWinningTicket - ? "${RaffleTextConstants.winner} !" + ? "${AppLocalizations.of(context)!.raffleWinner} !" : "${price.toStringAsFixed(2)} €", maxLines: 1, textAlign: TextAlign.right, @@ -125,7 +125,7 @@ class TicketWidget extends HookConsumerWidget { AutoSizeText( isWinningTicket ? ticket[0].prize!.name - : "${ticket.length} ${RaffleTextConstants.ticket}${ticket.length > 1 ? "s" : ""}", + : "${ticket.length} ${AppLocalizations.of(context)!.raffleTicket}${ticket.length > 1 ? "s" : ""}", maxLines: 2, style: TextStyle( color: isWinningTicket diff --git a/lib/raffle/ui/pages/pack_ticket_page/add_edit_pack_ticket_page.dart b/lib/raffle/ui/pages/pack_ticket_page/add_edit_pack_ticket_page.dart index 9aaeb90bb3..a5a39491eb 100644 --- a/lib/raffle/ui/pages/pack_ticket_page/add_edit_pack_ticket_page.dart +++ b/lib/raffle/ui/pages/pack_ticket_page/add_edit_pack_ticket_page.dart @@ -12,6 +12,7 @@ import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; import 'package:titan/tools/ui/widgets/text_entry.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AddEditPackTicketPage extends HookConsumerWidget { const AddEditPackTicketPage({super.key}); @@ -46,11 +47,11 @@ class AddEditPackTicketPage extends HookConsumerWidget { child: Column( children: [ const SizedBox(height: 20), - const Align( + Align( alignment: Alignment.centerLeft, child: Text( - RaffleTextConstants.addTypeTicketSimple, - style: TextStyle( + AppLocalizations.of(context)!.raffleAddTypeTicketSimple, + style: const TextStyle( fontWeight: FontWeight.w800, fontSize: 25, color: RaffleColorConstants.gradient1, @@ -75,7 +76,9 @@ class AddEditPackTicketPage extends HookConsumerWidget { isInt: true, validator: (value) { if (int.parse(value) < 1) { - return RaffleTextConstants.mustBePositive; + return AppLocalizations.of( + context, + )!.raffleMustBePositive; } return null; }, @@ -123,6 +126,20 @@ class AddEditPackTicketPage extends HookConsumerWidget { final typeTicketNotifier = ref.watch( packTicketListProvider.notifier, ); + final editedTicketMsg = isEdit + ? AppLocalizations.of( + context, + )!.raffleEditedTicket + : AppLocalizations.of( + context, + )!.raffleAddedTicket; + final addingErrorMsg = isEdit + ? AppLocalizations.of( + context, + )!.raffleEditingError + : AppLocalizations.of( + context, + )!.raffleAddingError; final value = isEdit ? await typeTicketNotifier.updatePackTicket( newPackTicket, @@ -132,50 +149,40 @@ class AddEditPackTicketPage extends HookConsumerWidget { ); if (value) { QR.back(); - if (isEdit) { - displayToastWithContext( - TypeMsg.msg, - RaffleTextConstants.editedTicket, - ); - } else { - displayToastWithContext( - TypeMsg.msg, - RaffleTextConstants.addedTicket, - ); - } + displayToastWithContext( + TypeMsg.msg, + editedTicketMsg, + ); } else { - if (isEdit) { - displayToastWithContext( - TypeMsg.error, - RaffleTextConstants.editingError, - ); - } else { - displayToastWithContext( - TypeMsg.error, - RaffleTextConstants.alreadyExistTicket, - ); - } + displayToastWithContext( + TypeMsg.error, + addingErrorMsg, + ); } }); } else { displayToast( context, TypeMsg.error, - RaffleTextConstants.invalidPrice, + AppLocalizations.of(context)!.raffleInvalidPrice, ); } } else { displayToast( context, TypeMsg.error, - RaffleTextConstants.addingError, + AppLocalizations.of(context)!.raffleAddingError, ); } }, child: Text( isEdit - ? RaffleTextConstants.editTypeTicketSimple - : RaffleTextConstants.addTypeTicketSimple, + ? AppLocalizations.of( + context, + )!.raffleEditTypeTicketSimple + : AppLocalizations.of( + context, + )!.raffleAddTypeTicketSimple, ), ), const SizedBox(height: 40), diff --git a/lib/raffle/ui/pages/prize_page/add_edit_prize_page.dart b/lib/raffle/ui/pages/prize_page/add_edit_prize_page.dart index 1181d8b8b3..8e3c98da9c 100644 --- a/lib/raffle/ui/pages/prize_page/add_edit_prize_page.dart +++ b/lib/raffle/ui/pages/prize_page/add_edit_prize_page.dart @@ -15,6 +15,7 @@ import 'package:titan/tools/ui/widgets/align_left_text.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; import 'package:titan/tools/ui/widgets/text_entry.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AddEditPrizePage extends HookConsumerWidget { const AddEditPrizePage({super.key}); @@ -49,32 +50,38 @@ class AddEditPrizePage extends HookConsumerWidget { child: Column( children: [ const SizedBox(height: 20), - const AlignLeftText( - RaffleTextConstants.addPrize, + AlignLeftText( + AppLocalizations.of(context)!.raffleAddPrize, fontSize: 25, color: RaffleColorConstants.gradient1, ), const SizedBox(height: 35), - const SectionTitle(text: RaffleTextConstants.quantity), + SectionTitle( + text: AppLocalizations.of(context)!.raffleQuantity, + ), const SizedBox(height: 5), TextEntry( - label: RaffleTextConstants.quantity, + label: AppLocalizations.of(context)!.raffleQuantity, isInt: true, controller: quantity, keyboardType: TextInputType.number, ), const SizedBox(height: 50), - const SectionTitle(text: RaffleTextConstants.name), + SectionTitle( + text: AppLocalizations.of(context)!.raffleName, + ), const SizedBox(height: 5), TextEntry( - label: RaffleTextConstants.name, + label: AppLocalizations.of(context)!.raffleName, controller: name, ), const SizedBox(height: 50), - const SectionTitle(text: RaffleTextConstants.description), + SectionTitle( + text: AppLocalizations.of(context)!.raffleDescription, + ), const SizedBox(height: 5), TextEntry( - label: RaffleTextConstants.description, + label: AppLocalizations.of(context)!.raffleDescription, canBeEmpty: true, controller: description, ), @@ -93,48 +100,48 @@ class AddEditPrizePage extends HookConsumerWidget { final prizeNotifier = ref.watch( prizeListProvider.notifier, ); + final editedTicket = isEdit + ? AppLocalizations.of( + context, + )!.raffleEditedTicket + : AppLocalizations.of( + context, + )!.raffleAddedTicket; + final addingError = isEdit + ? AppLocalizations.of( + context, + )!.raffleEditingError + : AppLocalizations.of( + context, + )!.raffleAddingError; final value = isEdit ? await prizeNotifier.updatePrize(newPrize) : await prizeNotifier.addPrize(newPrize); if (value) { QR.back(); - if (isEdit) { - displayToastWithContext( - TypeMsg.msg, - RaffleTextConstants.editedTicket, - ); - } else { - displayToastWithContext( - TypeMsg.msg, - RaffleTextConstants.addedTicket, - ); - } + displayToastWithContext( + TypeMsg.msg, + editedTicket, + ); } else { - if (isEdit) { - displayToastWithContext( - TypeMsg.error, - RaffleTextConstants.editingError, - ); - } else { - displayToastWithContext( - TypeMsg.error, - RaffleTextConstants.addingError, - ); - } + displayToastWithContext( + TypeMsg.error, + addingError, + ); } }); } else { displayToast( context, TypeMsg.error, - RaffleTextConstants.addingError, + AppLocalizations.of(context)!.raffleAddingError, ); } }, child: Text( isEdit - ? RaffleTextConstants.editPrize - : RaffleTextConstants.addPrize, + ? AppLocalizations.of(context)!.raffleEditPrize + : AppLocalizations.of(context)!.raffleAddPrize, style: const TextStyle( fontSize: 20, fontWeight: FontWeight.w700, diff --git a/lib/raffle/ui/pages/raffle_page/buy_type_ticket_card.dart b/lib/raffle/ui/pages/raffle_page/buy_type_ticket_card.dart index 949456f640..6e869b9967 100644 --- a/lib/raffle/ui/pages/raffle_page/buy_type_ticket_card.dart +++ b/lib/raffle/ui/pages/raffle_page/buy_type_ticket_card.dart @@ -9,6 +9,7 @@ import 'package:titan/raffle/providers/tombola_logos_provider.dart'; import 'package:titan/raffle/tools/constants.dart'; import 'package:titan/raffle/ui/pages/raffle_page/confirm_payment.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; +import 'package:titan/l10n/app_localizations.dart'; class BuyPackTicket extends HookConsumerWidget { final PackTicket packTicket; @@ -140,7 +141,7 @@ class BuyPackTicket extends HookConsumerWidget { ), const SizedBox(height: 10), Text( - "${packTicket.packSize} ${RaffleTextConstants.ticket}${packTicket.packSize > 1 ? "s" : ""}", + "${packTicket.packSize} ${AppLocalizations.of(context)!.raffleTicket}${packTicket.packSize > 1 ? "s" : ""}", style: TextStyle( color: Colors.white.withValues(alpha: 0.8), fontSize: 18, @@ -169,10 +170,10 @@ class BuyPackTicket extends HookConsumerWidget { fit: BoxFit.fitWidth, child: Text( raffle.raffleStatusType == RaffleStatusType.open - ? RaffleTextConstants.buyThisTicket + ? AppLocalizations.of(context)!.raffleBuyThisTicket : raffle.raffleStatusType == RaffleStatusType.lock - ? RaffleTextConstants.lockedRaffle - : RaffleTextConstants.unavailableRaffle, + ? AppLocalizations.of(context)!.raffleLockedRaffle + : AppLocalizations.of(context)!.raffleUnavailableRaffle, style: TextStyle( color: raffle.raffleStatusType != RaffleStatusType.open ? Colors.white diff --git a/lib/raffle/ui/pages/raffle_page/confirm_payment.dart b/lib/raffle/ui/pages/raffle_page/confirm_payment.dart index f549b5332d..5ba37d85de 100644 --- a/lib/raffle/ui/pages/raffle_page/confirm_payment.dart +++ b/lib/raffle/ui/pages/raffle_page/confirm_payment.dart @@ -15,6 +15,7 @@ import 'package:titan/raffle/tools/constants.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; +import 'package:titan/l10n/app_localizations.dart'; class ConfirmPaymentDialog extends HookConsumerWidget { final PackTicket packTicket; @@ -247,6 +248,12 @@ class ConfirmPaymentDialog extends HookConsumerWidget { ); } else { await tokenExpireWrapper(ref, () async { + final boughtTicketMsg = AppLocalizations.of( + context, + )!.raffleBoughtTicket; + final boughtTicketErrorMsg = AppLocalizations.of( + context, + )!.raffleAddingError; final value = await userTicketListNotifier .buyTicket(packTicket); if (value) { @@ -255,12 +262,12 @@ class ConfirmPaymentDialog extends HookConsumerWidget { ); displayToastWithContext( TypeMsg.msg, - RaffleTextConstants.boughtTicket, + boughtTicketMsg, ); } else { displayToastWithContext( TypeMsg.error, - RaffleTextConstants.addingError, + boughtTicketErrorMsg, ); } navigationPop(); diff --git a/lib/raffle/ui/pages/raffle_page/prize_dialog.dart b/lib/raffle/ui/pages/raffle_page/prize_dialog.dart index ea9ca8a3df..51c2d7a224 100644 --- a/lib/raffle/ui/pages/raffle_page/prize_dialog.dart +++ b/lib/raffle/ui/pages/raffle_page/prize_dialog.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/raffle/class/prize.dart'; import 'package:titan/raffle/tools/constants.dart'; +import 'package:titan/l10n/app_localizations.dart'; class PrizeDialog extends HookConsumerWidget { final Prize prize; @@ -65,7 +66,7 @@ class PrizeDialog extends HookConsumerWidget { child: FittedBox( fit: BoxFit.fitWidth, child: Text( - "${prize.quantity} ${RaffleTextConstants.prize}${prize.quantity > 1 ? "s" : ""} ${RaffleTextConstants.winnable}${prize.quantity > 1 ? "s" : ""}", + "${prize.quantity} ${AppLocalizations.of(context)!.rafflePrize}${prize.quantity > 1 ? "s" : ""} ${AppLocalizations.of(context)!.raffleWinnable}${prize.quantity > 1 ? "s" : ""}", style: const TextStyle( color: RaffleColorConstants.writtenWhite, fontSize: 50, @@ -77,7 +78,7 @@ class PrizeDialog extends HookConsumerWidget { const Spacer(), AutoSizeText( prize.description == null || prize.description!.isEmpty - ? RaffleTextConstants.noDescription + ? AppLocalizations.of(context)!.raffleNoDescription : prize.description!, maxLines: 4, textAlign: TextAlign.justify, diff --git a/lib/raffle/ui/pages/raffle_page/raffle_page.dart b/lib/raffle/ui/pages/raffle_page/raffle_page.dart index 2f7c304cc2..71c7dde85b 100644 --- a/lib/raffle/ui/pages/raffle_page/raffle_page.dart +++ b/lib/raffle/ui/pages/raffle_page/raffle_page.dart @@ -13,6 +13,7 @@ import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; import 'package:titan/tools/ui/widgets/align_left_text.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/layouts/refresher.dart'; +import 'package:titan/l10n/app_localizations.dart'; class RaffleInfoPage extends HookConsumerWidget { const RaffleInfoPage({super.key}); @@ -30,6 +31,7 @@ class RaffleInfoPage extends HookConsumerWidget { return RaffleTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { userId.whenData( (value) async => await balanceNotifier.loadCashByUser(value), @@ -67,7 +69,7 @@ class RaffleInfoPage extends HookConsumerWidget { child: AsyncChild( value: balance, builder: (context, s) => Text( - "${RaffleTextConstants.amount} : ${s.balance.toStringAsFixed(2)}€", + "${AppLocalizations.of(context)!.raffleAmount} : ${s.balance.toStringAsFixed(2)}€", style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, @@ -84,7 +86,9 @@ class RaffleInfoPage extends HookConsumerWidget { height: 190, alignment: Alignment.centerLeft, padding: const EdgeInsets.symmetric(horizontal: 30), - child: const Text(RaffleTextConstants.noTicketBuyable), + child: Text( + AppLocalizations.of(context)!.raffleNoTicketBuyable, + ), ) : HorizontalListView.builder( height: 160, @@ -116,19 +120,19 @@ class RaffleInfoPage extends HookConsumerWidget { vertical: 10, horizontal: 30, ), - child: const Column( + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - RaffleTextConstants.actualPrize, - style: TextStyle( + AppLocalizations.of(context)!.raffleActualPrize, + style: const TextStyle( fontSize: 25, color: RaffleColorConstants.gradient2, fontWeight: FontWeight.bold, ), ), - SizedBox(height: 10), - Text(RaffleTextConstants.noPrize), + const SizedBox(height: 10), + Text(AppLocalizations.of(context)!.raffleNoPrize), ], ), ) @@ -136,8 +140,10 @@ class RaffleInfoPage extends HookConsumerWidget { children: [ AlignLeftText( prizes.isEmpty - ? RaffleTextConstants.noPrize - : RaffleTextConstants.actualPrize, + ? AppLocalizations.of(context)!.raffleNoPrize + : AppLocalizations.of( + context, + )!.raffleActualPrize, padding: const EdgeInsets.symmetric( vertical: 10, horizontal: 30, @@ -168,9 +174,9 @@ class RaffleInfoPage extends HookConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - RaffleTextConstants.actualPrize, - style: TextStyle( + Text( + AppLocalizations.of(context)!.raffleActualPrize, + style: const TextStyle( fontSize: 25, color: RaffleColorConstants.gradient2, fontWeight: FontWeight.bold, @@ -190,9 +196,9 @@ class RaffleInfoPage extends HookConsumerWidget { left: 30, right: 30, ), - child: const Text( - RaffleTextConstants.description, - style: TextStyle( + child: Text( + AppLocalizations.of(context)!.raffleDescription, + style: const TextStyle( fontSize: 25, fontWeight: FontWeight.bold, color: RaffleColorConstants.gradient2, diff --git a/lib/raffle/ui/raffle.dart b/lib/raffle/ui/raffle.dart index 99a1eebe6f..b6be955160 100644 --- a/lib/raffle/ui/raffle.dart +++ b/lib/raffle/ui/raffle.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/raffle/router.dart'; -import 'package:titan/raffle/tools/constants.dart'; import 'package:titan/tools/ui/widgets/top_bar.dart'; +import 'package:titan/tools/constants.dart'; class RaffleTemplate extends HookConsumerWidget { final Widget child; @@ -10,15 +10,18 @@ class RaffleTemplate extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return SafeArea( - child: Column( - children: [ - const TopBar( - title: RaffleTextConstants.raffle, - root: RaffleRouter.root, + return Scaffold( + body: Container( + decoration: const BoxDecoration(color: ColorConstants.background), + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const TopBar(root: RaffleRouter.root), + Expanded(child: child), + ], ), - Expanded(child: child), - ], + ), ), ); } diff --git a/lib/recommendation/providers/is_recommendation_admin_provider.dart b/lib/recommendation/providers/is_recommendation_admin_provider.dart index 343a990199..391e7acaed 100644 --- a/lib/recommendation/providers/is_recommendation_admin_provider.dart +++ b/lib/recommendation/providers/is_recommendation_admin_provider.dart @@ -5,5 +5,5 @@ final isRecommendationAdminProvider = StateProvider((ref) { final me = ref.watch(userProvider); return me.groups .map((e) => e.id) - .contains("53a669d6-84b1-4352-8d7c-421c1fbd9c6a"); + .contains("389215b2-ea45-4991-adc1-4d3e471541cf"); // admin_recommandation }); diff --git a/lib/recommendation/router.dart b/lib/recommendation/router.dart index 3cdce98c4c..393ccd62d9 100644 --- a/lib/recommendation/router.dart +++ b/lib/recommendation/router.dart @@ -1,7 +1,7 @@ -import 'package:either_dart/either.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:titan/drawer/class/module.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/class/module.dart'; import 'package:titan/recommendation/providers/is_recommendation_admin_provider.dart'; import 'package:titan/recommendation/ui/pages/main_page.dart' deferred as main_page; @@ -21,10 +21,10 @@ class RecommendationRouter { static const String information = '/information'; static const String addEdit = '/add_edit'; static final Module module = Module( - name: "Bons plans", - icon: const Left(HeroIcons.currencyEuro), + getName: (context) => AppLocalizations.of(context)!.moduleRecommendation, + getDescription: (context) => + AppLocalizations.of(context)!.moduleRecommendationDescription, root: RecommendationRouter.root, - selected: false, ); RecommendationRouter(this.ref); @@ -37,6 +37,10 @@ class RecommendationRouter { AuthenticatedMiddleware(ref), DeferredLoadingMiddleware(main_page.loadLibrary), ], + pageType: QCustomPage( + transitionsBuilder: (_, animation, _, child) => + FadeTransition(opacity: animation, child: child), + ), children: [ QRoute( path: information, diff --git a/lib/recommendation/tools/constants.dart b/lib/recommendation/tools/constants.dart index 90b3266180..8b13789179 100644 --- a/lib/recommendation/tools/constants.dart +++ b/lib/recommendation/tools/constants.dart @@ -1,25 +1 @@ -class RecommendationTextConstants { - static const String recommendation = "Bons plans"; - static const String title = "Titre"; - static const String logo = "Logo"; - static const String code = "Code"; - static const String summary = "Court résumé"; - static const String description = "Description"; - static const String add = "Ajouter"; - static const String edit = "Modifier"; - static const String delete = "Supprimer"; - static const String addImage = "Veuillez ajouter une image"; - static const String addedRecommendation = "Bon plan ajouté"; - static const String editedRecommendation = "Bon plan modifié"; - static const String deleteRecommendationConfirmation = - "Êtes-vous sûr de vouloir supprimer ce bon plan ?"; - static const String deleteRecommendation = "Suppresion"; - static const String deletingRecommendationError = - "Erreur lors de la suppression"; - static const String deletedRecommendation = "Bon plan supprimé"; - static const String incorrectOrMissingFields = - 'Champs incorrects ou manquants'; - static const String editingError = "Échec de la modification"; - static const String addingError = "Échec de l'ajout"; - static const String copiedCode = "Code de réduction copié"; -} + diff --git a/lib/recommendation/ui/pages/add_edit_page.dart b/lib/recommendation/ui/pages/add_edit_page.dart index d1e8f8ba27..fe7efb0ef2 100644 --- a/lib/recommendation/ui/pages/add_edit_page.dart +++ b/lib/recommendation/ui/pages/add_edit_page.dart @@ -11,7 +11,6 @@ import 'package:titan/recommendation/providers/recommendation_list_provider.dart import 'package:titan/recommendation/providers/recommendation_logo_map_provider.dart'; import 'package:titan/recommendation/providers/recommendation_logo_provider.dart'; import 'package:titan/recommendation/providers/recommendation_provider.dart'; -import 'package:titan/recommendation/tools/constants.dart'; import 'package:titan/recommendation/ui/widgets/recommendation_template.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; @@ -19,6 +18,7 @@ import 'package:titan/tools/ui/layouts/add_edit_button_layout.dart'; import 'package:titan/tools/ui/widgets/image_picker_on_tap.dart'; import 'package:titan/tools/ui/widgets/text_entry.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AddEditRecommendationPage extends HookConsumerWidget { const AddEditRecommendationPage({super.key}); @@ -71,14 +71,16 @@ class AddEditRecommendationPage extends HookConsumerWidget { children: [ TextEntry( maxLines: 1, - label: RecommendationTextConstants.title, + label: AppLocalizations.of(context)!.recommendationTitle, controller: title, ), const SizedBox(height: 30), FormField( validator: (e) { if (logoBytes.value == null && !isEdit) { - return RecommendationTextConstants.addImage; + return AppLocalizations.of( + context, + )!.recommendationAddImage; } return null; }, @@ -111,7 +113,7 @@ class AddEditRecommendationPage extends HookConsumerWidget { ), TextEntry( maxLines: 1, - label: RecommendationTextConstants.code, + label: AppLocalizations.of(context)!.recommendationCode, controller: code, canBeEmpty: true, ), @@ -120,7 +122,7 @@ class AddEditRecommendationPage extends HookConsumerWidget { minLines: 1, maxLines: 2, keyboardType: TextInputType.multiline, - label: RecommendationTextConstants.summary, + label: AppLocalizations.of(context)!.recommendationSummary, controller: summary, ), const SizedBox(height: 30), @@ -128,15 +130,17 @@ class AddEditRecommendationPage extends HookConsumerWidget { minLines: 5, maxLines: 50, keyboardType: TextInputType.multiline, - label: RecommendationTextConstants.description, + label: AppLocalizations.of( + context, + )!.recommendationDescription, controller: description, ), const SizedBox(height: 50), WaitingButton( child: Text( isEdit - ? RecommendationTextConstants.edit - : RecommendationTextConstants.add, + ? AppLocalizations.of(context)!.recommendationEdit + : AppLocalizations.of(context)!.recommendationAdd, style: const TextStyle( color: Colors.white, fontSize: 25, @@ -153,6 +157,20 @@ class AddEditRecommendationPage extends HookConsumerWidget { summary: summary.text, description: description.text, ); + final editedRecommendationMsg = isEdit + ? AppLocalizations.of( + context, + )!.recommendationEditedRecommendation + : AppLocalizations.of( + context, + )!.recommendationAddedRecommendation; + final editingErrorMsg = isEdit + ? AppLocalizations.of( + context, + )!.recommendationEditingError + : AppLocalizations.of( + context, + )!.recommendationAddingError; final value = isEdit ? await recommendationListNotifier .updateRecommendation(newRecommendation) @@ -160,14 +178,14 @@ class AddEditRecommendationPage extends HookConsumerWidget { newRecommendation, ); if (value) { + displayAdvertToastWithContext( + TypeMsg.msg, + editedRecommendationMsg, + ); if (isEdit) { recommendationNotifier.setRecommendation( newRecommendation, ); - displayAdvertToastWithContext( - TypeMsg.msg, - RecommendationTextConstants.editedRecommendation, - ); recommendationList.maybeWhen( data: (list) { if (logoBytes.value != null) { @@ -181,10 +199,6 @@ class AddEditRecommendationPage extends HookConsumerWidget { orElse: () {}, ); } else { - displayAdvertToastWithContext( - TypeMsg.msg, - RecommendationTextConstants.addedRecommendation, - ); recommendationList.maybeWhen( data: (list) { final newRecommendation = list.last; @@ -201,16 +215,16 @@ class AddEditRecommendationPage extends HookConsumerWidget { } else { displayAdvertToastWithContext( TypeMsg.error, - isEdit - ? RecommendationTextConstants.editingError - : RecommendationTextConstants.addingError, + editingErrorMsg, ); } } else { displayToast( context, TypeMsg.error, - RecommendationTextConstants.incorrectOrMissingFields, + AppLocalizations.of( + context, + )!.recommendationIncorrectOrMissingFields, ); } }, diff --git a/lib/recommendation/ui/pages/main_page.dart b/lib/recommendation/ui/pages/main_page.dart index dd0f782c83..d62cc8942c 100644 --- a/lib/recommendation/ui/pages/main_page.dart +++ b/lib/recommendation/ui/pages/main_page.dart @@ -27,6 +27,7 @@ class RecommendationMainPage extends HookConsumerWidget { return RecommendationTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await recommendationListNotifier.loadRecommendation(); }, diff --git a/lib/recommendation/ui/widgets/recommendation_card.dart b/lib/recommendation/ui/widgets/recommendation_card.dart index 4e4f07739f..66880d7610 100644 --- a/lib/recommendation/ui/widgets/recommendation_card.dart +++ b/lib/recommendation/ui/widgets/recommendation_card.dart @@ -9,7 +9,6 @@ import 'package:titan/recommendation/providers/recommendation_logo_map_provider. import 'package:titan/recommendation/providers/recommendation_logo_provider.dart'; import 'package:titan/recommendation/providers/recommendation_provider.dart'; import 'package:titan/recommendation/router.dart'; -import 'package:titan/recommendation/tools/constants.dart'; import 'package:titan/recommendation/ui/widgets/recommendation_card_layout.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; @@ -17,6 +16,7 @@ import 'package:titan/tools/ui/builders/auto_loader_child.dart'; import 'package:titan/tools/ui/layouts/card_button.dart'; import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class RecommendationCard extends HookConsumerWidget { final Recommendation recommendation; @@ -95,13 +95,13 @@ class RecommendationCard extends HookConsumerWidget { ), IconButton( onPressed: () async { + final copiedCodeMsg = AppLocalizations.of( + context, + )!.recommendationCopiedCode; await Clipboard.setData( ClipboardData(text: recommendation.code!), ); - displayToastWithContext( - TypeMsg.msg, - RecommendationTextConstants.copiedCode, - ); + displayToastWithContext(TypeMsg.msg, copiedCodeMsg); }, icon: const Icon(Icons.copy), ), @@ -158,9 +158,18 @@ class RecommendationCard extends HookConsumerWidget { await showDialog( context: context, builder: (context) => CustomDialogBox( - descriptions: RecommendationTextConstants - .deleteRecommendationConfirmation, + descriptions: AppLocalizations.of( + context, + )!.recommendationDeleteRecommendationConfirmation, onYes: () async { + final deletedRecommendationMsg = + AppLocalizations.of( + context, + )!.recommendationDeletedRecommendation; + final deletedRecommendationErrorMsg = + AppLocalizations.of( + context, + )!.recommendationDeletingRecommendationError; final value = await recommendationListNotifier .deleteRecommendation( @@ -169,20 +178,19 @@ class RecommendationCard extends HookConsumerWidget { if (value) { displayToastWithContext( TypeMsg.msg, - RecommendationTextConstants - .deletedRecommendation, + deletedRecommendationMsg, ); QR.back(); } else { displayToastWithContext( TypeMsg.error, - RecommendationTextConstants - .deletingRecommendationError, + deletedRecommendationErrorMsg, ); } }, - title: RecommendationTextConstants - .deleteRecommendation, + title: AppLocalizations.of( + context, + )!.recommendationDeleteRecommendation, ), ); }); diff --git a/lib/recommendation/ui/widgets/recommendation_template.dart b/lib/recommendation/ui/widgets/recommendation_template.dart index 1782cfbadc..3e22826904 100644 --- a/lib/recommendation/ui/widgets/recommendation_template.dart +++ b/lib/recommendation/ui/widgets/recommendation_template.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:titan/recommendation/router.dart'; -import 'package:titan/recommendation/tools/constants.dart'; import 'package:titan/tools/ui/widgets/top_bar.dart'; +import 'package:titan/tools/constants.dart'; class RecommendationTemplate extends StatelessWidget { final Widget child; @@ -9,16 +9,18 @@ class RecommendationTemplate extends StatelessWidget { @override Widget build(BuildContext context) { - return SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const TopBar( - title: RecommendationTextConstants.recommendation, - root: RecommendationRouter.root, + return Scaffold( + body: Container( + decoration: const BoxDecoration(color: ColorConstants.background), + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const TopBar(root: RecommendationRouter.root), + Expanded(child: child), + ], ), - Expanded(child: child), - ], + ), ), ); } diff --git a/lib/router.dart b/lib/router.dart index 6db942612d..fbb4d55e90 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:titan/admin/router.dart'; import 'package:titan/advert/router.dart'; @@ -6,14 +7,18 @@ import 'package:titan/booking/router.dart'; import 'package:titan/centralisation/router.dart'; import 'package:titan/cinema/router.dart'; import 'package:titan/event/router.dart'; +import 'package:titan/feed/router.dart'; import 'package:titan/flappybird/router.dart'; import 'package:titan/home/router.dart'; -import 'package:titan/home/ui/home.dart' deferred as home_page; +import 'package:titan/feed/ui/pages/main_page/main_page.dart' + deferred as feed_main_page; import 'package:titan/loan/router.dart'; import 'package:titan/login/router.dart'; import 'package:titan/others/ui/loading_page.dart' deferred as loading_page; import 'package:titan/others/ui/no_internet_page.dart' deferred as no_internet_page; +import 'package:titan/navigation/ui/all_module_page.dart' + deferred as all_module_page; import 'package:titan/others/ui/no_module.dart' deferred as no_module_page; import 'package:titan/others/ui/update_page.dart' deferred as update_page; import 'package:titan/paiement/router.dart'; @@ -24,8 +29,11 @@ import 'package:titan/recommendation/router.dart'; import 'package:titan/seed-library/router.dart'; import 'package:titan/settings/router.dart'; import 'package:titan/raffle/router.dart'; +import 'package:titan/super_admin/router.dart'; import 'package:titan/tools/middlewares/authenticated_middleware.dart'; import 'package:titan/tools/middlewares/deferred_middleware.dart'; +import 'package:titan/tools/ui/styleguide/router.dart'; + import 'package:titan/vote/router.dart'; import 'package:qlevar_router/qlevar_router.dart'; @@ -39,15 +47,16 @@ class AppRouter { static const String update = '/update'; static const String noInternet = '/no_internet'; static const String noModule = '/no_module'; + static const String allModules = '/all_modules'; AppRouter(this.ref) { routes = [ QRoute( path: root, - builder: () => home_page.HomePage(), + builder: () => feed_main_page.FeedMainPage(), middleware: [ AuthenticatedMiddleware(ref), - DeferredLoadingMiddleware(home_page.loadLibrary), + DeferredLoadingMiddleware(feed_main_page.loadLibrary), ], ), QRoute( @@ -70,7 +79,18 @@ class AppRouter { builder: () => no_module_page.NoModulePage(), middleware: [DeferredLoadingMiddleware(no_module_page.loadLibrary)], ), - AdminRouter(ref).route(), + QRoute( + path: allModules, + builder: () => all_module_page.AllModulePage(), + middleware: [ + AuthenticatedMiddleware(ref), + DeferredLoadingMiddleware(all_module_page.loadLibrary), + ], + pageType: QCustomPage( + transitionsBuilder: (_, animation, _, child) => + FadeTransition(opacity: animation, child: child), + ), + ), AdvertRouter(ref).route(), AmapRouter(ref).route(), BookingRouter(ref).route(), @@ -78,11 +98,10 @@ class AppRouter { CinemaRouter(ref).route(), EventRouter(ref).route(), FlappyBirdRouter(ref).route(), + FeedRouter(ref).route(), HomeRouter(ref).route(), LoanRouter(ref).route(), - LoginRouter(ref).accountRoute(), LoginRouter(ref).route(), - LoginRouter(ref).passwordRoute(), PaymentRouter(ref).route(), PhonebookRouter(ref).route(), PhRouter(ref).route(), @@ -90,8 +109,11 @@ class AppRouter { RaffleRouter(ref).route(), RecommendationRouter(ref).route(), SettingsRouter(ref).route(), + StyleGuideRouter(ref).route(), VoteRouter(ref).route(), SeedLibraryRouter(ref).route(), + AdminRouter(ref).route(), + SuperAdminRouter(ref).route(), ]; } } diff --git a/lib/seed-library/providers/is_seed_library_admin_provider.dart b/lib/seed-library/providers/is_seed_library_admin_provider.dart index 2fbe5945f5..8561852d46 100644 --- a/lib/seed-library/providers/is_seed_library_admin_provider.dart +++ b/lib/seed-library/providers/is_seed_library_admin_provider.dart @@ -5,5 +5,5 @@ final isSeedLibraryAdminProvider = StateProvider((ref) { final me = ref.watch(userProvider); return me.groups .map((e) => e.id) - .contains("09153d2a-14f4-49a4-be57-5d0f265261b9"); + .contains("09153d2a-14f4-49a4-be57-5d0f265261b9"); // admin_seed_library }); diff --git a/lib/seed-library/router.dart b/lib/seed-library/router.dart index 6451cc95aa..2599555ad7 100644 --- a/lib/seed-library/router.dart +++ b/lib/seed-library/router.dart @@ -1,7 +1,7 @@ -import 'package:either_dart/either.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:titan/drawer/class/module.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/class/module.dart'; import 'package:titan/seed-library/providers/is_seed_library_admin_provider.dart'; import 'package:titan/seed-library/ui/pages/add_edit_species_page/add_edit_species_page.dart' deferred as add_edit_species_page; @@ -48,10 +48,10 @@ class SeedLibraryRouter { SeedLibraryRouter(this.ref); static final Module module = Module( - name: "Grainothèque", - icon: const Left(HeroIcons.inboxStack), + getName: (context) => AppLocalizations.of(context)!.moduleSeedLibrary, + getDescription: (context) => + AppLocalizations.of(context)!.moduleSeedLibraryDescription, root: SeedLibraryRouter.root, - selected: false, ); QRoute route() => QRoute( @@ -62,6 +62,10 @@ class SeedLibraryRouter { AuthenticatedMiddleware(ref), DeferredLoadingMiddleware(main_page.loadLibrary), ], + pageType: QCustomPage( + transitionsBuilder: (_, animation, _, child) => + FadeTransition(opacity: animation, child: child), + ), children: [ QRoute( path: SeedLibraryRouter.information, diff --git a/lib/seed-library/ui/pages/edit_plant_detail_page/editable_plant_detail.dart b/lib/seed-library/ui/pages/edit_plant_detail_page/editable_plant_detail.dart index 6986b90876..ecb440483c 100644 --- a/lib/seed-library/ui/pages/edit_plant_detail_page/editable_plant_detail.dart +++ b/lib/seed-library/ui/pages/edit_plant_detail_page/editable_plant_detail.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:titan/seed-library/class/plant_complete.dart'; import 'package:titan/seed-library/providers/plant_complete_provider.dart'; import 'package:titan/seed-library/providers/plants_list_provider.dart'; @@ -20,6 +21,7 @@ class EditablePlantDetail extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final locale = Localizations.localeOf(context); final species = ref.watch(syncSpeciesListProvider); final plantNotifier = ref.watch(plantProvider.notifier); final myPlantsNotifier = ref.watch(myPlantListProvider.notifier); @@ -28,7 +30,9 @@ class EditablePlantDetail extends HookConsumerWidget { ); final notes = TextEditingController(text: plant.currentNote ?? ''); final plantationDate = TextEditingController( - text: plant.plantingDate != null ? processDate(plant.plantingDate!) : '', + text: plant.plantingDate != null + ? DateFormat.yMd(locale).format(plant.plantingDate!) + : '', ); final plantSpecies = species.firstWhere( @@ -120,7 +124,7 @@ class EditablePlantDetail extends HookConsumerWidget { style: const TextStyle(fontSize: 16), ), Text( - '${SeedLibraryTextConstants.borrowingDate} ${processDate(plant.borrowingDate!)}', + '${SeedLibraryTextConstants.borrowingDate} ${DateFormat.yMd(locale).format(plant.borrowingDate!)}', style: const TextStyle(fontSize: 16), ), SizedBox(height: 10), @@ -130,7 +134,9 @@ class EditablePlantDetail extends HookConsumerWidget { plantNotifier.updatePlant( plant.copyWith(plantingDate: DateTime.now()), ); - plantationDate.text = processDate(DateTime.now()); + plantationDate.text = DateFormat.yMd( + locale, + ).format(DateTime.now()); myPlantsNotifier.updatePlantInList( plant .copyWith(plantingDate: DateTime.now()) @@ -183,7 +189,9 @@ class EditablePlantDetail extends HookConsumerWidget { ) .toPlantSimple(), ); - plantationDate.text = processDate(DateTime.now()); + plantationDate.text = DateFormat.yMd( + locale, + ).format(DateTime.now()); } else { displayToastWithContext( TypeMsg.error, @@ -227,7 +235,12 @@ class EditablePlantDetail extends HookConsumerWidget { plantNotifier.updatePlant( plant.copyWith( plantingDate: plantationDate.text.isNotEmpty - ? DateTime.parse(processDateBack(plantationDate.text)) + ? DateTime.parse( + processDateBack( + plantationDate.text, + locale.toString(), + ), + ) : null, ), ); diff --git a/lib/seed-library/ui/pages/plants_page/personal_plant_card.dart b/lib/seed-library/ui/pages/plants_page/personal_plant_card.dart index 674c2d5a7e..881a9a0f28 100644 --- a/lib/seed-library/ui/pages/plants_page/personal_plant_card.dart +++ b/lib/seed-library/ui/pages/plants_page/personal_plant_card.dart @@ -1,10 +1,10 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:titan/seed-library/class/plant_simple.dart'; import 'package:titan/seed-library/providers/species_list_provider.dart'; import 'package:titan/seed-library/tools/constants.dart'; -import 'package:titan/tools/functions.dart'; import 'package:titan/seed-library/tools/functions.dart' as function; class PersonalPlantCard extends HookConsumerWidget { @@ -19,6 +19,7 @@ class PersonalPlantCard extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final locale = Localizations.localeOf(context); final species = ref.watch(syncSpeciesListProvider); final plantSpecies = species.firstWhere( (element) => element.id == plant.speciesId, @@ -64,7 +65,9 @@ class PersonalPlantCard extends HookConsumerWidget { ? SeedLibraryTextConstants.deathDate : SeedLibraryTextConstants.plantingDate, ), - Text(processDate(plant.plantingDate!)), + Text( + DateFormat.yMd(locale).format(plant.plantingDate!), + ), ...plantSpecies.timeMaturation != null && plant.state != function.State.consumed ? [ diff --git a/lib/seed-library/ui/pages/plants_page/plants_page.dart b/lib/seed-library/ui/pages/plants_page/plants_page.dart index 0000a6f950..50956bab7d 100644 --- a/lib/seed-library/ui/pages/plants_page/plants_page.dart +++ b/lib/seed-library/ui/pages/plants_page/plants_page.dart @@ -27,6 +27,7 @@ class PlantsPage extends HookConsumerWidget { return SeedLibraryTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await plantListNotifier.loadPlants(); }, diff --git a/lib/seed-library/ui/pages/species_page/species_page.dart b/lib/seed-library/ui/pages/species_page/species_page.dart index f8e0914694..007c362a78 100644 --- a/lib/seed-library/ui/pages/species_page/species_page.dart +++ b/lib/seed-library/ui/pages/species_page/species_page.dart @@ -39,6 +39,7 @@ class SpeciesPage extends HookConsumerWidget { return SeedLibraryTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await speciesListNotifier.loadSpecies(); }, diff --git a/lib/seed-library/ui/pages/stock_page/stocks_page.dart b/lib/seed-library/ui/pages/stock_page/stocks_page.dart index e291b8bd1f..a67b0eda82 100644 --- a/lib/seed-library/ui/pages/stock_page/stocks_page.dart +++ b/lib/seed-library/ui/pages/stock_page/stocks_page.dart @@ -23,6 +23,7 @@ class StockPage extends HookConsumerWidget { return SeedLibraryTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await plantListNotifier.loadPlants(); }, diff --git a/lib/seed-library/ui/seed_library.dart b/lib/seed-library/ui/seed_library.dart index 74630c7aea..b439d5932b 100644 --- a/lib/seed-library/ui/seed_library.dart +++ b/lib/seed-library/ui/seed_library.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; +import 'package:qlevar_router/qlevar_router.dart'; import 'package:titan/seed-library/router.dart'; -import 'package:titan/seed-library/tools/constants.dart'; +import 'package:titan/tools/constants.dart'; import 'package:titan/tools/ui/widgets/top_bar.dart'; -import 'package:qlevar_router/qlevar_router.dart'; class SeedLibraryTemplate extends StatelessWidget { final Widget child; @@ -11,29 +11,32 @@ class SeedLibraryTemplate extends StatelessWidget { @override Widget build(BuildContext context) { - return SafeArea( - child: Column( - children: [ - TopBar( - title: SeedLibraryTextConstants.seedLibrary, - root: SeedLibraryRouter.root, - rightIcon: QR.currentPath == SeedLibraryRouter.root - ? IconButton( - onPressed: () { - QR.to( - SeedLibraryRouter.root + SeedLibraryRouter.information, - ); - }, - icon: const HeroIcon( - HeroIcons.informationCircle, - color: Colors.black, - size: 40, - ), - ) - : null, - ), - Expanded(child: child), - ], + return Container( + color: ColorConstants.background, + child: SafeArea( + child: Column( + children: [ + TopBar( + root: SeedLibraryRouter.root, + rightIcon: QR.currentPath == SeedLibraryRouter.root + ? IconButton( + onPressed: () { + QR.to( + SeedLibraryRouter.root + + SeedLibraryRouter.information, + ); + }, + icon: const HeroIcon( + HeroIcons.informationCircle, + color: Colors.black, + size: 40, + ), + ) + : null, + ), + Expanded(child: child), + ], + ), ), ); } diff --git a/lib/service/provider_list.dart b/lib/service/provider_list.dart index 1f2713c1ff..2fdd310e02 100644 --- a/lib/service/provider_list.dart +++ b/lib/service/provider_list.dart @@ -1,4 +1,4 @@ -import 'package:titan/admin/notification_service.dart'; +import 'package:titan/super_admin/notification_service.dart'; import 'package:titan/advert/notification_service.dart'; import 'package:titan/amap/notification_service.dart'; import 'package:titan/booking/notification_service.dart'; diff --git a/lib/service/tools/setup.dart b/lib/service/tools/setup.dart index b557d0af29..43d2551889 100644 --- a/lib/service/tools/setup.dart +++ b/lib/service/tools/setup.dart @@ -1,6 +1,5 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:titan/service/class/message.dart' as message_class; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:titan/service/local_notification_service.dart'; @@ -58,8 +57,8 @@ void setUpNotification(WidgetRef ref) { ); message_class.Message me = message_class.Message( - title: message.notification?.title ?? "No title", - content: message.notification?.body ?? "No body", + title: message.notification?.title, + content: message.notification?.body, actionModule: messages.actionModule, actionTable: messages.actionTable, ); @@ -69,7 +68,6 @@ void setUpNotification(WidgetRef ref) { @pragma('vm:entry-point') Future firebaseMessagingBackgroundHandler(RemoteMessage message) async { - await dotenv.load(); await Firebase.initializeApp(); await FirebaseMessaging.instance.getToken(); await LocalNotificationService().init(); diff --git a/lib/settings/class/notification_topic.dart b/lib/settings/class/notification_topic.dart new file mode 100644 index 0000000000..e0b70a7405 --- /dev/null +++ b/lib/settings/class/notification_topic.dart @@ -0,0 +1,61 @@ +class NotificationTopic { + NotificationTopic({ + required this.id, + required this.name, + required this.moduleRoot, + this.topicIdentifier, + required this.isUserSubscribed, + }); + late final String id; + late final String name; + late final String moduleRoot; + late final String? topicIdentifier; + late final bool isUserSubscribed; + + NotificationTopic.fromJson(Map json) { + id = json['id']; + name = json['name']; + moduleRoot = json['module_root']; + topicIdentifier = json['topic_identifier']; + isUserSubscribed = json['is_user_subscribed']; + } + + Map toJson() { + final data = {}; + data['id'] = id; + data['name'] = name; + data['module_root'] = moduleRoot; + data['topic_identifier'] = topicIdentifier; + data['is_user_subscribed'] = isUserSubscribed; + return data; + } + + NotificationTopic copyWith({ + String? id, + String? name, + String? moduleRoot, + String? topicIdentifier, + bool? isUserSubscribed, + }) { + return NotificationTopic( + id: id ?? this.id, + name: name ?? this.name, + moduleRoot: moduleRoot ?? this.moduleRoot, + topicIdentifier: topicIdentifier ?? this.topicIdentifier, + isUserSubscribed: isUserSubscribed ?? this.isUserSubscribed, + ); + } + + NotificationTopic.empty() { + id = ''; + name = ''; + moduleRoot = ''; + topicIdentifier = null; + isUserSubscribed = false; + } + + @override + String toString() { + return 'NotificationTopic{id : $id, name : $name, moduleRoot : $moduleRoot, topicIdentifier : $topicIdentifier, isUserSubscribed : $isUserSubscribed}'; + } +} diff --git a/lib/settings/providers/logs_provider.dart b/lib/settings/providers/logs_provider.dart deleted file mode 100644 index 61b4e03c42..0000000000 --- a/lib/settings/providers/logs_provider.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/tools/logs/log.dart'; -import 'package:titan/tools/logs/logger.dart'; -import 'package:titan/tools/providers/list_notifier.dart'; -import 'package:titan/tools/repository/repository.dart'; - -class LogsProvider extends ListNotifier { - Logger logger = Repository.logger; - LogsProvider() : super(const AsyncValue.loading()); - - Future>> getLogs() async { - return await loadList(() async => logger.getLogs()); - } - - Future> getNotificationLogs() async { - return logger.getNotificationLogs(); - } - - Future deleteLogs() async { - return await delete( - (id) async => true, - (listT, t) { - logger.clearLogs(); - return []; - }, - "", - Log.empty(), - ); - } -} - -final logsProvider = StateNotifierProvider>>( - (ref) { - LogsProvider notifier = LogsProvider(); - notifier.getLogs(); - return notifier; - }, -); - -class NotificationLogsProvider extends ListNotifier { - Logger logger = Repository.logger; - NotificationLogsProvider() : super(const AsyncValue.loading()); - - Future>> getLogs() async { - return await loadList(() async => logger.getNotificationLogs()); - } - - Future deleteLogs() async { - return await delete( - (id) async => true, - (listT, t) { - logger.clearNotificationLogs(); - return []; - }, - "", - Log.empty(), - ); - } -} - -final notificationLogsProvider = - StateNotifierProvider>>(( - ref, - ) { - NotificationLogsProvider notifier = NotificationLogsProvider(); - notifier.getLogs(); - return notifier; - }); diff --git a/lib/settings/providers/logs_tab_provider.dart b/lib/settings/providers/logs_tab_provider.dart deleted file mode 100644 index bef4623762..0000000000 --- a/lib/settings/providers/logs_tab_provider.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -enum LogTabs { log, notification } - -class LogTabsNotifier extends StateNotifier { - LogTabsNotifier() : super(LogTabs.log); - - void setLogTabs(LogTabs i) { - state = i; - } -} - -final logTabProvider = StateNotifierProvider((ref) { - return LogTabsNotifier(); -}); diff --git a/lib/settings/providers/module_list_provider.dart b/lib/settings/providers/module_list_provider.dart index 6e4149cd0f..542f7cdf4c 100644 --- a/lib/settings/providers/module_list_provider.dart +++ b/lib/settings/providers/module_list_provider.dart @@ -1,25 +1,30 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/admin/router.dart'; +import 'package:titan/admin/providers/is_admin_provider.dart'; import 'package:titan/advert/router.dart'; -import 'package:titan/admin/providers/all_my_module_roots_list_provider.dart'; +import 'package:titan/super_admin/providers/all_my_module_roots_list_provider.dart'; import 'package:titan/amap/router.dart'; import 'package:titan/booking/router.dart'; import 'package:titan/centralisation/router.dart'; import 'package:titan/cinema/router.dart'; -import 'package:titan/drawer/class/module.dart'; -import 'package:collection/collection.dart'; import 'package:titan/event/router.dart'; -import 'package:titan/home/router.dart'; import 'package:titan/loan/router.dart'; +import 'package:titan/navigation/class/module.dart'; +import 'package:collection/collection.dart'; +import 'package:titan/home/router.dart'; import 'package:titan/paiement/router.dart'; -import 'package:titan/phonebook/router.dart'; import 'package:titan/ph/router.dart'; +import 'package:titan/phonebook/router.dart'; import 'package:titan/purchases/router.dart'; import 'package:titan/raffle/router.dart'; import 'package:titan/recommendation/router.dart'; import 'package:titan/seed-library/router.dart'; -import 'package:titan/vote/router.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:titan/settings/router.dart'; +import 'package:titan/super_admin/providers/is_super_admin_provider.dart'; +import 'package:titan/super_admin/router.dart'; +import 'package:titan/vote/router.dart'; final modulesProvider = StateNotifierProvider>(( ref, @@ -29,7 +34,13 @@ final modulesProvider = StateNotifierProvider>(( .map((root) => '/$root') .toList(); - ModulesNotifier modulesNotifier = ModulesNotifier(); + final isAdmin = ref.watch(isAdminProvider); + final isSuperAdmin = ref.watch(isSuperAdminProvider); + + ModulesNotifier modulesNotifier = ModulesNotifier( + isAdmin: isAdmin, + isSuperAdmin: isSuperAdmin, + ); modulesNotifier.loadModules(myModulesRoot); return modulesNotifier; }); @@ -37,6 +48,8 @@ final modulesProvider = StateNotifierProvider>(( class ModulesNotifier extends StateNotifier> { String dbModule = "modules"; String dbAllModules = "allModules"; + final bool isAdmin; + final bool isSuperAdmin; final eq = const DeepCollectionEquality.unordered(); List allModules = [ HomeRouter.module, @@ -56,7 +69,8 @@ class ModulesNotifier extends StateNotifier> { VoteRouter.module, SeedLibraryRouter.module, ]; - ModulesNotifier() : super([]); + ModulesNotifier({required this.isAdmin, required this.isSuperAdmin}) + : super([]); void saveModules() { SharedPreferences.getInstance().then((prefs) { @@ -78,6 +92,14 @@ class ModulesNotifier extends StateNotifier> { }); } + Module getModuleByRoot(String root) { + try { + return allModules.firstWhere((m) => m.root.toString() == root); + } catch (e) { + return allModules.first; + } + } + Future loadModules(List roots) async { final prefs = await SharedPreferences.getInstance(); List modulesName = prefs.getStringList(dbModule) ?? []; @@ -120,6 +142,11 @@ class ModulesNotifier extends StateNotifier> { for (Module module in toDelete) { allModules.remove(module); } + allModules.addAll([ + SettingsRouter.module, + if (isAdmin) AdminRouter.module, + if (isSuperAdmin) SuperAdminRouter.module, + ]); state = allModules; } diff --git a/lib/settings/providers/notification_topic_provider.dart b/lib/settings/providers/notification_topic_provider.dart new file mode 100644 index 0000000000..7a8f56fb8b --- /dev/null +++ b/lib/settings/providers/notification_topic_provider.dart @@ -0,0 +1,50 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/auth/providers/openid_provider.dart'; +import 'package:titan/settings/class/notification_topic.dart'; +import 'package:titan/settings/repositories/notification_topic_repository.dart'; +import 'package:titan/tools/providers/list_notifier.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; + +class NotificationTopicNotifier extends ListNotifier { + final NotificationTopicRepository notificationTopicRepository = + NotificationTopicRepository(); + NotificationTopicNotifier({required String token}) + : super(const AsyncValue.loading()) { + notificationTopicRepository.setToken(token); + } + + Future>> + loadNotificationTopicList() async { + return await loadList( + () async => notificationTopicRepository.getAllNotificationTopic(), + ); + } + + Future toggleSubscription(NotificationTopic topic) async { + return await update( + topic.isUserSubscribed + ? notificationTopicRepository.unsubscribeTopic + : notificationTopicRepository.subscribeTopic, + (topics, topic) { + topics[topics.indexWhere((t) => t.id == topic.id)] = topic.copyWith( + isUserSubscribed: !topic.isUserSubscribed, + ); + return topics; + }, + topic, + ); + } +} + +final notificationTopicListProvider = + StateNotifierProvider< + NotificationTopicNotifier, + AsyncValue> + >((ref) { + final token = ref.watch(tokenProvider); + final notifier = NotificationTopicNotifier(token: token); + tokenExpireWrapperAuth(ref, () async { + await notifier.loadNotificationTopicList(); + }); + return notifier; + }); diff --git a/lib/settings/repositories/notification_topic_repository.dart b/lib/settings/repositories/notification_topic_repository.dart new file mode 100644 index 0000000000..d1a597bc65 --- /dev/null +++ b/lib/settings/repositories/notification_topic_repository.dart @@ -0,0 +1,30 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/auth/providers/openid_provider.dart'; +import 'package:titan/settings/class/notification_topic.dart'; +import 'package:titan/tools/repository/repository.dart'; + +class NotificationTopicRepository extends Repository { + @override + // ignore: overridden_fields + final ext = "notification/"; + + Future> getAllNotificationTopic() async { + return (await getList( + suffix: 'topics', + )).map((e) => NotificationTopic.fromJson(e)).toList(); + } + + Future subscribeTopic(NotificationTopic topic) async { + return await create({}, suffix: "topics/${topic.id}/subscribe"); + } + + Future unsubscribeTopic(NotificationTopic topic) async { + return await create({}, suffix: "topics/${topic.id}/unsubscribe"); + } +} + +final notificationTopicRepositoryProvider = + Provider((ref) { + final token = ref.watch(tokenProvider); + return NotificationTopicRepository()..setToken(token); + }); diff --git a/lib/settings/router.dart b/lib/settings/router.dart index 2718ccf5e0..3bf72e30ac 100644 --- a/lib/settings/router.dart +++ b/lib/settings/router.dart @@ -1,17 +1,11 @@ -import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:titan/settings/ui/pages/change_pass/change_pass.dart' - deferred as change_pass; -import 'package:titan/settings/ui/pages/edit_user_page/edit_user_page.dart' - deferred as edit_user_page; -import 'package:titan/settings/ui/pages/log_page/log_page.dart' - deferred as log_page; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/class/module.dart'; + import 'package:titan/settings/ui/pages/main_page/main_page.dart' deferred as main_page; -import 'package:titan/settings/ui/pages/modules_page/modules_page.dart' - deferred as modules_page; -import 'package:titan/settings/ui/pages/notification_page/notification_page.dart' - deferred as notification_page; + import 'package:titan/tools/middlewares/authenticated_middleware.dart'; import 'package:titan/tools/middlewares/deferred_middleware.dart'; import 'package:qlevar_router/qlevar_router.dart'; @@ -19,11 +13,13 @@ import 'package:qlevar_router/qlevar_router.dart'; class SettingsRouter { final Ref ref; static const String root = '/settings'; - static const String editAccount = '/edit_account'; - static const String changePassword = '/change_password'; - static const String logs = '/logs'; - static const String modules = '/modules'; - static const String notifications = '/notifications'; + static final Module module = Module( + getName: (context) => AppLocalizations.of(context)!.moduleSettings, + getDescription: (context) => + AppLocalizations.of(context)!.moduleSettingsDescription, + root: SettingsRouter.root, + ); + SettingsRouter(this.ref); QRoute route() => QRoute( @@ -34,33 +30,9 @@ class SettingsRouter { AuthenticatedMiddleware(ref), DeferredLoadingMiddleware(main_page.loadLibrary), ], - children: [ - QRoute( - path: editAccount, - builder: () => edit_user_page.EditUserPage(), - middleware: [DeferredLoadingMiddleware(edit_user_page.loadLibrary)], - ), - QRoute( - path: changePassword, - builder: () => change_pass.ChangePassPage(), - middleware: [DeferredLoadingMiddleware(change_pass.loadLibrary)], - ), - if (!kIsWeb) - QRoute( - path: logs, - builder: () => log_page.LogPage(), - middleware: [DeferredLoadingMiddleware(log_page.loadLibrary)], - ), - QRoute( - path: modules, - builder: () => modules_page.ModulesPage(), - middleware: [DeferredLoadingMiddleware(modules_page.loadLibrary)], - ), - QRoute( - path: notifications, - builder: () => notification_page.NotificationPage(), - middleware: [DeferredLoadingMiddleware(notification_page.loadLibrary)], - ), - ], + pageType: QCustomPage( + transitionsBuilder: (_, animation, _, child) => + FadeTransition(opacity: animation, child: child), + ), ); } diff --git a/lib/settings/tools/constants.dart b/lib/settings/tools/constants.dart deleted file mode 100644 index 2ae11f4f1a..0000000000 --- a/lib/settings/tools/constants.dart +++ /dev/null @@ -1,76 +0,0 @@ -class SettingsTextConstants { - static const String account = "Compte"; - static const String addProfilePicture = "Ajouter une photo"; - static const String admin = "Administrateur"; - static const String askHelp = "Demander de l'aide"; - static const String association = "Association"; - static const String birthday = "Date de naissance"; - static const String bugs = "Bugs"; - static const String changePassword = "Changer de mot de passe"; - static const String changingPassword = - "Voulez-vous vraiment changer votre mot de passe ?"; - static const String confirmPassword = "Confirmer le mot de passe"; - static const String copied = "Copié !"; - static const String darkMode = "Mode sombre"; - static const String darkModeOff = "Désactivé"; - static const String deleteLogs = "Supprimer les logs ?"; - static const String deleteNotificationLogs = - "Supprimer les logs des notifications ?"; - static const String detelePersonalData = "Supprimer mes données personnelles"; - static const String detelePersonalDataDesc = - "Cette action notifie l'administrateur que vous souhaitez supprimer vos données personnelles."; - static const String deleting = "Suppresion"; - static const String edit = "Modifier"; - static const String editAccount = "Modifier le compte"; - static const String editPassword = "Modifier le mot de passe"; - static const String email = "Email"; - static const String emptyField = "Ce champ ne peut pas être vide"; - static const String errorProfilePicture = - "Erreur lors de la modification de la photo de profil"; - static const String errorSendingDemand = - "Erreur lors de l'envoi de la demande"; - static const String eventsIcal = "Lien Ical des événements"; - static const String expectingDate = "Date de naissance attendue"; - static const String firstname = "Prénom"; - static const String floor = "Étage"; - static const String help = "Aide"; - static const String icalCopied = "Lien Ical copié !"; - static const String language = "Langue"; - static const String languageFr = "Français"; - static const String logs = "Logs"; - static const String modules = "Modules"; - static const String myIcs = "Mon lien Ical"; - static const String name = "Nom"; - static const String newPassword = "Nouveau mot de passe"; - static const String nickname = "Surnom"; - static const String notifications = "Notifications"; - static const String oldPassword = "Ancien mot de passe"; - static const String passwordChanged = "Mot de passe changé"; - static const String passwordsNotMatch = - "Les mots de passe ne correspondent pas"; - static const String personalData = "Données personnelles"; - static const String personalisation = "Personnalisation"; - static const String phone = "Téléphone"; - static const String profilePicture = "Photo de profil"; - static const String promo = "Promotion"; - static const String repportBug = "Signaler un bug"; - static const String save = "Enregistrer"; - static const String security = "Sécurité"; - static const String sendedDemand = "Demande envoyée"; - static const String settings = "Paramètres"; - static const String tooHeavyProfilePicture = - "L'image est trop lourde (max 4Mo)"; - static const String updatedProfile = "Profil modifié"; - static const String updatedProfilePicture = "Photo de profil modifiée"; - static const String updateNotification = "Mettre à jour les notifications"; - static const String updatingError = - "Erreur lors de la modification du profil"; - static const String version = "Version"; - - static const String passwordStrength = "Force du mot de passe"; - static const String passwordStrengthVeryWeak = "Très faible"; - static const String passwordStrengthWeak = "Faible"; - static const String passwordStrengthMedium = "Moyen"; - static const String passwordStrengthStrong = "Fort"; - static const String passwordStrengthVeryStrong = "Très fort"; -} diff --git a/lib/settings/tools/functions.dart b/lib/settings/tools/functions.dart new file mode 100644 index 0000000000..47bf664eed --- /dev/null +++ b/lib/settings/tools/functions.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/settings/class/notification_topic.dart'; +import 'package:titan/settings/providers/module_list_provider.dart'; + +Map> groupNotificationTopicsByModuleRoot( + List topics, + WidgetRef ref, + BuildContext context, +) { + final Map> tempGroups = {}; + final Map> result = {}; + final allModules = ref.read(modulesProvider.notifier).allModules; + for (final topic in topics) { + tempGroups.putIfAbsent(topic.moduleRoot, () => []).add(topic); + } + + final Map rootToName = { + for (final module in allModules) + module.root.replaceFirst('/', ''): module.getName(context), + }; + + final List singleTopics = []; + + tempGroups.forEach((moduleRoot, topicList) { + if (topicList.length == 1) { + singleTopics.addAll(topicList); + } else { + final moduleName = rootToName[moduleRoot] ?? moduleRoot; + result[moduleName] = topicList; + } + }); + + if (singleTopics.isNotEmpty) { + result["others"] = singleTopics; + } + + return result; +} diff --git a/lib/settings/ui/pages/change_pass/change_pass.dart b/lib/settings/ui/pages/change_pass/change_pass.dart deleted file mode 100644 index 336444414e..0000000000 --- a/lib/settings/ui/pages/change_pass/change_pass.dart +++ /dev/null @@ -1,170 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/settings/tools/constants.dart'; -import 'package:titan/settings/ui/pages/change_pass/password_strength.dart'; -import 'package:titan/settings/ui/pages/change_pass/test_entry_style.dart'; -import 'package:titan/settings/ui/settings.dart'; -import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/ui/layouts/add_edit_button_layout.dart'; -import 'package:titan/tools/ui/widgets/align_left_text.dart'; -import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:titan/tools/token_expire_wrapper.dart'; -import 'package:titan/tools/ui/builders/waiting_button.dart'; -import 'package:titan/user/providers/user_provider.dart'; -import 'package:qlevar_router/qlevar_router.dart'; - -class ChangePassPage extends HookConsumerWidget { - const ChangePassPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final key = GlobalKey(); - final oldPassword = useTextEditingController(); - final newPassword = useTextEditingController(); - final confirmPassword = useTextEditingController(); - final hideOldPass = useState(true); - final hideNewPass = useState(true); - final hideConfirmPass = useState(true); - final userNotifier = ref.watch(asyncUserProvider.notifier); - final user = ref.watch(userProvider); - void displayToastWithContext(TypeMsg type, String msg) { - displayToast(context, type, msg); - } - - return SettingsTemplate( - child: SingleChildScrollView( - physics: const BouncingScrollPhysics(), - child: Form( - key: key, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 30.0), - child: Column( - children: [ - const SizedBox(height: 30), - const AlignLeftText( - SettingsTextConstants.changePassword, - fontSize: 20, - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16), - child: TextFormField( - cursorColor: ColorConstants.gradient1, - decoration: changePassInputDecoration( - hintText: SettingsTextConstants.oldPassword, - notifier: hideOldPass, - ), - controller: oldPassword, - obscureText: hideOldPass.value, - validator: (value) { - if (value == null || value.isEmpty) { - return SettingsTextConstants.emptyField; - } - return null; - }, - ), - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16), - child: TextFormField( - cursorColor: ColorConstants.gradient1, - decoration: changePassInputDecoration( - hintText: SettingsTextConstants.newPassword, - notifier: hideNewPass, - ), - controller: newPassword, - obscureText: hideNewPass.value, - validator: (value) { - if (value == null || value.isEmpty) { - return SettingsTextConstants.emptyField; - } - return null; - }, - ), - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16), - child: TextFormField( - cursorColor: ColorConstants.gradient1, - decoration: changePassInputDecoration( - hintText: SettingsTextConstants.confirmPassword, - notifier: hideConfirmPass, - ), - controller: confirmPassword, - obscureText: hideConfirmPass.value, - validator: (value) { - if (value == null || value.isEmpty) { - return SettingsTextConstants.emptyField; - } else if (value != newPassword.text) { - return SettingsTextConstants.passwordsNotMatch; - } - return null; - }, - ), - ), - const SizedBox(height: 40), - PasswordStrength(newPassword: newPassword), - const SizedBox(height: 60), - WaitingButton( - builder: (child) => AddEditButtonLayout( - colors: const [ - ColorConstants.gradient1, - ColorConstants.gradient2, - ], - child: child, - ), - onTap: () async { - if (key.currentState!.validate()) { - await showDialog( - context: context, - builder: (context) => CustomDialogBox( - descriptions: SettingsTextConstants.changingPassword, - onYes: () async { - await tokenExpireWrapper(ref, () async { - final value = await userNotifier.changePassword( - oldPassword.text, - newPassword.text, - user, - ); - if (value) { - QR.back(); - displayToastWithContext( - TypeMsg.msg, - SettingsTextConstants.passwordChanged, - ); - } else { - displayToastWithContext( - TypeMsg.error, - SettingsTextConstants.updatingError, - ); - } - }); - }, - title: SettingsTextConstants.edit, - ), - ); - } - }, - child: const Center( - child: Text( - SettingsTextConstants.save, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/settings/ui/pages/change_pass/password_strength.dart b/lib/settings/ui/pages/change_pass/password_strength.dart deleted file mode 100644 index 1eb4504499..0000000000 --- a/lib/settings/ui/pages/change_pass/password_strength.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/settings/tools/constants.dart'; -import 'package:titan/settings/ui/pages/change_pass/secure_bar.dart'; -import 'package:titan/tools/ui/widgets/align_left_text.dart'; - -class PasswordStrength extends HookConsumerWidget { - final TextEditingController newPassword; - final Color textColor; - final Color color0 = const Color(0xffd31336); - final Color color1 = const Color(0xff880e65); - final Color color2 = const Color(0xff1c1840); - final Color color3 = const Color(0xff3a5a81); - final Color color4 = const Color(0xff1791b1); - - const PasswordStrength({ - super.key, - required this.newPassword, - this.textColor = Colors.black, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final currentStrength = useState( - SettingsTextConstants.passwordStrengthVeryWeak, - ); - final useColor = textColor == Colors.black; - return ValueListenableBuilder( - valueListenable: newPassword, - builder: (context, value, child) { - return Column( - children: [ - const SizedBox(height: 10), - AlignLeftText( - "${SettingsTextConstants.passwordStrength} : ${currentStrength.value}", - color: textColor, - ), - const SizedBox(height: 10), - FlutterPasswordStrength( - password: newPassword.text, - backgroundColor: Colors.transparent, - radius: 10, - strengthColors: TweenSequence([ - TweenSequenceItem( - weight: 1.0, - tween: Tween( - begin: useColor ? color0 : Colors.white, - end: useColor ? color1 : Colors.white, - ), - ), - TweenSequenceItem( - weight: 1.0, - tween: Tween( - begin: useColor ? color1 : Colors.white, - end: useColor ? color2 : Colors.white, - ), - ), - TweenSequenceItem( - weight: 1.0, - tween: Tween( - begin: useColor ? color2 : Colors.white, - end: useColor ? color3 : Colors.white, - ), - ), - TweenSequenceItem( - weight: 1.0, - tween: Tween( - begin: useColor ? color3 : Colors.white, - end: useColor ? color4 : Colors.white, - ), - ), - ]), - strengthCallback: (strength) { - if (strength < 0.2) { - currentStrength.value = - SettingsTextConstants.passwordStrengthVeryWeak; - } else if (strength < 0.4) { - currentStrength.value = - SettingsTextConstants.passwordStrengthWeak; - } else if (strength < 0.6) { - currentStrength.value = - SettingsTextConstants.passwordStrengthMedium; - } else if (strength < 0.8) { - currentStrength.value = - SettingsTextConstants.passwordStrengthStrong; - } else { - currentStrength.value = - SettingsTextConstants.passwordStrengthVeryStrong; - } - }, - ), - ], - ); - }, - ); - } -} diff --git a/lib/settings/ui/pages/change_pass/secure_bar.dart b/lib/settings/ui/pages/change_pass/secure_bar.dart deleted file mode 100644 index ab081792b9..0000000000 --- a/lib/settings/ui/pages/change_pass/secure_bar.dart +++ /dev/null @@ -1,312 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:zxcvbn/zxcvbn.dart'; - -// Source : https://github.com/JinHoSo/flutter-password-strength/blob/master/lib/flutter_password_strength.dart - -class FlutterPasswordStrength extends StatefulWidget { - final String? password; - - //Strength bar width - final double? width; - - //Strength bar height - final double height; - - //Strength bar colors are changed depending on strength - final Animatable strengthColors; - - //Strength bar background color - final Color backgroundColor; - - //Strength bar radius - final double radius; - - //Strength bar animation duration - final Duration? duration; - - //Strength callback - final void Function(double strength)? strengthCallback; - - const FlutterPasswordStrength({ - super.key, - required this.password, - this.width, - this.height = 5, - required this.strengthColors, - this.backgroundColor = Colors.grey, - this.radius = 0, - this.duration, - this.strengthCallback, - }); - - /* - default strength bar colors - This is approximate values - 0.0 ~ 0.25 : red - 0.26 ~ 0.5 : yellow - 0.51 ~ 0.75 : blue - 0.76 ~ 1 : green - */ - Animatable get _strengthColors => strengthColors; - - //default duration is 300 milliseconds - Duration? get _duration => duration ?? const Duration(milliseconds: 300); - - @override - FlutterPasswordStrengthState createState() => FlutterPasswordStrengthState(); -} - -class FlutterPasswordStrengthState extends State - with SingleTickerProviderStateMixin { - //Animation controller for strength bar - late AnimationController _animationController; - - //Animation for strength bar sharp - late Animation _strengthBarAnimation; - - //Strength bar colors - late Animatable _strengthBarColors; - - //Strength bar color from the list of strength bar colors - late Color _strengthBarColor; - - //Strength bar color - late Color _backgroundColor; - - //Strength bar width - double? _width; - - //Strength bar height - late double _height; - - //Strength bar radius, default is 0 - double _radius = 0; - - //Strength callback - void Function(double strength)? _strengthCallback; - - //_begin is used in _strengthBarAnimation - double _begin = 0; - - //_end is used in _strengthBarAnimation - double _end = 0; - - //calculated strength from password - double _passwordStrength = 0; - - // zxcvbn password strength estimator - Zxcvbn zxcvbn = Zxcvbn(); - - @override - void initState() { - super.initState(); - - //initialize - _animationController = AnimationController( - duration: widget._duration, - vsync: this, - ); - _strengthBarAnimation = Tween( - begin: _begin, - end: _end, - ).animate(_animationController); - _strengthBarColors = widget._strengthColors; - _strengthBarColor = - _strengthBarColors.evaluate( - AlwaysStoppedAnimation(_passwordStrength), - ) ?? - Colors.transparent; - - _backgroundColor = widget.backgroundColor; - - _width = widget.width; - _height = widget.height; - _radius = widget.radius; - _strengthCallback = widget.strengthCallback; - - //start animation - _animationController.forward(); - } - - void animate() { - //calculate strength - if (widget.password == null || widget.password!.isEmpty) { - _passwordStrength = 0; - } else { - _passwordStrength = - (zxcvbn.evaluate(widget.password ?? "").score ?? 0) / 4; - } - - _begin = _end; - _end = _passwordStrength * 100; - - _strengthBarAnimation = Tween( - begin: _begin, - end: _end, - ).animate(_animationController); - _strengthBarColor = - _strengthBarColors.evaluate( - AlwaysStoppedAnimation(_passwordStrength), - ) ?? - Colors.transparent; - - _animationController.forward(from: 0.0); - - if (_strengthCallback != null) { - _strengthCallback!(_passwordStrength); - } - } - - @override - void dispose() { - super.dispose(); - _animationController.dispose(); - } - - @override - void didUpdateWidget(FlutterPasswordStrength oldWidget) { - super.didUpdateWidget(oldWidget); - - if (oldWidget.password != widget.password) { - animate(); - } - } - - @override - Widget build(BuildContext context) { - return StrengthBarContainer( - barColor: _strengthBarColor, - backgroundColor: _backgroundColor, - width: _width, - height: _height, - radius: _radius, - animation: _strengthBarAnimation, - ); - } -} - -class StrengthBarContainer extends AnimatedWidget { - final Color barColor; - final Color backgroundColor; - final double? width; - final double height; - final double radius; - - const StrengthBarContainer({ - super.key, - required this.barColor, - required this.backgroundColor, - this.width, - required this.height, - required this.radius, - required Animation animation, - }) : super(listenable: animation); - - Animation get _percent { - return listenable as Animation; - } - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return CustomPaint( - size: Size(width ?? constraints.maxWidth, height), - painter: StrengthBarBackground( - backgroundColor: backgroundColor, - backgroundRadius: radius, - ), - foregroundPainter: StrengthBar( - barColor: barColor, - barRadius: radius, - percent: _percent.value, - ), - ); - }, - ); - } -} - -class StrengthBar extends CustomPainter { - Color barColor; - double barRadius; - double percent; - - StrengthBar({ - required this.barColor, - required this.barRadius, - required this.percent, - }); - - @override - void paint(Canvas canvas, Size size) { - drawBar(canvas, size); - } - - void drawBar(Canvas canvas, Size size) { - Paint paint = Paint() - ..color = barColor - ..style = PaintingStyle.fill - ..strokeCap = StrokeCap.round; - - double left = 0; - double top = 0; - double right = size.width / 100 * percent; - double bottom = size.height; - - //the bar width needs to be bigger than radius width - if (barRadius != 0 && right > 0 && barRadius * 2 > right) { - right = barRadius * 2; - } - - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromLTRB(left, top, right, bottom), - Radius.circular(barRadius), - ), - paint, - ); - } - - @override - bool shouldRepaint(StrengthBar oldDelegate) { - return oldDelegate.percent != percent; - } -} - -class StrengthBarBackground extends CustomPainter { - Color backgroundColor; - double? backgroundRadius; - - StrengthBarBackground({required this.backgroundColor, this.backgroundRadius}); - - @override - void paint(Canvas canvas, Size size) { - drawBarBackground(canvas, size); - } - - void drawBarBackground(Canvas canvas, Size size) { - Paint paint = Paint() - ..color = backgroundColor - ..style = PaintingStyle.fill - ..strokeCap = StrokeCap.round; - - double left = 0; - double top = 0; - double right = size.width; - double bottom = size.height; - - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromLTRB(left, top, right, bottom), - Radius.circular(backgroundRadius ?? 0), - ), - paint, - ); - } - - @override - bool shouldRepaint(StrengthBarBackground oldDelegate) { - return true; - } -} diff --git a/lib/settings/ui/pages/change_pass/test_entry_style.dart b/lib/settings/ui/pages/change_pass/test_entry_style.dart deleted file mode 100644 index 60773f2ecc..0000000000 --- a/lib/settings/ui/pages/change_pass/test_entry_style.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:titan/tools/constants.dart'; - -InputDecoration changePassInputDecoration({ - required String hintText, - required ValueNotifier notifier, -}) { - return InputDecoration( - contentPadding: const EdgeInsets.symmetric(vertical: 18.0), - hintStyle: TextStyle(fontSize: 18, color: Colors.grey.shade400), - hintText: hintText, - focusedBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: ColorConstants.gradient1), - ), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide(color: Colors.grey.shade600), - ), - errorBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: ColorConstants.background2), - ), - focusedErrorBorder: const UnderlineInputBorder( - borderSide: BorderSide(width: 2.0, color: ColorConstants.gradient2), - ), - errorStyle: const TextStyle(color: ColorConstants.background2), - suffixIcon: IconButton( - icon: Icon( - notifier.value ? Icons.visibility : Icons.visibility_off, - color: Colors.grey.shade600, - ), - onPressed: () { - notifier.value = !notifier.value; - }, - ), - ); -} diff --git a/lib/settings/ui/pages/edit_user_page/edit_user_page.dart b/lib/settings/ui/pages/edit_user_page/edit_user_page.dart deleted file mode 100644 index 34d40239f1..0000000000 --- a/lib/settings/ui/pages/edit_user_page/edit_user_page.dart +++ /dev/null @@ -1,406 +0,0 @@ -import 'package:auto_size_text/auto_size_text.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:titan/settings/router.dart'; -import 'package:titan/settings/tools/constants.dart'; -import 'package:titan/settings/ui/pages/edit_user_page/picture_button.dart'; -import 'package:titan/settings/ui/pages/edit_user_page/user_field_modifier.dart'; -import 'package:titan/settings/ui/settings.dart'; -import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:titan/tools/ui/layouts/add_edit_button_layout.dart'; -import 'package:titan/tools/ui/widgets/align_left_text.dart'; -import 'package:titan/tools/ui/builders/async_child.dart'; -import 'package:titan/tools/ui/layouts/refresher.dart'; -import 'package:titan/tools/token_expire_wrapper.dart'; -import 'package:titan/tools/ui/builders/waiting_button.dart'; -import 'package:titan/tools/ui/widgets/text_entry.dart'; -import 'package:titan/user/class/floors.dart'; -import 'package:titan/user/providers/user_provider.dart'; -import 'package:titan/user/providers/profile_picture_provider.dart'; -import 'package:qlevar_router/qlevar_router.dart'; - -class EditUserPage extends HookConsumerWidget { - const EditUserPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final key = GlobalKey(); - final asyncUserNotifier = ref.watch(asyncUserProvider.notifier); - final user = ref.watch(userProvider); - final profilePicture = ref.watch(profilePictureProvider); - final profilePictureNotifier = ref.watch(profilePictureProvider.notifier); - final dateController = useTextEditingController( - text: user.birthday != null ? processDate(user.birthday!) : "", - ); - final nickNameController = useTextEditingController( - text: user.nickname ?? '', - ); - final phoneController = useTextEditingController(text: user.phone ?? ''); - final floorController = useTextEditingController( - text: user.floor.toString(), - ); - - void displayToastWithContext(TypeMsg type, String msg) { - displayToast(context, type, msg); - } - - List items = Floors.values - .map( - (e) => DropdownMenuItem( - value: capitalize(e.toString().split('.').last), - child: Text(capitalize(e.toString().split('.').last)), - ), - ) - .toList(); - - return SettingsTemplate( - child: Refresher( - onRefresh: () async { - await asyncUserNotifier.loadMe(); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 30.0), - child: Form( - key: key, - child: Column( - children: [ - const SizedBox(height: 20), - const AlignLeftText( - SettingsTextConstants.editAccount, - color: Colors.grey, - ), - const SizedBox(height: 40), - AsyncChild( - value: profilePicture, - builder: (context, profile) { - return Center( - child: Stack( - clipBehavior: Clip.none, - children: [ - Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.1), - spreadRadius: 5, - blurRadius: 10, - offset: const Offset(2, 3), - ), - ], - ), - child: CircleAvatar( - radius: 80, - backgroundImage: profile.isEmpty - ? const AssetImage( - 'assets/images/profile.png', - ) - : Image.memory(profile).image, - ), - ), - Positioned( - bottom: 0, - left: 0, - child: GestureDetector( - onTap: () async { - final value = await profilePictureNotifier - .setProfilePicture(ImageSource.gallery); - if (value != null) { - if (value) { - displayToastWithContext( - TypeMsg.msg, - SettingsTextConstants - .updatedProfilePicture, - ); - } else { - displayToastWithContext( - TypeMsg.error, - SettingsTextConstants - .tooHeavyProfilePicture, - ); - } - } else { - displayToastWithContext( - TypeMsg.error, - SettingsTextConstants.errorProfilePicture, - ); - } - }, - child: const PictureButton(icon: HeroIcons.photo), - ), - ), - Positioned( - bottom: 0, - right: 0, - child: GestureDetector( - onTap: () async { - final value = await profilePictureNotifier - .setProfilePicture(ImageSource.camera); - if (value != null) { - if (value) { - displayToastWithContext( - TypeMsg.msg, - SettingsTextConstants - .updatedProfilePicture, - ); - } else { - displayToastWithContext( - TypeMsg.error, - SettingsTextConstants - .tooHeavyProfilePicture, - ); - } - } else { - displayToastWithContext( - TypeMsg.error, - SettingsTextConstants.errorProfilePicture, - ); - } - }, - child: const PictureButton( - icon: HeroIcons.camera, - ), - ), - ), - Positioned( - bottom: -20, - right: 60, - child: GestureDetector( - onTap: () async { - final value = await profilePictureNotifier - .cropImage(); - if (value != null) { - if (value) { - displayToastWithContext( - TypeMsg.msg, - SettingsTextConstants - .updatedProfilePicture, - ); - } else { - displayToastWithContext( - TypeMsg.error, - SettingsTextConstants.errorProfilePicture, - ); - } - } - }, - child: const PictureButton( - icon: HeroIcons.sparkles, - ), - ), - ), - ], - ), - ); - }, - ), - const SizedBox(height: 50), - if (user.promo != null) - Container( - margin: const EdgeInsets.only(bottom: 20), - child: Text( - '${SettingsTextConstants.promo} ${user.promo}', - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - color: Colors.black, - ), - ), - ), - AutoSizeText( - '${SettingsTextConstants.email} : ${user.email}', - maxLines: 1, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - color: Colors.black, - ), - ), - const SizedBox(height: 50), - UserFieldModifier( - label: SettingsTextConstants.nickname, - keyboardType: TextInputType.text, - controller: nickNameController, - ), - const SizedBox(height: 50), - UserFieldModifier( - label: SettingsTextConstants.phone, - keyboardType: TextInputType.text, - controller: phoneController, - ), - const SizedBox(height: 50), - Row( - children: [ - SizedBox( - width: 120, - child: Text( - SettingsTextConstants.birthday, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w400, - color: Colors.grey.shade500, - ), - ), - ), - Expanded( - child: AbsorbPointer( - child: TextEntry( - label: SettingsTextConstants.birthday, - controller: dateController, - ), - ), - ), - GestureDetector( - onTap: () => getOnlyDayDate( - context, - dateController, - initialDate: user.birthday, - firstDate: DateTime(1900), - lastDate: DateTime.now(), - ), - child: Container( - margin: const EdgeInsets.only(left: 30), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [ - ColorConstants.gradient1, - ColorConstants.gradient2, - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - boxShadow: [ - BoxShadow( - color: ColorConstants.gradient2.withValues( - alpha: 0.5, - ), - spreadRadius: 1, - blurRadius: 7, - offset: const Offset(0, 3), - ), - ], - borderRadius: BorderRadius.circular(10), - ), - child: const HeroIcon( - HeroIcons.calendar, - size: 25, - color: Colors.white, - ), - ), - ), - ], - ), - const SizedBox(height: 50), - Row( - children: [ - SizedBox( - width: 120, - child: Text( - SettingsTextConstants.floor, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w400, - color: Colors.grey.shade500, - ), - ), - ), - Expanded( - child: DropdownButtonFormField( - items: items, - value: floorController.text, - hint: Text( - SettingsTextConstants.floor, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w400, - color: Colors.grey.shade500, - ), - ), - onChanged: (value) { - floorController.text = value.toString(); - }, - style: TextStyle( - fontSize: 20, - color: Colors.grey.shade800, - ), - decoration: const InputDecoration( - contentPadding: EdgeInsets.all(10), - isDense: true, - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: ColorConstants.gradient1, - ), - ), - ), - ), - ), - ], - ), - const SizedBox(height: 50), - WaitingButton( - builder: (child) => AddEditButtonLayout( - colors: const [ - ColorConstants.gradient1, - ColorConstants.gradient2, - ], - child: child, - ), - onTap: () async { - await tokenExpireWrapper(ref, () async { - final value = await asyncUserNotifier.updateMe( - user.copyWith( - birthday: dateController.value.text.isNotEmpty - ? DateTime.parse( - processDateBack(dateController.value.text), - ) - : null, - nickname: nickNameController.value.text.isEmpty - ? null - : nickNameController.value.text, - phone: phoneController.value.text.isEmpty - ? null - : phoneController.value.text, - floor: floorController.value.text, - ), - ); - if (value) { - displayToastWithContext( - TypeMsg.msg, - SettingsTextConstants.updatedProfile, - ); - QR.removeNavigator( - SettingsRouter.root + SettingsRouter.editAccount, - ); - } else { - displayToastWithContext( - TypeMsg.error, - SettingsTextConstants.updatingError, - ); - } - }); - }, - child: const Center( - child: Text( - SettingsTextConstants.save, - style: TextStyle( - fontSize: 25, - fontWeight: FontWeight.w400, - color: Colors.white, - ), - ), - ), - ), - const SizedBox(height: 30), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/settings/ui/pages/edit_user_page/user_field_modifier.dart b/lib/settings/ui/pages/edit_user_page/user_field_modifier.dart deleted file mode 100644 index 43220bf684..0000000000 --- a/lib/settings/ui/pages/edit_user_page/user_field_modifier.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/ui/widgets/text_entry.dart'; - -class UserFieldModifier extends StatelessWidget { - final String label; - final TextInputType keyboardType; - final TextEditingController controller; - const UserFieldModifier({ - super.key, - required this.label, - required this.keyboardType, - required this.controller, - }); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - SizedBox( - width: 120, - child: Text( - label, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w400, - color: Colors.grey.shade500, - ), - ), - ), - Expanded( - child: TextEntry( - label: label, - keyboardType: keyboardType, - controller: controller, - color: ColorConstants.gradient1, - isInt: keyboardType == TextInputType.number, - ), - ), - ], - ); - } -} diff --git a/lib/settings/ui/pages/log_page/log_card.dart b/lib/settings/ui/pages/log_page/log_card.dart deleted file mode 100644 index d601395b61..0000000000 --- a/lib/settings/ui/pages/log_page/log_card.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:flutter/services.dart'; -import 'package:flutter/material.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:titan/settings/tools/constants.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:titan/tools/logs/log.dart'; - -class LogCard extends StatelessWidget { - final Log log; - const LogCard({super.key, required this.log}); - - @override - Widget build(BuildContext context) { - List colors = log.level == LogLevel.debug - ? [const Color(0xff00c3ff), const Color(0xff0077ff)] - : log.level == LogLevel.info - ? [const Color(0xff549227), const Color(0xFF3E721A)] - : log.level == LogLevel.warning - ? [const Color(0xfffc9a01), const Color(0xffee8300)] - : log.level == LogLevel.error - ? [const Color(0xffc72c41), const Color(0xff801336)] - : [ - const Color.fromARGB(255, 198, 190, 21), - const Color.fromARGB(255, 187, 178, 14), - ]; - - Color color = colors[0]; - - return Container( - width: double.infinity, - margin: const EdgeInsets.symmetric(vertical: 10), - padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 20), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: colors, - ), - boxShadow: [ - BoxShadow( - color: color.withValues(alpha: 0.2), - spreadRadius: 5, - blurRadius: 10, - offset: const Offset(3, 3), // changes position of shadow - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - log.time.toString(), - style: const TextStyle( - fontSize: 16, - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - GestureDetector( - onTap: () { - Clipboard.setData(ClipboardData(text: log.message)); - displayToast( - context, - TypeMsg.msg, - SettingsTextConstants.copied, - ); - }, - child: const HeroIcon(HeroIcons.clipboard, color: Colors.white), - ), - ], - ), - const SizedBox(height: 10), - Text( - log.message, - style: const TextStyle(fontSize: 12, color: Colors.white), - ), - ], - ), - ); - } -} diff --git a/lib/settings/ui/pages/log_page/log_page.dart b/lib/settings/ui/pages/log_page/log_page.dart deleted file mode 100644 index e13f9664ee..0000000000 --- a/lib/settings/ui/pages/log_page/log_page.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/settings/providers/logs_provider.dart'; -import 'package:titan/settings/providers/logs_tab_provider.dart'; -import 'package:titan/settings/ui/pages/log_page/log_tab.dart'; -import 'package:titan/settings/ui/pages/log_page/notification_tab.dart'; -import 'package:titan/settings/ui/settings.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; -import 'package:titan/tools/ui/layouts/item_chip.dart'; -import 'package:titan/tools/ui/layouts/refresher.dart'; - -class LogPage extends HookConsumerWidget { - const LogPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final logsNotifier = ref.watch(logsProvider.notifier); - final notificationLogsNotifier = ref.watch( - notificationLogsProvider.notifier, - ); - final logTab = ref.watch(logTabProvider); - final logTabNotifier = ref.read(logTabProvider.notifier); - - Widget getTab(LogTabs tab) { - switch (tab) { - case LogTabs.log: - return const LogTab(); - case LogTabs.notification: - return const NotificationTab(); - } - } - - return SettingsTemplate( - child: Refresher( - onRefresh: () async { - await logsNotifier.getLogs(); - await notificationLogsNotifier.getLogs(); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 30.0), - child: Column( - children: [ - const SizedBox(height: 20), - HorizontalListView.builder( - height: 40, - items: LogTabs.values, - itemBuilder: (context, item, i) => GestureDetector( - onTap: () { - logTabNotifier.setLogTabs(item); - }, - child: ItemChip( - selected: logTab == item, - child: Text( - capitalize(item.name), - style: TextStyle( - color: logTab == item ? Colors.white : Colors.black, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ), - const SizedBox(height: 10), - getTab(logTab), - ], - ), - ), - ), - ); - } -} diff --git a/lib/settings/ui/pages/log_page/log_tab.dart b/lib/settings/ui/pages/log_page/log_tab.dart deleted file mode 100644 index c59eb9182a..0000000000 --- a/lib/settings/ui/pages/log_page/log_tab.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/settings/providers/logs_provider.dart'; -import 'package:titan/settings/tools/constants.dart'; -import 'package:titan/settings/ui/pages/log_page/log_card.dart'; -import 'package:titan/tools/ui/builders/async_child.dart'; -import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; - -class LogTab extends HookConsumerWidget { - const LogTab({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final logs = ref.watch(logsProvider); - final logsNotifier = ref.watch(logsProvider.notifier); - return Column( - children: [ - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - SettingsTextConstants.logs, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.grey, - ), - ), - GestureDetector( - onTap: () { - showDialog( - context: context, - builder: ((context) => CustomDialogBox( - title: SettingsTextConstants.deleting, - descriptions: SettingsTextConstants.deleteLogs, - onYes: (() async { - logsNotifier.deleteLogs(); - }), - )), - ); - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 12, - ), - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.2), - blurRadius: 10, - offset: const Offset(0, 5), - ), - ], - ), - child: const Row( - children: [ - HeroIcon(HeroIcons.trash, color: Colors.white, size: 20), - ], - ), - ), - ), - ], - ), - const SizedBox(height: 20), - AsyncChild( - value: logs, - builder: (context, data) => - Column(children: data.map((e) => LogCard(log: e)).toList()), - ), - const SizedBox(height: 20), - ], - ); - } -} diff --git a/lib/settings/ui/pages/log_page/notification_tab.dart b/lib/settings/ui/pages/log_page/notification_tab.dart deleted file mode 100644 index f7da8d2f1f..0000000000 --- a/lib/settings/ui/pages/log_page/notification_tab.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/settings/providers/logs_provider.dart'; -import 'package:titan/settings/tools/constants.dart'; -import 'package:titan/settings/ui/pages/log_page/log_card.dart'; -import 'package:titan/tools/ui/builders/async_child.dart'; -import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; - -class NotificationTab extends HookConsumerWidget { - const NotificationTab({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final logs = ref.watch(notificationLogsProvider); - final logsNotifier = ref.watch(notificationLogsProvider.notifier); - return Column( - children: [ - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - SettingsTextConstants.notifications, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.grey, - ), - ), - GestureDetector( - onTap: () { - showDialog( - context: context, - builder: ((context) => CustomDialogBox( - title: SettingsTextConstants.deleting, - descriptions: SettingsTextConstants.deleteNotificationLogs, - onYes: (() async { - logsNotifier.deleteLogs(); - }), - )), - ); - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 12, - ), - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.2), - blurRadius: 10, - offset: const Offset(0, 5), - ), - ], - ), - child: const Row( - children: [ - HeroIcon(HeroIcons.trash, color: Colors.white, size: 20), - ], - ), - ), - ), - ], - ), - const SizedBox(height: 20), - AsyncChild( - value: logs, - builder: (context, data) => - Column(children: data.map((e) => LogCard(log: e)).toList()), - ), - const SizedBox(height: 20), - ], - ); - } -} diff --git a/lib/settings/ui/pages/main_page/edit_profile.dart b/lib/settings/ui/pages/main_page/edit_profile.dart new file mode 100644 index 0000000000..a1248e3ef8 --- /dev/null +++ b/lib/settings/ui/pages/main_page/edit_profile.dart @@ -0,0 +1,238 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/settings/ui/pages/main_page/picture_button.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; +import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; +import 'package:titan/tools/ui/styleguide/text_entry.dart'; +import 'package:titan/tools/ui/widgets/date_entry.dart'; +import 'package:titan/user/providers/profile_picture_provider.dart'; +import 'package:titan/user/providers/user_provider.dart'; + +class EditProfile extends HookConsumerWidget { + const EditProfile({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final locale = Localizations.localeOf(context); + final me = ref.watch(userProvider); + final profilePictureNotifier = ref.watch(profilePictureProvider.notifier); + final profilePicture = ref.watch(profilePictureProvider); + final asyncUserNotifier = ref.watch(asyncUserProvider.notifier); + void displayToastWithContext(TypeMsg type, String msg) { + displayToast(context, type, msg); + } + + final emailController = useTextEditingController(text: me.email); + final phoneController = useTextEditingController(text: me.phone ?? ''); + final birthdayController = useTextEditingController( + text: me.birthday != null + ? "${me.birthday!.day.toString().padLeft(2, '0')}/${me.birthday!.month.toString().padLeft(2, '0')}/${me.birthday!.year}" + : '', + ); + + MediaQuery.of(context).viewInsets.bottom; + + final localizeWithContext = AppLocalizations.of(context)!; + final navigatorWithContext = Navigator.of(context); + + return SingleChildScrollView( + child: Column( + children: [ + if (View.of(context).viewInsets.bottom == 0) + AsyncChild( + value: profilePicture, + builder: (context, profile) { + return Center( + child: Stack( + clipBehavior: Clip.none, + children: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + spreadRadius: 5, + blurRadius: 10, + offset: const Offset(2, 3), + ), + ], + ), + child: CircleAvatar( + radius: 80, + backgroundImage: profile.isEmpty + ? const AssetImage('assets/images/profile.png') + : Image.memory(profile).image, + ), + ), + Positioned( + bottom: 0, + left: 0, + child: GestureDetector( + onTap: () async { + final value = await profilePictureNotifier + .setProfilePicture(ImageSource.gallery); + if (value != null) { + if (value) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext + .settingsUpdatedProfilePicture, + ); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext + .settingsTooHeavyProfilePicture, + ); + } + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.settingsErrorProfilePicture, + ); + } + }, + child: const PictureButton(icon: HeroIcons.photo), + ), + ), + Positioned( + bottom: 0, + right: 0, + child: GestureDetector( + onTap: () async { + final value = await profilePictureNotifier + .setProfilePicture(ImageSource.camera); + if (value != null) { + if (value) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext + .settingsUpdatedProfilePicture, + ); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext + .settingsTooHeavyProfilePicture, + ); + } + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.settingsErrorProfilePicture, + ); + } + }, + child: const PictureButton(icon: HeroIcons.camera), + ), + ), + Positioned( + bottom: -20, + right: 60, + child: GestureDetector( + onTap: () async { + final value = await profilePictureNotifier + .cropImage(); + if (value != null) { + if (value) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext + .settingsUpdatedProfilePicture, + ); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext + .settingsErrorProfilePicture, + ); + } + } + }, + child: const PictureButton(icon: HeroIcons.sparkles), + ), + ), + ], + ), + ); + }, + ), + SizedBox(height: View.of(context).viewInsets.bottom == 0 ? 30 : 10), + TextEntry( + label: localizeWithContext.settingsEmail, + controller: emailController, + enabled: false, + ), + SizedBox(height: 10), + TextEntry( + label: localizeWithContext.settingsPhoneNumber, + controller: phoneController, + textInputAction: TextInputAction.done, + ), + SizedBox(height: 10), + DateEntry( + label: localizeWithContext.settingsBirthday, + controller: birthdayController, + onTap: () async { + await getOnlyDayDate( + context, + birthdayController, + initialDate: DateTime(2004), + firstDate: DateTime(1900), + lastDate: DateTime.now(), + ); + }, + ), + SizedBox(height: 30), + Button( + text: localizeWithContext.settingsValidate, + disabled: + !(phoneController.value.text != me.phone || + birthdayController.value.text != + "${me.birthday!.day.toString().padLeft(2, '0')}/${me.birthday!.month.toString().padLeft(2, '0')}/${me.birthday!.year}"), + onPressed: () async { + if (phoneController.value.text != me.phone || + birthdayController.value.text.isNotEmpty) { + await tokenExpireWrapper(ref, () async { + final newMe = me.copyWith( + birthday: birthdayController.value.text.isNotEmpty + ? DateTime.parse( + processDateBack( + birthdayController.value.text, + locale.toString(), + ), + ) + : null, + phone: phoneController.value.text.isEmpty + ? null + : phoneController.value.text, + ); + final value = await asyncUserNotifier.updateMe(newMe); + if (value) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.settingsEditedAccount, + ); + navigatorWithContext.pop(); + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.settingsFailedToEditAccount, + ); + } + }); + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/settings/ui/pages/main_page/load_switch_topic.dart b/lib/settings/ui/pages/main_page/load_switch_topic.dart new file mode 100644 index 0000000000..b8acc586ad --- /dev/null +++ b/lib/settings/ui/pages/main_page/load_switch_topic.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:load_switch/load_switch.dart'; +import 'package:titan/settings/class/notification_topic.dart'; +import 'package:titan/settings/providers/notification_topic_provider.dart'; + +class LoadSwitchTopic extends ConsumerWidget { + const LoadSwitchTopic({super.key, required this.notificationTopic}); + final NotificationTopic notificationTopic; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final notificationTopicListNotifier = ref.watch( + notificationTopicListProvider.notifier, + ); + return LoadSwitch( + value: notificationTopic.isUserSubscribed, + future: () async { + await notificationTopicListNotifier.toggleSubscription( + notificationTopic, + ); + return !notificationTopic.isUserSubscribed; + }, + height: 30, + width: 60, + curveIn: Curves.easeInBack, + curveOut: Curves.easeOutBack, + animationDuration: const Duration(milliseconds: 500), + switchDecoration: (value, _) => BoxDecoration( + color: value ? Colors.red.withValues(alpha: 0.3) : Colors.grey.shade200, + borderRadius: BorderRadius.circular(30), + shape: BoxShape.rectangle, + boxShadow: [ + BoxShadow( + color: value + ? Colors.red.withValues(alpha: 0.2) + : Colors.grey.withValues(alpha: 0.2), + spreadRadius: 1, + blurRadius: 3, + offset: const Offset(0, 1), + ), + ], + ), + spinColor: (value) => value ? Colors.red : Colors.grey, + spinStrokeWidth: 2, + thumbDecoration: (value, _) => BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(30), + shape: BoxShape.rectangle, + boxShadow: [ + BoxShadow( + color: value + ? Colors.red.withValues(alpha: 0.2) + : Colors.grey.shade200.withValues(alpha: 0.2), + spreadRadius: 5, + blurRadius: 7, + offset: const Offset(0, 3), + ), + ], + ), + onChange: (v) {}, + onTap: (v) {}, + ); + } +} diff --git a/lib/settings/ui/pages/main_page/main_page.dart b/lib/settings/ui/pages/main_page/main_page.dart index 77765f501b..7f42bf4f02 100644 --- a/lib/settings/ui/pages/main_page/main_page.dart +++ b/lib/settings/ui/pages/main_page/main_page.dart @@ -4,309 +4,448 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/flappybird/ui/flappybird_item_chip.dart'; -import 'package:titan/settings/providers/logs_provider.dart'; -import 'package:titan/settings/router.dart'; -import 'package:titan/settings/tools/constants.dart'; -import 'package:titan/settings/ui/pages/main_page/settings_item.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/auth/providers/openid_provider.dart'; +import 'package:titan/login/router.dart'; +import 'package:titan/service/providers/firebase_token_expiration_provider.dart'; +import 'package:titan/service/providers/messages_provider.dart'; +import 'package:titan/tools/providers/path_forwarding_provider.dart'; +import 'package:titan/tools/ui/styleguide/confirm_modal.dart'; +import 'package:titan/tools/ui/widgets/vertical_clip_scroll.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/settings/providers/notification_topic_provider.dart'; +import 'package:titan/settings/tools/functions.dart'; +import 'package:titan/settings/ui/pages/main_page/edit_profile.dart'; +import 'package:titan/settings/ui/pages/main_page/load_switch_topic.dart'; + import 'package:titan/settings/ui/settings.dart'; -import 'package:titan/tools/ui/widgets/align_left_text.dart'; -import 'package:titan/tools/ui/builders/async_child.dart'; -import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; +import 'package:titan/tools/constants.dart'; import 'package:titan/tools/functions.dart'; -import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; -import 'package:titan/tools/ui/layouts/item_chip.dart'; -import 'package:titan/tools/ui/layouts/refresher.dart'; +import 'package:titan/tools/providers/locale_notifier.dart'; import 'package:titan/tools/repository/repository.dart'; -import 'package:titan/user/providers/user_provider.dart'; +import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/tools/ui/layouts/refresher.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/list_item.dart'; +import 'package:titan/tools/ui/styleguide/list_item_template.dart'; import 'package:titan/user/providers/profile_picture_provider.dart'; +import 'package:titan/user/providers/user_provider.dart'; import 'package:titan/version/providers/titan_version_provider.dart'; -import 'package:qlevar_router/qlevar_router.dart'; +import 'package:url_launcher/url_launcher.dart'; class SettingsMainPage extends HookConsumerWidget { const SettingsMainPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final me = ref.watch(userProvider); - final meNotifier = ref.watch(asyncUserProvider.notifier); final titanVersion = ref.watch(titanVersionProvider); - final profilePicture = ref.watch(profilePictureProvider); - ref.watch(logsProvider.notifier).getLogs(); - void displayToastWithContext(TypeMsg type, String msg) { displayToast(context, type, msg); } + final pathForwardingProviderNotifier = ref.read( + pathForwardingProvider.notifier, + ); + + final notificationTopicListNotifier = ref.watch( + notificationTopicListProvider.notifier, + ); + final notificationTopicList = ref.watch(notificationTopicListProvider); + final meNotifier = ref.watch(asyncUserProvider.notifier); + final profilePicture = ref.watch(profilePictureProvider); + final auth = ref.watch(authTokenProvider.notifier); + final isCachingNotifier = ref.watch(isCachingProvider.notifier); + + final results = notificationTopicList.when( + data: (data) { + final activatedCount = data + .where((topic) => topic.isUserSubscribed) + .length; + final totalCount = data.length; + return {'activatedCount': activatedCount, 'totalCount': totalCount}; + }, + loading: () => {'activatedCount': 0, 'totalCount': 0}, + error: (_, _) => {'activatedCount': 0, 'totalCount': 0}, + ); + + final notificationActivatedCounts = results['activatedCount']!; + final notificationTopicsLength = results['totalCount']!; + + final localizeWithContext = AppLocalizations.of(context)!; + + final selectedLanguage = localizeWithContext.settingsLanguageVar; + return SettingsTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { - await meNotifier.loadMe(); + await notificationTopicListNotifier.loadNotificationTopicList(); }, - child: Column( - children: [ - const SizedBox(height: 25), - AsyncChild( - value: profilePicture, - builder: (context, profile) { - return Center( - child: Stack( - clipBehavior: Clip.none, - children: [ - Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - spreadRadius: 6, - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: CircleAvatar( - radius: 70, - backgroundImage: profile.isEmpty - ? AssetImage(getTitanLogo()) - : Image.memory(profile).image, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + AsyncChild( + value: profilePicture, + builder: (context, profile) { + return Center( + child: Stack( + clipBehavior: Clip.none, + children: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + spreadRadius: 6, + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: CircleAvatar( + radius: 70, + backgroundImage: profile.isEmpty + ? AssetImage(getTitanLogo()) + : Image.memory(profile).image, + ), ), + ], + ), + ); + }, + errorBuilder: (e, s) => + const HeroIcon(HeroIcons.userCircle, size: 140), + ), + const SizedBox(height: 20), + Text( + localizeWithContext.settingsAccount, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorConstants.title, + ), + ), + ListItem( + title: localizeWithContext.settingsProfile, + subtitle: localizeWithContext.settingsEditAccount, + onTap: () async { + await showCustomBottomModal( + modal: BottomModalTemplate( + title: localizeWithContext.settingsEditAccount, + child: EditProfile(), + ), + context: context, + ref: ref, + ); + }, + ), + ListItem( + title: localizeWithContext.settingsLanguage, + subtitle: selectedLanguage, + onTap: () async { + await showCustomBottomModal( + modal: BottomModalTemplate( + title: localizeWithContext.settingsChooseLanguage, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListItemTemplate( + title: "🇫🇷 Français", + onTap: () async { + Navigator.of(context).pop(); + await ref + .read(localeProvider.notifier) + .setLocale(const Locale('fr')); + }, + trailing: + ref.watch(localeProvider)?.languageCode == 'fr' + ? const HeroIcon( + HeroIcons.check, + color: ColorConstants.tertiary, + ) + : Container(), + ), + const SizedBox(height: 10), + ListItemTemplate( + title: "🇬🇧 English", + onTap: () async { + Navigator.of(context).pop(); + await ref + .read(localeProvider.notifier) + .setLocale(const Locale('en')); + }, + trailing: + ref.watch(localeProvider)?.languageCode == 'en' + ? const HeroIcon( + HeroIcons.check, + color: ColorConstants.tertiary, + ) + : Container(), + ), + const SizedBox(height: 20), + ], ), - Positioned( - top: 0, - left: -MediaQuery.of(context).size.width / 2 + 70, - child: Column( - children: [ - const SizedBox(height: 125), - Container( - width: MediaQuery.of(context).size.width, - decoration: BoxDecoration( - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.white.withValues(alpha: 0.5), - spreadRadius: 5, - blurRadius: 10, - offset: const Offset(-2, -3), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Column( + ), + context: context, + ref: ref, + ); + }, + ), + ListItem( + title: localizeWithContext.settingsNotifications, + subtitle: localizeWithContext.settingsNotificationCounter( + notificationActivatedCounts, + notificationTopicsLength, + ), + onTap: () async { + await showCustomBottomModal( + modal: BottomModalTemplate( + title: localizeWithContext.settingsNotifications, + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 600), + child: Consumer( + builder: (context, ref, child) { + final notificationTopicList = ref.watch( + notificationTopicListProvider, + ); + return AsyncChild( + value: notificationTopicList, + builder: (context, notificationTopicList) { + final notificationTopicsByModuleRoot = + groupNotificationTopicsByModuleRoot( + notificationTopicList, + ref, + context, + ); + final uniqueTopics = + notificationTopicsByModuleRoot['others'] ?? + []; + final groupedTopics = Map.from( + notificationTopicsByModuleRoot, + )..remove('others'); + return VerticalClipScroll( + child: Column( children: [ - const SizedBox(height: 8), - Text( - me.nickname ?? me.firstname, - style: const TextStyle( - fontSize: 25, - fontWeight: FontWeight.bold, + ...uniqueTopics.map( + (notificationTopic) => Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, + ), + child: ListItemTemplate( + title: notificationTopic.name, + trailing: LoadSwitchTopic( + notificationTopic: + notificationTopic, + ), + ), ), ), + ...groupedTopics.entries.map((entry) { + final moduleRoot = entry.key; + final topics = entry.value; + bool expanded = true; + return StatefulBuilder( + builder: (context, setState) { + return Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + Padding( + padding: + const EdgeInsets.symmetric( + vertical: 8.0, + ), + child: ListItemTemplate( + title: moduleRoot, + trailing: HeroIcon( + expanded + ? HeroIcons + .chevronDown + : HeroIcons + .chevronRight, + color: ColorConstants + .tertiary, + ), + onTap: () { + setState(() { + expanded = !expanded; + }); + }, + ), + ), + const SizedBox(height: 10), + if (expanded) + ...topics.map( + ( + notificationTopic, + ) => Padding( + padding: + const EdgeInsets.symmetric( + vertical: 8.0, + ), + child: ListItemTemplate( + title: notificationTopic + .name, + trailing: LoadSwitchTopic( + notificationTopic: + notificationTopic, + ), + ), + ), + ), + ], + ); + }, + ); + }), ], ), - const SizedBox(height: 3), - Text( - me.nickname != null - ? "${me.firstname} ${me.name}" - : me.name, - style: const TextStyle(fontSize: 20), - ), - ], - ), - ), - ], + ); + }, + ); + }, ), ), - ], - ), - ); - }, - errorBuilder: (e, s) => - const HeroIcon(HeroIcons.userCircle, size: 140), - ), - const SizedBox(height: 100), - HorizontalListView.builder( - height: 40, - items: me.groups, - itemBuilder: (context, item, i) => ItemChip( - selected: true, - child: Text( - capitalize(item.name), - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), + ), + context: context, + ref: ref, + ); + }, + ), + const SizedBox(height: 20), + Text( + localizeWithContext.settingsEvent, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorConstants.title, ), ), - lastChild: const FlappyBirdItemChip(), - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 30.0), - child: Column( - children: [ - const AlignLeftText( - SettingsTextConstants.account, - fontSize: 25, - ), - const SizedBox(height: 30), - SettingsItem( - icon: HeroIcons.pencil, - onTap: () { - QR.to(SettingsRouter.root + SettingsRouter.editAccount); - }, - child: const Text( - SettingsTextConstants.editAccount, - style: TextStyle(fontSize: 16, color: Colors.black), - ), - ), - const SizedBox(height: 30), - SettingsItem( - icon: HeroIcons.calendarDays, - onTap: () { - Clipboard.setData( - ClipboardData(text: "${Repository.host}calendar/ical"), - ).then((value) { - displayToastWithContext( + ListItemTemplate( + title: localizeWithContext.settingsIcal, + subtitle: localizeWithContext.settingsSynncWithCalendar, + trailing: const HeroIcon( + HeroIcons.clipboardDocumentList, + color: ColorConstants.tertiary, + ), + onTap: () { + Clipboard.setData( + ClipboardData(text: "${Repository.host}calendar/ical"), + ).then((value) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.settingsIcalCopied, + ); + }); + }, + ), + const SizedBox(height: 20), + Text( + localizeWithContext.settingsConnexion, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorConstants.title, + ), + ), + const SizedBox(height: 10), + ListItem( + title: localizeWithContext.settingsChangePassword, + onTap: () async { + await launchUrl( + Uri.parse("${getTitanHost()}calypsso/change-password"), + ); + }, + ), + const SizedBox(height: 10), + ListItem( + title: localizeWithContext.settingsLogOut, + onTap: () async { + await showCustomBottomModal( + ref: ref, + context: context, + modal: ConfirmModal( + description: + localizeWithContext.settingsLogOutDescription, + title: localizeWithContext.settingsLogOut, + onYes: () { + auth.deleteToken(); + if (!kIsWeb) { + ref.watch(messagesProvider.notifier).forgetDevice(); + ref + .watch(firebaseTokenExpirationProvider.notifier) + .reset(); + } + isCachingNotifier.set(false); + pathForwardingProviderNotifier.reset(); + QR.to(LoginRouter.root); + displayToast( + context, TypeMsg.msg, - SettingsTextConstants.icalCopied, + localizeWithContext.settingsLogOutSuccess, ); - }); - }, - child: const Text( - SettingsTextConstants.eventsIcal, - style: TextStyle(fontSize: 16, color: Colors.black), - ), - ), - const SizedBox(height: 50), - const AlignLeftText( - SettingsTextConstants.security, - fontSize: 25, - ), - const SizedBox(height: 30), - SettingsItem( - icon: HeroIcons.lockClosed, - onTap: () { - QR.to( - SettingsRouter.root + SettingsRouter.changePassword, - ); - }, - child: const Text( - SettingsTextConstants.editPassword, - style: TextStyle(fontSize: 16, color: Colors.black), - ), - ), - const SizedBox(height: 50), - if (!kIsWeb) ...[ - const AlignLeftText( - SettingsTextConstants.help, - fontSize: 25, - ), - const SizedBox(height: 30), - SettingsItem( - icon: HeroIcons.clipboardDocumentList, - onTap: () { - QR.to(SettingsRouter.root + SettingsRouter.logs); }, - child: const Text( - SettingsTextConstants.logs, - style: TextStyle(fontSize: 16, color: Colors.black), - ), - ), - const SizedBox(height: 50), - ], - const AlignLeftText( - SettingsTextConstants.personalisation, - fontSize: 25, - ), - const SizedBox(height: 30), - SettingsItem( - icon: HeroIcons.queueList, - onTap: () { - QR.to(SettingsRouter.root + SettingsRouter.modules); - }, - child: const Text( - SettingsTextConstants.modules, - style: TextStyle(fontSize: 16, color: Colors.black), - ), - ), - const SizedBox(height: 30), - SettingsItem( - icon: HeroIcons.bellAlert, - onTap: () { - QR.to(SettingsRouter.root + SettingsRouter.notifications); - }, - child: const Text( - SettingsTextConstants.notifications, - style: TextStyle(fontSize: 16, color: Colors.black), ), - ), - const SizedBox(height: 50), - const AlignLeftText( - SettingsTextConstants.personalData, - fontSize: 25, - ), - const SizedBox(height: 30), - SettingsItem( - icon: HeroIcons.circleStack, - onTap: () async { - showDialog( - context: context, - builder: (BuildContext context) { - return CustomDialogBox( - title: SettingsTextConstants.detelePersonalData, - descriptions: - SettingsTextConstants.detelePersonalDataDesc, - onYes: () async { - final value = await meNotifier.deletePersonal(); - if (value) { - displayToastWithContext( - TypeMsg.msg, - SettingsTextConstants.sendedDemand, - ); - } else { - displayToastWithContext( - TypeMsg.error, - SettingsTextConstants.errorSendingDemand, - ); - } - }, + ); + }, + ), + const SizedBox(height: 10), + ListItem( + title: localizeWithContext.settingsDeleteMyAccount, + onTap: () async { + await showCustomBottomModal( + context: context, + ref: ref, + modal: ConfirmModal.danger( + description: localizeWithContext + .settingsDeleteMyAccountDescription, + title: localizeWithContext.settingsDeleteMyAccount, + onYes: () async { + final value = await meNotifier.deletePersonal(); + if (value) { + displayToastWithContext( + TypeMsg.msg, + localizeWithContext.settingsDeletionAsked, ); - }, - ); - }, - child: const Text( - SettingsTextConstants.detelePersonalData, - style: TextStyle(fontSize: 16, color: Colors.black), - ), - ), - const SizedBox(height: 60), - Text( - "${SettingsTextConstants.version} $titanVersion", - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w500, - color: Colors.black, - ), - ), - const SizedBox(height: 10), - AutoSizeText( - Repository.host, - maxLines: 1, - minFontSize: 10, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w500, - color: Colors.black, + } else { + displayToastWithContext( + TypeMsg.error, + localizeWithContext.settingsDeleteMyAccountError, + ); + } + }, ), - ), - const SizedBox(height: 20), - ], + ); + }, + ), + const SizedBox(height: 60), + Text( + "${localizeWithContext.othersVersion} $titanVersion", + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + ), + const SizedBox(height: 10), + AutoSizeText( + Repository.host, + maxLines: 1, + minFontSize: 10, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: Colors.black, + ), ), - ), - ], + const SizedBox(height: 20), + ], + ), ), ), ); diff --git a/lib/settings/ui/pages/edit_user_page/picture_button.dart b/lib/settings/ui/pages/main_page/picture_button.dart similarity index 85% rename from lib/settings/ui/pages/edit_user_page/picture_button.dart rename to lib/settings/ui/pages/main_page/picture_button.dart index 905238fb15..335a312b32 100644 --- a/lib/settings/ui/pages/edit_user_page/picture_button.dart +++ b/lib/settings/ui/pages/main_page/picture_button.dart @@ -15,13 +15,13 @@ class PictureButton extends StatelessWidget { decoration: BoxDecoration( shape: BoxShape.circle, gradient: const LinearGradient( - colors: [ColorConstants.gradient1, ColorConstants.gradient2], + colors: [ColorConstants.main, ColorConstants.onMain], begin: Alignment.topLeft, end: Alignment.bottomRight, ), boxShadow: [ BoxShadow( - color: ColorConstants.gradient2.withValues(alpha: 0.3), + color: ColorConstants.main.withValues(alpha: 0.3), spreadRadius: 2, blurRadius: 4, offset: const Offset(2, 3), diff --git a/lib/settings/ui/pages/main_page/settings_item.dart b/lib/settings/ui/pages/main_page/settings_item.dart deleted file mode 100644 index 4b26ad2bba..0000000000 --- a/lib/settings/ui/pages/main_page/settings_item.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:heroicons/heroicons.dart'; - -class SettingsItem extends StatelessWidget { - final Widget child; - final HeroIcons icon; - final void Function() onTap; - - const SettingsItem({ - super.key, - required this.icon, - required this.onTap, - required this.child, - }); - - @override - Widget build(BuildContext context) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: onTap, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(right: 20), - child: HeroIcon(icon, size: 30, color: Colors.black), - ), - Expanded(child: child), - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: Colors.white, - border: Border.all(color: Colors.black), - boxShadow: [ - BoxShadow( - color: Colors.grey.shade400.withValues(alpha: 0.3), - spreadRadius: 2, - blurRadius: 5, - offset: const Offset(2, 3), - ), - ], - borderRadius: BorderRadius.circular(10), - ), - child: const HeroIcon( - HeroIcons.chevronRight, - size: 25, - color: Colors.black, - ), - ), - ], - ), - ); - } -} diff --git a/lib/settings/ui/pages/modules_page/modules_page.dart b/lib/settings/ui/pages/modules_page/modules_page.dart deleted file mode 100644 index 4a2dbec914..0000000000 --- a/lib/settings/ui/pages/modules_page/modules_page.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/settings/providers/module_list_provider.dart'; -import 'package:titan/settings/ui/settings.dart'; - -class ModulesPage extends HookConsumerWidget { - const ModulesPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final modules = ref.watch(modulesProvider); - final modulesNotifier = ref.watch(modulesProvider.notifier); - return SettingsTemplate( - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 20), - child: ReorderableListView( - physics: const BouncingScrollPhysics(), - proxyDecorator: (child, index, animation) { - return Material( - child: FadeTransition(opacity: animation, child: child), - ); - }, - onReorder: (int oldIndex, int newIndex) { - modulesNotifier.reorderModules(oldIndex, newIndex); - }, - children: modulesNotifier.allModules.map((module) { - return Container( - margin: const EdgeInsets.all(10), - padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 5), - key: Key(module.root.toString()), - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - color: Colors.grey.withValues(alpha: 0.2), - spreadRadius: 5, - blurRadius: 10, - offset: const Offset(0, 2), // changes position of shadow - ), - ], - color: Colors.white, - borderRadius: BorderRadius.circular(15), - ), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 5), - child: Row( - children: [ - module.getIcon(Colors.grey.shade700), - const SizedBox(width: 20), - Text( - module.name, - style: const TextStyle( - color: Colors.black, - fontSize: 20, - fontWeight: FontWeight.w500, - ), - textAlign: TextAlign.left, - ), - const Spacer(), - Checkbox( - value: modules.contains(module), - activeColor: Colors.grey.shade700, - onChanged: (bool? value) { - modulesNotifier.toggleModule(module); - }, - ), - const HeroIcon(HeroIcons.chevronUpDown, size: 30), - ], - ), - ), - ); - }).toList(), - ), - ), - ); - } -} diff --git a/lib/settings/ui/pages/notification_page/notification_page.dart b/lib/settings/ui/pages/notification_page/notification_page.dart deleted file mode 100644 index 3912ae09f1..0000000000 --- a/lib/settings/ui/pages/notification_page/notification_page.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:load_switch/load_switch.dart'; -import 'package:titan/service/class/topic.dart'; -import 'package:titan/service/providers/topic_provider.dart'; -import 'package:titan/service/tools/functions.dart'; -import 'package:titan/settings/tools/constants.dart'; -import 'package:titan/settings/ui/settings.dart'; -import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/ui/widgets/align_left_text.dart'; -import 'package:titan/tools/ui/builders/async_child.dart'; -import 'package:titan/tools/ui/layouts/refresher.dart'; - -class NotificationPage extends HookConsumerWidget { - const NotificationPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final topics = ref.watch(topicsProvider); - final topicsNotifier = ref.read(topicsProvider.notifier); - return SettingsTemplate( - child: Refresher( - onRefresh: () async { - await topicsNotifier.getTopics(); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 30.0), - child: Column( - children: [ - const AlignLeftText( - SettingsTextConstants.updateNotification, - padding: EdgeInsets.symmetric(vertical: 30), - color: Colors.grey, - ), - AsyncChild( - value: topics, - builder: (context, topic) => Column( - children: Topic.values - .map( - (e) => Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - topicToFrenchString(e), - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.w700, - color: ColorConstants.background2, - ), - ), - LoadSwitch( - value: topic.contains(e), - future: () => - topicsNotifier.toggleSubscription(e), - height: 30, - width: 60, - curveIn: Curves.easeInBack, - curveOut: Curves.easeOutBack, - animationDuration: const Duration( - milliseconds: 500, - ), - switchDecoration: (value, _) => BoxDecoration( - color: value - ? ColorConstants.gradient1.withValues( - alpha: 0.3, - ) - : Colors.grey.shade200, - borderRadius: BorderRadius.circular(30), - shape: BoxShape.rectangle, - boxShadow: [ - BoxShadow( - color: value - ? ColorConstants.gradient1.withValues( - alpha: 0.2, - ) - : Colors.grey.withValues(alpha: 0.2), - spreadRadius: 1, - blurRadius: 3, - offset: const Offset(0, 1), - ), - ], - ), - spinColor: (value) => value - ? ColorConstants.gradient1 - : Colors.grey, - spinStrokeWidth: 2, - thumbDecoration: (value, _) => BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(30), - shape: BoxShape.rectangle, - boxShadow: [ - BoxShadow( - color: value - ? ColorConstants.gradient1.withValues( - alpha: 0.2, - ) - : Colors.grey.shade200.withValues( - alpha: 0.2, - ), - spreadRadius: 5, - blurRadius: 7, - offset: const Offset( - 0, - 3, - ), // changes position of shadow - ), - ], - ), - onChange: (v) {}, - onTap: (v) {}, - ), - ], - ), - ), - ) - .toList(), - ), - loaderColor: ColorConstants.gradient1, - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/settings/ui/settings.dart b/lib/settings/ui/settings.dart index d80b9f7c4e..9f0ee0c89e 100644 --- a/lib/settings/ui/settings.dart +++ b/lib/settings/ui/settings.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:titan/settings/router.dart'; -import 'package:titan/settings/tools/constants.dart'; import 'package:titan/tools/ui/widgets/top_bar.dart'; +import 'package:titan/tools/constants.dart'; class SettingsTemplate extends StatelessWidget { final Widget child; @@ -9,18 +9,17 @@ class SettingsTemplate extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - decoration: const BoxDecoration(color: Colors.white), - child: SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const TopBar( - title: SettingsTextConstants.settings, - root: SettingsRouter.root, - ), - Expanded(child: child), - ], + return Scaffold( + body: Container( + decoration: const BoxDecoration(color: ColorConstants.background), + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const TopBar(root: SettingsRouter.root), + Expanded(child: child), + ], + ), ), ), ); diff --git a/lib/admin/class/account_type.dart b/lib/super_admin/class/account_type.dart similarity index 100% rename from lib/admin/class/account_type.dart rename to lib/super_admin/class/account_type.dart diff --git a/lib/admin/class/module_visibility.dart b/lib/super_admin/class/module_visibility.dart similarity index 95% rename from lib/admin/class/module_visibility.dart rename to lib/super_admin/class/module_visibility.dart index 8e2ef1a441..b4f8452b8d 100644 --- a/lib/admin/class/module_visibility.dart +++ b/lib/super_admin/class/module_visibility.dart @@ -1,4 +1,4 @@ -import 'package:titan/admin/class/account_type.dart'; +import 'package:titan/super_admin/class/account_type.dart'; class ModuleVisibility { ModuleVisibility({ diff --git a/lib/admin/class/school.dart b/lib/super_admin/class/school.dart similarity index 88% rename from lib/admin/class/school.dart rename to lib/super_admin/class/school.dart index 47694bb973..db3b2c0dde 100644 --- a/lib/admin/class/school.dart +++ b/lib/super_admin/class/school.dart @@ -1,5 +1,3 @@ -import 'package:titan/admin/tools/function.dart'; - class School { School({required this.name, required this.id, required this.emailRegex}); late final String name; @@ -7,7 +5,7 @@ class School { late final String emailRegex; School.fromJson(Map json) { - name = getSchoolNameFromId(json['id'], json['name']); + name = json['name']; id = json['id']; emailRegex = json['email_regex']; } diff --git a/lib/admin/notification_service.dart b/lib/super_admin/notification_service.dart similarity index 81% rename from lib/admin/notification_service.dart rename to lib/super_admin/notification_service.dart index 42346e87a7..c49cf574dc 100644 --- a/lib/admin/notification_service.dart +++ b/lib/super_admin/notification_service.dart @@ -1,6 +1,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:titan/admin/providers/group_list_provider.dart'; -import 'package:titan/admin/router.dart'; +import 'package:titan/super_admin/router.dart'; import 'package:titan/router.dart'; import 'package:titan/user/providers/user_provider.dart'; import 'package:tuple/tuple.dart'; @@ -12,5 +12,5 @@ final Map>> adminProviders = allGroupListProvider, asyncUserProvider, ]), - "groups": Tuple2(AdminRouter.root, [allGroupListProvider]), + "groups": Tuple2(SuperAdminRouter.root, [allGroupListProvider]), }; diff --git a/lib/admin/providers/account_types_list_provider.dart b/lib/super_admin/providers/account_types_list_provider.dart similarity index 87% rename from lib/admin/providers/account_types_list_provider.dart rename to lib/super_admin/providers/account_types_list_provider.dart index c0b850faee..9f6c3e135b 100644 --- a/lib/admin/providers/account_types_list_provider.dart +++ b/lib/super_admin/providers/account_types_list_provider.dart @@ -1,6 +1,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:titan/admin/class/account_type.dart'; -import 'package:titan/admin/repositories/account_type_repository.dart'; +import 'package:titan/super_admin/class/account_type.dart'; +import 'package:titan/super_admin/repositories/account_type_repository.dart'; import 'package:titan/tools/providers/list_notifier.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; diff --git a/lib/admin/providers/all_account_types_list_provider.dart b/lib/super_admin/providers/all_account_types_list_provider.dart similarity index 62% rename from lib/admin/providers/all_account_types_list_provider.dart rename to lib/super_admin/providers/all_account_types_list_provider.dart index 51bd6ca7ff..2cc1986f90 100644 --- a/lib/admin/providers/all_account_types_list_provider.dart +++ b/lib/super_admin/providers/all_account_types_list_provider.dart @@ -1,6 +1,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:titan/admin/class/account_type.dart'; -import 'package:titan/admin/providers/account_types_list_provider.dart'; +import 'package:titan/super_admin/class/account_type.dart'; +import 'package:titan/super_admin/providers/account_types_list_provider.dart'; final allAccountTypes = Provider>((ref) { return ref diff --git a/lib/admin/providers/all_my_module_roots_list_provider.dart b/lib/super_admin/providers/all_my_module_roots_list_provider.dart similarity index 74% rename from lib/admin/providers/all_my_module_roots_list_provider.dart rename to lib/super_admin/providers/all_my_module_roots_list_provider.dart index 22aafc4a5b..f9a90bd7fd 100644 --- a/lib/admin/providers/all_my_module_roots_list_provider.dart +++ b/lib/super_admin/providers/all_my_module_roots_list_provider.dart @@ -1,5 +1,5 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:titan/admin/providers/module_root_list_provider.dart'; +import 'package:titan/super_admin/providers/module_root_list_provider.dart'; final allMyModuleRootList = Provider>((ref) { return ref diff --git a/lib/admin/providers/is_expanded_list_provider.dart b/lib/super_admin/providers/is_expanded_list_provider.dart similarity index 82% rename from lib/admin/providers/is_expanded_list_provider.dart rename to lib/super_admin/providers/is_expanded_list_provider.dart index f895def407..c2297526ac 100644 --- a/lib/admin/providers/is_expanded_list_provider.dart +++ b/lib/super_admin/providers/is_expanded_list_provider.dart @@ -1,6 +1,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:titan/admin/class/module_visibility.dart'; -import 'package:titan/admin/providers/module_visibility_list_provider.dart'; +import 'package:titan/super_admin/class/module_visibility.dart'; +import 'package:titan/super_admin/providers/module_visibility_list_provider.dart'; class IsExpandedListProvider extends StateNotifier> { IsExpandedListProvider(List modules) diff --git a/lib/super_admin/providers/is_super_admin_provider.dart b/lib/super_admin/providers/is_super_admin_provider.dart new file mode 100644 index 0000000000..91235065a4 --- /dev/null +++ b/lib/super_admin/providers/is_super_admin_provider.dart @@ -0,0 +1,7 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/user/providers/user_provider.dart'; + +final isSuperAdminProvider = StateProvider((ref) { + final me = ref.watch(userProvider); + return me.isSuperAdmin; +}); diff --git a/lib/admin/providers/members_provider.dart b/lib/super_admin/providers/members_provider.dart similarity index 100% rename from lib/admin/providers/members_provider.dart rename to lib/super_admin/providers/members_provider.dart diff --git a/lib/admin/providers/module_root_list_provider.dart b/lib/super_admin/providers/module_root_list_provider.dart similarity index 93% rename from lib/admin/providers/module_root_list_provider.dart rename to lib/super_admin/providers/module_root_list_provider.dart index fea7948193..e55fce6485 100644 --- a/lib/admin/providers/module_root_list_provider.dart +++ b/lib/super_admin/providers/module_root_list_provider.dart @@ -1,5 +1,5 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:titan/admin/repositories/module_visibility_repository.dart'; +import 'package:titan/super_admin/repositories/module_visibility_repository.dart'; import 'package:titan/auth/providers/openid_provider.dart'; import 'package:titan/tools/providers/list_notifier.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; diff --git a/lib/admin/providers/module_visibility_list_provider.dart b/lib/super_admin/providers/module_visibility_list_provider.dart similarity index 95% rename from lib/admin/providers/module_visibility_list_provider.dart rename to lib/super_admin/providers/module_visibility_list_provider.dart index 0f7d4b9615..5917c76b7e 100644 --- a/lib/admin/providers/module_visibility_list_provider.dart +++ b/lib/super_admin/providers/module_visibility_list_provider.dart @@ -1,6 +1,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:titan/admin/class/module_visibility.dart'; -import 'package:titan/admin/repositories/module_visibility_repository.dart'; +import 'package:titan/super_admin/class/module_visibility.dart'; +import 'package:titan/super_admin/repositories/module_visibility_repository.dart'; import 'package:titan/auth/providers/openid_provider.dart'; import 'package:titan/tools/providers/list_notifier.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; diff --git a/lib/admin/providers/school_list_provider.dart b/lib/super_admin/providers/school_list_provider.dart similarity index 93% rename from lib/admin/providers/school_list_provider.dart rename to lib/super_admin/providers/school_list_provider.dart index 3490e330b9..812647533b 100644 --- a/lib/admin/providers/school_list_provider.dart +++ b/lib/super_admin/providers/school_list_provider.dart @@ -1,6 +1,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:titan/admin/class/school.dart'; -import 'package:titan/admin/repositories/school_repository.dart'; +import 'package:titan/super_admin/class/school.dart'; +import 'package:titan/super_admin/repositories/school_repository.dart'; import 'package:titan/tools/providers/list_notifier.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; diff --git a/lib/admin/providers/school_provider.dart b/lib/super_admin/providers/school_provider.dart similarity index 79% rename from lib/admin/providers/school_provider.dart rename to lib/super_admin/providers/school_provider.dart index 4c0b5f0f6c..93022d8a27 100644 --- a/lib/admin/providers/school_provider.dart +++ b/lib/super_admin/providers/school_provider.dart @@ -1,6 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/class/school.dart'; -import 'package:titan/admin/repositories/school_repository.dart'; +import 'package:titan/super_admin/class/school.dart'; +import 'package:titan/super_admin/repositories/school_repository.dart'; class SchoolNotifier extends StateNotifier { final SchoolRepository schoolRepository; diff --git a/lib/admin/repositories/account_type_repository.dart b/lib/super_admin/repositories/account_type_repository.dart similarity index 91% rename from lib/admin/repositories/account_type_repository.dart rename to lib/super_admin/repositories/account_type_repository.dart index 9dff0fac15..c2fc1aa95c 100644 --- a/lib/admin/repositories/account_type_repository.dart +++ b/lib/super_admin/repositories/account_type_repository.dart @@ -1,5 +1,5 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:titan/admin/class/account_type.dart'; +import 'package:titan/super_admin/class/account_type.dart'; import 'package:titan/auth/providers/openid_provider.dart'; import 'package:titan/tools/repository/repository.dart'; diff --git a/lib/admin/repositories/module_visibility_repository.dart b/lib/super_admin/repositories/module_visibility_repository.dart similarity index 94% rename from lib/admin/repositories/module_visibility_repository.dart rename to lib/super_admin/repositories/module_visibility_repository.dart index 6d4e80b15f..08fcfb1770 100644 --- a/lib/admin/repositories/module_visibility_repository.dart +++ b/lib/super_admin/repositories/module_visibility_repository.dart @@ -1,4 +1,4 @@ -import 'package:titan/admin/class/module_visibility.dart'; +import 'package:titan/super_admin/class/module_visibility.dart'; import 'package:titan/tools/repository/repository.dart'; class ModuleVisibilityRepository extends Repository { diff --git a/lib/admin/repositories/school_repository.dart b/lib/super_admin/repositories/school_repository.dart similarity index 94% rename from lib/admin/repositories/school_repository.dart rename to lib/super_admin/repositories/school_repository.dart index ff0e55a6f1..814ccef216 100644 --- a/lib/admin/repositories/school_repository.dart +++ b/lib/super_admin/repositories/school_repository.dart @@ -1,5 +1,5 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:titan/admin/class/school.dart'; +import 'package:titan/super_admin/class/school.dart'; import 'package:titan/auth/providers/openid_provider.dart'; import 'package:titan/tools/repository/repository.dart'; diff --git a/lib/super_admin/router.dart b/lib/super_admin/router.dart new file mode 100644 index 0000000000..1ebec9c866 --- /dev/null +++ b/lib/super_admin/router.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/super_admin/providers/is_super_admin_provider.dart'; + +import 'package:titan/super_admin/ui/pages/edit_module_visibility/edit_module_visibility.dart' + deferred as edit_module_visibility; +import 'package:titan/super_admin/ui/pages/schools/school_page/school_page.dart' + deferred as school_page; +import 'package:titan/super_admin/ui/pages/schools/add_school_page/add_school_page.dart' + deferred as add_school_page; +import 'package:titan/super_admin/ui/pages/schools/edit_school_page/edit_school_page.dart' + deferred as edit_school_page; +import 'package:titan/super_admin/ui/pages/main_page/main_page.dart' + deferred as main_page; +import 'package:titan/navigation/class/module.dart'; +import 'package:titan/tools/middlewares/admin_middleware.dart'; +import 'package:titan/tools/middlewares/authenticated_middleware.dart'; +import 'package:titan/tools/middlewares/deferred_middleware.dart'; +import 'package:qlevar_router/qlevar_router.dart'; + +class SuperAdminRouter { + final Ref ref; + static const String root = '/super_admin'; + static const String groups = '/groups'; + static const String addGroup = '/add_group'; + static const String editGroup = '/edit_group'; + static const String addLoaner = '/add_loaner'; + static const String schools = '/schools'; + static const String addSchool = '/add_school'; + static const String editSchool = '/edit_school'; + static const String structures = '/structures'; + static const String addEditStructure = '/add_edit_structure'; + static const String editModuleVisibility = '/edit_module_visibility'; + static const String associationMemberships = '/association_memberships'; + static const String detailAssociationMembership = + '/detail_association_membership'; + static const String addEditMember = '/add_edit_member'; + static final Module module = Module( + getName: (context) => "Super Admin", + getDescription: (context) => "Super Admin", + root: SuperAdminRouter.root, + ); + SuperAdminRouter(this.ref); + + QRoute route() => QRoute( + name: "super_admin", + path: SuperAdminRouter.root, + builder: () => main_page.SuperAdminMainPage(), + middleware: [ + AuthenticatedMiddleware(ref), + AdminMiddleware(ref, isSuperAdminProvider), + DeferredLoadingMiddleware(main_page.loadLibrary), + ], + pageType: QCustomPage( + transitionsBuilder: (_, animation, _, child) => + FadeTransition(opacity: animation, child: child), + ), + children: [ + QRoute( + path: editModuleVisibility, + builder: () => edit_module_visibility.EditModulesVisibilityPage(), + middleware: [ + DeferredLoadingMiddleware(edit_module_visibility.loadLibrary), + ], + ), + QRoute( + path: schools, + builder: () => school_page.SchoolsPage(), + middleware: [DeferredLoadingMiddleware(school_page.loadLibrary)], + children: [ + QRoute( + path: addSchool, + builder: () => add_school_page.AddSchoolPage(), + middleware: [ + DeferredLoadingMiddleware(add_school_page.loadLibrary), + ], + ), + QRoute( + path: editSchool, + builder: () => edit_school_page.EditSchoolPage(), + middleware: [ + DeferredLoadingMiddleware(edit_school_page.loadLibrary), + ], + ), + ], + ), + ], + ); +} diff --git a/lib/super_admin/tools/constants.dart b/lib/super_admin/tools/constants.dart new file mode 100644 index 0000000000..eaca405e25 --- /dev/null +++ b/lib/super_admin/tools/constants.dart @@ -0,0 +1,7 @@ +enum SchoolIdConstant { + noSchool("dce19aa2-8863-4c93-861e-fb7be8f610ed"), + eclSchool("d9772da7-1142-4002-8b86-b694b431dfed"); + + const SchoolIdConstant(this.value); + final String value; +} diff --git a/lib/super_admin/tools/function.dart b/lib/super_admin/tools/function.dart new file mode 100644 index 0000000000..76d39810a7 --- /dev/null +++ b/lib/super_admin/tools/function.dart @@ -0,0 +1,14 @@ +import 'package:flutter/widgets.dart'; +import 'package:titan/super_admin/tools/constants.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/functions.dart'; + +String getSchoolNameFromId(String id, String name, BuildContext context) { + if (id == SchoolIdConstant.noSchool.value) { + return AppLocalizations.of(context)!.adminNoSchool; + } + if (id == SchoolIdConstant.eclSchool.value) { + return getBaseSchoolName(); + } + return name; +} diff --git a/lib/super_admin/ui/admin.dart b/lib/super_admin/ui/admin.dart new file mode 100644 index 0000000000..1283fda4f5 --- /dev/null +++ b/lib/super_admin/ui/admin.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/super_admin/router.dart'; +import 'package:titan/tools/ui/widgets/top_bar.dart'; +import 'package:titan/tools/constants.dart'; + +class SuperAdminTemplate extends HookConsumerWidget { + final Widget child; + const SuperAdminTemplate({super.key, required this.child}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + body: Container( + decoration: const BoxDecoration(color: ColorConstants.background), + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + TopBar(root: SuperAdminRouter.root), + Expanded(child: child), + ], + ), + ), + ), + ); + } +} diff --git a/lib/admin/ui/components/admin_button.dart b/lib/super_admin/ui/components/admin_button.dart similarity index 88% rename from lib/admin/ui/components/admin_button.dart rename to lib/super_admin/ui/components/admin_button.dart index 307c4887f7..72352229c5 100644 --- a/lib/admin/ui/components/admin_button.dart +++ b/lib/super_admin/ui/components/admin_button.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:titan/tools/constants.dart'; -class AdminButton extends StatelessWidget { +class SuperAdminButton extends StatelessWidget { final Widget child; - const AdminButton({super.key, required this.child}); + const SuperAdminButton({super.key, required this.child}); @override Widget build(BuildContext context) { diff --git a/lib/super_admin/ui/components/item_card_ui.dart b/lib/super_admin/ui/components/item_card_ui.dart new file mode 100644 index 0000000000..9adf51fb06 --- /dev/null +++ b/lib/super_admin/ui/components/item_card_ui.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +class ItemCardUi extends StatelessWidget { + final List children; + const ItemCardUi({super.key, required this.children}); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(vertical: 10), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: Colors.grey.withValues(alpha: 0.2), + blurRadius: 5, + spreadRadius: 2, + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: children, + ), + ); + } +} diff --git a/lib/admin/ui/components/text_editing.dart b/lib/super_admin/ui/components/text_editing.dart similarity index 100% rename from lib/admin/ui/components/text_editing.dart rename to lib/super_admin/ui/components/text_editing.dart diff --git a/lib/super_admin/ui/components/user_ui.dart b/lib/super_admin/ui/components/user_ui.dart new file mode 100644 index 0000000000..d7ca60174d --- /dev/null +++ b/lib/super_admin/ui/components/user_ui.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/user/class/simple_users.dart'; + +class UserUi extends HookConsumerWidget { + final SimpleUser user; + final void Function() onDelete; + + const UserUi({super.key, required this.user, required this.onDelete}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SizedBox( + height: 55, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + user.getName(), + style: const TextStyle(fontSize: 15), + overflow: TextOverflow.ellipsis, + ), + ), + GestureDetector( + onTap: onDelete, + child: Container( + padding: const EdgeInsets.all(7), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [ColorConstants.background2, Colors.black], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + boxShadow: [ + BoxShadow( + color: ColorConstants.background2.withValues(alpha: 0.4), + offset: const Offset(2, 3), + blurRadius: 5, + ), + ], + borderRadius: const BorderRadius.all(Radius.circular(10)), + ), + child: const HeroIcon( + HeroIcons.trash, + size: 20, + color: Colors.white, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/admin/ui/pages/edit_module_visibility/edit_module_visibility.dart b/lib/super_admin/ui/pages/edit_module_visibility/edit_module_visibility.dart similarity index 78% rename from lib/admin/ui/pages/edit_module_visibility/edit_module_visibility.dart rename to lib/super_admin/ui/pages/edit_module_visibility/edit_module_visibility.dart index 6b52a806b2..86809dbae9 100644 --- a/lib/admin/ui/pages/edit_module_visibility/edit_module_visibility.dart +++ b/lib/super_admin/ui/pages/edit_module_visibility/edit_module_visibility.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/providers/all_account_types_list_provider.dart'; +import 'package:titan/super_admin/providers/all_account_types_list_provider.dart'; import 'package:titan/admin/providers/all_groups_list_provider.dart'; -import 'package:titan/admin/providers/module_visibility_list_provider.dart'; -import 'package:titan/admin/tools/constants.dart'; -import 'package:titan/admin/ui/admin.dart'; -import 'package:titan/admin/ui/pages/edit_module_visibility/modules_expansion_panel.dart'; +import 'package:titan/super_admin/providers/module_visibility_list_provider.dart'; +import 'package:titan/super_admin/ui/admin.dart'; +import 'package:titan/super_admin/ui/pages/edit_module_visibility/modules_expansion_panel.dart'; import 'package:titan/tools/constants.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/widgets/loader.dart'; +import 'package:titan/l10n/app_localizations.dart'; class EditModulesVisibilityPage extends HookConsumerWidget { const EditModulesVisibilityPage({super.key}); @@ -18,7 +18,7 @@ class EditModulesVisibilityPage extends HookConsumerWidget { final modulesProvider = ref.watch(moduleVisibilityListProvider); final groups = ref.watch(allGroupList); final accountTypes = ref.watch(allAccountTypes); - return AdminTemplate( + return SuperAdminTemplate( child: Container( margin: const EdgeInsets.symmetric(horizontal: 20), child: SingleChildScrollView( @@ -28,10 +28,12 @@ class EditModulesVisibilityPage extends HookConsumerWidget { SizedBox( child: Column( children: [ - const Align( + Align( alignment: Alignment.centerLeft, child: Text( - AdminTextConstants.modifyModuleVisibility, + AppLocalizations.of( + context, + )!.adminModifyModuleVisibility, style: TextStyle( fontSize: 20, fontWeight: FontWeight.w700, diff --git a/lib/admin/ui/pages/edit_module_visibility/modules_expansion_panel.dart b/lib/super_admin/ui/pages/edit_module_visibility/modules_expansion_panel.dart similarity index 93% rename from lib/admin/ui/pages/edit_module_visibility/modules_expansion_panel.dart rename to lib/super_admin/ui/pages/edit_module_visibility/modules_expansion_panel.dart index 11686afeff..7cee25b697 100644 --- a/lib/admin/ui/pages/edit_module_visibility/modules_expansion_panel.dart +++ b/lib/super_admin/ui/pages/edit_module_visibility/modules_expansion_panel.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/class/account_type.dart'; -import 'package:titan/admin/class/module_visibility.dart'; -import 'package:titan/admin/providers/all_account_types_list_provider.dart'; +import 'package:titan/super_admin/class/account_type.dart'; +import 'package:titan/super_admin/class/module_visibility.dart'; +import 'package:titan/super_admin/providers/all_account_types_list_provider.dart'; import 'package:titan/admin/providers/all_groups_list_provider.dart'; -import 'package:titan/admin/providers/is_expanded_list_provider.dart'; -import 'package:titan/admin/providers/module_visibility_list_provider.dart'; -import 'package:titan/admin/tools/constants.dart'; +import 'package:titan/super_admin/providers/is_expanded_list_provider.dart'; +import 'package:titan/super_admin/providers/module_visibility_list_provider.dart'; +import 'package:titan/l10n/app_localizations.dart'; class ModulesExpansionPanel extends HookConsumerWidget { final List modules; @@ -52,8 +52,8 @@ class ModulesExpansionPanel extends HookConsumerWidget { Column( children: [ const Divider(), - const Text( - AdminTextConstants.accountTypes, + Text( + AppLocalizations.of(context)!.adminAccountTypes, style: TextStyle( color: Color.fromARGB(255, 0, 0, 0), fontSize: 20, @@ -133,9 +133,9 @@ class ModulesExpansionPanel extends HookConsumerWidget { const Divider(), Column( children: [ - const Text( - AdminTextConstants.groups, - style: TextStyle( + Text( + AppLocalizations.of(context)!.adminGroups, + style: const TextStyle( color: Color.fromARGB(255, 0, 0, 0), fontSize: 20, fontWeight: FontWeight.w900, diff --git a/lib/super_admin/ui/pages/main_page/main_page.dart b/lib/super_admin/ui/pages/main_page/main_page.dart new file mode 100644 index 0000000000..9f1cc07850 --- /dev/null +++ b/lib/super_admin/ui/pages/main_page/main_page.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/super_admin/router.dart'; +import 'package:titan/super_admin/ui/admin.dart'; +import 'package:titan/super_admin/ui/pages/main_page/menu_card_ui.dart'; +import 'package:titan/user/providers/user_list_provider.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; + +class SuperAdminMainPage extends HookConsumerWidget { + const SuperAdminMainPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + ref.watch(userList); + + final controller = ScrollController(); + + return SuperAdminTemplate( + child: Padding( + padding: const EdgeInsets.all(40), + child: GridView( + controller: controller, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 20, + crossAxisSpacing: 20, + childAspectRatio: + MediaQuery.of(context).size.width < + MediaQuery.of(context).size.height + ? 0.75 + : 1.5, + ), + children: [ + GestureDetector( + onTap: () { + QR.to( + SuperAdminRouter.root + SuperAdminRouter.editModuleVisibility, + ); + }, + child: MenuCardUi( + text: AppLocalizations.of(context)!.adminVisibilities, + icon: HeroIcons.eye, + ), + ), + GestureDetector( + onTap: () { + QR.to(SuperAdminRouter.root + SuperAdminRouter.schools); + }, + child: MenuCardUi( + text: AppLocalizations.of(context)!.adminSchools, + icon: HeroIcons.academicCap, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/admin/ui/pages/main_page/menu_card_ui.dart b/lib/super_admin/ui/pages/main_page/menu_card_ui.dart similarity index 100% rename from lib/admin/ui/pages/main_page/menu_card_ui.dart rename to lib/super_admin/ui/pages/main_page/menu_card_ui.dart diff --git a/lib/admin/ui/pages/schools/add_school_page/add_school_page.dart b/lib/super_admin/ui/pages/schools/add_school_page/add_school_page.dart similarity index 62% rename from lib/admin/ui/pages/schools/add_school_page/add_school_page.dart rename to lib/super_admin/ui/pages/schools/add_school_page/add_school_page.dart index 7ae21c8247..367fb6a3f1 100644 --- a/lib/admin/ui/pages/schools/add_school_page/add_school_page.dart +++ b/lib/super_admin/ui/pages/schools/add_school_page/add_school_page.dart @@ -1,17 +1,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/class/school.dart'; -import 'package:titan/admin/providers/school_list_provider.dart'; -import 'package:titan/admin/tools/constants.dart'; -import 'package:titan/admin/ui/admin.dart'; -import 'package:titan/admin/ui/components/admin_button.dart'; -import 'package:titan/admin/ui/components/text_editing.dart'; +import 'package:titan/super_admin/class/school.dart'; +import 'package:titan/super_admin/providers/school_list_provider.dart'; +import 'package:titan/super_admin/ui/admin.dart'; +import 'package:titan/super_admin/ui/components/admin_button.dart'; +import 'package:titan/super_admin/ui/components/text_editing.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/tools/ui/widgets/align_left_text.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AddSchoolPage extends HookConsumerWidget { const AddSchoolPage({super.key}); @@ -26,7 +26,7 @@ class AddSchoolPage extends HookConsumerWidget { displayToast(context, type, msg); } - return AdminTemplate( + return SuperAdminTemplate( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 30.0), child: SingleChildScrollView( @@ -37,16 +37,25 @@ class AddSchoolPage extends HookConsumerWidget { key: key, child: Column( children: [ - const AlignLeftText(AdminTextConstants.addSchool), + AlignLeftText(AppLocalizations.of(context)!.adminAddSchool), const SizedBox(height: 30), - TextEditing(controller: name, label: AdminTextConstants.name), + TextEditing( + controller: name, + label: AppLocalizations.of(context)!.adminName, + ), TextEditing( controller: emailRegex, - label: AdminTextConstants.emailRegex, + label: AppLocalizations.of(context)!.adminEmailRegex, ), WaitingButton( onTap: () async { await tokenExpireWrapper(ref, () async { + final addedSchoolMsg = AppLocalizations.of( + context, + )!.adminAddedSchool; + final addingErrorMsg = AppLocalizations.of( + context, + )!.adminAddingError; final value = await schoolListNotifier.createSchool( School( name: name.text, @@ -56,22 +65,16 @@ class AddSchoolPage extends HookConsumerWidget { ); if (value) { QR.back(); - displayToastWithContext( - TypeMsg.msg, - AdminTextConstants.addedSchool, - ); + displayToastWithContext(TypeMsg.msg, addedSchoolMsg); } else { - displayToastWithContext( - TypeMsg.error, - AdminTextConstants.addingError, - ); + displayToastWithContext(TypeMsg.error, addingErrorMsg); } }); }, - builder: (child) => AdminButton(child: child), - child: const Text( - AdminTextConstants.add, - style: TextStyle( + builder: (child) => SuperAdminButton(child: child), + child: Text( + AppLocalizations.of(context)!.adminAdd, + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white, diff --git a/lib/admin/ui/pages/schools/edit_school_page/edit_school_page.dart b/lib/super_admin/ui/pages/schools/edit_school_page/edit_school_page.dart similarity index 73% rename from lib/admin/ui/pages/schools/edit_school_page/edit_school_page.dart rename to lib/super_admin/ui/pages/schools/edit_school_page/edit_school_page.dart index 4253ff6305..78688ab925 100644 --- a/lib/admin/ui/pages/schools/edit_school_page/edit_school_page.dart +++ b/lib/super_admin/ui/pages/schools/edit_school_page/edit_school_page.dart @@ -2,12 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/class/school.dart'; -import 'package:titan/admin/providers/school_list_provider.dart'; -import 'package:titan/admin/providers/school_provider.dart'; -import 'package:titan/admin/tools/constants.dart'; -import 'package:titan/admin/ui/admin.dart'; -import 'package:titan/admin/ui/components/admin_button.dart'; +import 'package:titan/super_admin/class/school.dart'; +import 'package:titan/super_admin/providers/school_list_provider.dart'; +import 'package:titan/super_admin/providers/school_provider.dart'; +import 'package:titan/super_admin/tools/function.dart'; +import 'package:titan/super_admin/ui/admin.dart'; +import 'package:titan/super_admin/ui/components/admin_button.dart'; import 'package:titan/tools/constants.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; @@ -15,6 +15,7 @@ import 'package:titan/tools/ui/widgets/align_left_text.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; import 'package:titan/tools/ui/widgets/text_entry.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class EditSchoolPage extends HookConsumerWidget { const EditSchoolPage({super.key}); @@ -32,16 +33,16 @@ class EditSchoolPage extends HookConsumerWidget { displayToast(context, type, msg); } - name.text = school.name; + name.text = getSchoolNameFromId(school.id, school.name, context); emailRegex.text = school.emailRegex; - return AdminTemplate( + return SuperAdminTemplate( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 30.0), child: Column( children: [ - const AlignLeftText( - AdminTextConstants.edit, + AlignLeftText( + AppLocalizations.of(context)!.adminEdit, fontSize: 20, color: ColorConstants.gradient1, ), @@ -56,7 +57,7 @@ class EditSchoolPage extends HookConsumerWidget { child: TextEntry( controller: name, color: ColorConstants.gradient1, - label: AdminTextConstants.name, + label: AppLocalizations.of(context)!.adminName, suffixIcon: const HeroIcon(HeroIcons.pencil), enabledColor: Colors.transparent, ), @@ -67,7 +68,7 @@ class EditSchoolPage extends HookConsumerWidget { child: TextEntry( controller: emailRegex, color: ColorConstants.gradient1, - label: AdminTextConstants.emailRegex, + label: AppLocalizations.of(context)!.adminEmailRegex, suffixIcon: const HeroIcon(HeroIcons.pencil), enabledColor: Colors.transparent, ), @@ -78,6 +79,12 @@ class EditSchoolPage extends HookConsumerWidget { if (!key.currentState!.validate()) { return; } + final updatedGroupMsg = AppLocalizations.of( + context, + )!.adminUpdatedGroup; + final updatingErrorMsg = AppLocalizations.of( + context, + )!.adminUpdatingError; await tokenExpireWrapper(ref, () async { School newSchool = school.copyWith( name: name.text, @@ -89,22 +96,19 @@ class EditSchoolPage extends HookConsumerWidget { ); if (value) { QR.back(); - displayToastWithContext( - TypeMsg.msg, - AdminTextConstants.updatedGroup, - ); + displayToastWithContext(TypeMsg.msg, updatedGroupMsg); } else { displayToastWithContext( TypeMsg.msg, - AdminTextConstants.updatingError, + updatingErrorMsg, ); } }); }, - builder: (child) => AdminButton(child: child), - child: const Text( - AdminTextConstants.edit, - style: TextStyle( + builder: (child) => SuperAdminButton(child: child), + child: Text( + AppLocalizations.of(context)!.adminEdit, + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white, diff --git a/lib/admin/ui/pages/schools/school_page/school_button.dart b/lib/super_admin/ui/pages/schools/school_page/school_button.dart similarity index 100% rename from lib/admin/ui/pages/schools/school_page/school_button.dart rename to lib/super_admin/ui/pages/schools/school_page/school_button.dart diff --git a/lib/admin/ui/pages/schools/school_page/school_page.dart b/lib/super_admin/ui/pages/schools/school_page/school_page.dart similarity index 71% rename from lib/admin/ui/pages/schools/school_page/school_page.dart rename to lib/super_admin/ui/pages/schools/school_page/school_page.dart index e7c12830d0..8fe594aa0f 100644 --- a/lib/admin/ui/pages/schools/school_page/school_page.dart +++ b/lib/super_admin/ui/pages/schools/school_page/school_page.dart @@ -1,13 +1,12 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/providers/school_list_provider.dart'; -import 'package:titan/admin/providers/school_provider.dart'; -import 'package:titan/admin/router.dart'; -import 'package:titan/admin/ui/admin.dart'; -import 'package:titan/admin/ui/components/item_card_ui.dart'; -import 'package:titan/admin/ui/pages/schools/school_page/school_ui.dart'; -import 'package:titan/admin/tools/constants.dart'; +import 'package:titan/super_admin/providers/school_list_provider.dart'; +import 'package:titan/super_admin/providers/school_provider.dart'; +import 'package:titan/super_admin/router.dart'; +import 'package:titan/super_admin/ui/admin.dart'; +import 'package:titan/super_admin/ui/components/item_card_ui.dart'; +import 'package:titan/super_admin/ui/pages/schools/school_page/school_ui.dart'; import 'package:titan/tools/constants.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; @@ -16,6 +15,7 @@ import 'package:titan/tools/ui/layouts/refresher.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/user/providers/user_list_provider.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class SchoolsPage extends HookConsumerWidget { const SchoolsPage({super.key}); @@ -30,8 +30,9 @@ class SchoolsPage extends HookConsumerWidget { displayToast(context, type, msg); } - return AdminTemplate( + return SuperAdminTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await schoolsNotifier.loadSchools(); }, @@ -40,11 +41,11 @@ class SchoolsPage extends HookConsumerWidget { child: Column( children: [ const SizedBox(height: 20), - const Align( + Align( alignment: Alignment.centerLeft, child: Text( - AdminTextConstants.schools, - style: TextStyle( + AppLocalizations.of(context)!.adminSchools, + style: const TextStyle( fontSize: 20, fontWeight: FontWeight.w700, color: ColorConstants.gradient1, @@ -66,9 +67,9 @@ class SchoolsPage extends HookConsumerWidget { GestureDetector( onTap: () { QR.to( - AdminRouter.root + - AdminRouter.schools + - AdminRouter.addSchool, + SuperAdminRouter.root + + SuperAdminRouter.schools + + SuperAdminRouter.addSchool, ); }, child: ItemCardUi( @@ -89,9 +90,9 @@ class SchoolsPage extends HookConsumerWidget { onEdit: () { schoolNotifier.setSchool(school); QR.to( - AdminRouter.root + - AdminRouter.schools + - AdminRouter.editSchool, + SuperAdminRouter.root + + SuperAdminRouter.schools + + SuperAdminRouter.editSchool, ); }, onDelete: () async { @@ -99,22 +100,31 @@ class SchoolsPage extends HookConsumerWidget { context: context, builder: (context) { return CustomDialogBox( - title: AdminTextConstants.deleting, - descriptions: - AdminTextConstants.deleteSchool, + title: AppLocalizations.of( + context, + )!.adminDeleting, + descriptions: AppLocalizations.of( + context, + )!.adminDeleteSchool, onYes: () async { + final deletedMsg = AppLocalizations.of( + context, + )!.adminDeletedSchool; + final errorMsg = AppLocalizations.of( + context, + )!.adminDeletingError; tokenExpireWrapper(ref, () async { final value = await schoolsNotifier .deleteSchool(school); if (value) { displayToastWithContext( TypeMsg.msg, - AdminTextConstants.deletedSchool, + deletedMsg, ); } else { displayToastWithContext( TypeMsg.error, - AdminTextConstants.deletingError, + errorMsg, ); } }); diff --git a/lib/admin/ui/pages/schools/school_page/school_ui.dart b/lib/super_admin/ui/pages/schools/school_page/school_ui.dart similarity index 82% rename from lib/admin/ui/pages/schools/school_page/school_ui.dart rename to lib/super_admin/ui/pages/schools/school_page/school_ui.dart index 54f788df7b..7c3058f278 100644 --- a/lib/admin/ui/pages/schools/school_page/school_ui.dart +++ b/lib/super_admin/ui/pages/schools/school_page/school_ui.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/admin/class/school.dart'; -import 'package:titan/admin/tools/constants.dart'; -import 'package:titan/admin/ui/components/item_card_ui.dart'; -import 'package:titan/admin/ui/pages/schools/school_page/school_button.dart'; +import 'package:titan/super_admin/class/school.dart'; +import 'package:titan/super_admin/tools/constants.dart'; +import 'package:titan/super_admin/tools/function.dart'; +import 'package:titan/super_admin/ui/components/item_card_ui.dart'; +import 'package:titan/super_admin/ui/pages/schools/school_page/school_button.dart'; import 'package:titan/tools/constants.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; @@ -26,7 +27,7 @@ class SchoolUi extends HookConsumerWidget { const SizedBox(width: 10), Expanded( child: Text( - school.name, + getSchoolNameFromId(school.id, school.name, context), style: const TextStyle( color: Colors.black, fontSize: 20, diff --git a/lib/tools/constants.dart b/lib/tools/constants.dart index 43233ef1fa..05dc3eae01 100644 --- a/lib/tools/constants.dart +++ b/lib/tools/constants.dart @@ -7,17 +7,16 @@ class ColorConstants { static const Color background2 = Color(0xFF222643); static const Color deactivated1 = Color(0xFF9E9E9E); static const Color deactivated2 = Color(0xFFC0C0C0); -} -class TextConstants { - static const String admin = 'Admin'; - static const String error = "Une erreur est survenue"; - static const String noValue = "Veuillez entrer une valeur"; - static const String invalidNumber = "Veuillez entrer un nombre"; - static const String noDateError = "Veuillez entrer une date"; - static const String imageSizeTooBig = - "La taille de l'image ne doit pas dépasser 4 Mio"; - static const String imageError = "Erreur lors de l'ajout de l'image"; + static const Color background = Color(0xFFffffff); + static const Color onBackground = Color(0xffb4b4b4); + static const Color secondary = Color(0xFFb1b2b5); + static const Color tertiary = Color(0xFF424242); + static const Color onTertiary = Color(0xFF212121); + static const Color title = Color(0xFF000000); + static const Color main = Color(0xFFed0000); + static const Color onMain = Color(0xFFaa0202); + static const Color mainBorder = Color(0xFF950303); } const String previousEmailRegex = diff --git a/lib/tools/functions.dart b/lib/tools/functions.dart index dca0d7a05d..88809f8f5b 100644 --- a/lib/tools/functions.dart +++ b/lib/tools/functions.dart @@ -1,14 +1,67 @@ import 'package:datetime_picker_formfield/datetime_picker_formfield.dart'; -import 'package:flash/flash.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:heroicons/heroicons.dart'; import 'package:intl/intl.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/tools/constants.dart'; -import 'package:auto_size_text/auto_size_text.dart'; import 'package:titan/tools/plausible/plausible.dart'; import 'package:syncfusion_flutter_calendar/calendar.dart'; +import 'package:toastification/toastification.dart'; + +/// Parses CSV content with automatic separator detection +/// Supports common separators: comma, semicolon, tab, pipe +List parseCsvContent(String content) { + if (content.isEmpty) return []; + + final separators = [',', ';', '\t', '|']; + final lines = content.split('\n').where((line) => line.trim().isNotEmpty); + + if (lines.isEmpty) return []; + + // Try to detect the best separator by counting occurrences in the first few lines + String bestSeparator = ','; // Default to comma + int maxFieldCount = 0; + + for (final separator in separators) { + int totalFields = 0; + int lineCount = 0; + + for (final line in lines.take(3)) { + // Check first 3 lines + final fields = line + .split(separator) + .where((field) => field.trim().isNotEmpty); + totalFields += fields.length; + lineCount++; + } + + final avgFields = lineCount > 0 ? totalFields / lineCount : 0; + if (avgFields > maxFieldCount) { + maxFieldCount = avgFields.round(); + bestSeparator = separator; + } + } + + // Parse all lines with the detected separator + final result = []; + for (final line in lines) { + final fields = line + .split(bestSeparator) + .map((field) => field.trim()) + .where((field) => field.isNotEmpty && _isValidEmail(field)); + result.addAll(fields); + } + + return result.toSet().toList(); // Remove duplicates +} + +/// Simple email validation helper +bool _isValidEmail(String email) { + final emailRegex = RegExp( + r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', + ); + return emailRegex.hasMatch(email.trim()); +} enum TypeMsg { msg, error } @@ -33,75 +86,60 @@ void displayToast( String text, { int? duration, }) { - LinearGradient linearGradient; - HeroIcons icon; + String title; + Color primaryColor, textColor; + ToastificationType toastType; + final localization = AppLocalizations.of(context)!; switch (type) { case TypeMsg.msg: - linearGradient = const LinearGradient( - colors: [ColorConstants.gradient1, ColorConstants.gradient2], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ); - icon = HeroIcons.check; - duration = duration ?? 1500; + title = localization.toolSuccess; + primaryColor = ColorConstants.background; + textColor = ColorConstants.tertiary; + toastType = ToastificationType.success; break; case TypeMsg.error: - linearGradient = const LinearGradient( - colors: [ColorConstants.background2, Colors.black], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ); - icon = HeroIcons.exclamationTriangle; - duration = duration ?? 3000; + title = localization.adminError; + primaryColor = ColorConstants.onMain; + textColor = ColorConstants.background; + toastType = ToastificationType.error; break; } - showFlash( + toastification.show( context: context, - duration: Duration(milliseconds: duration), - builder: (context, controller) { - return FlashBar( - position: FlashPosition.top, - controller: controller, - surfaceTintColor: Colors.transparent, - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - margin: const EdgeInsets.only(top: 30, left: 20, right: 20), - content: Container( - alignment: Alignment.topCenter, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(15)), - gradient: linearGradient, - ), - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - height: 50 + text.length / 2, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - width: 40, - alignment: Alignment.center, - child: HeroIcon(icon, color: Colors.white), - ), - const SizedBox(width: 10), - Expanded( - child: Center( - child: AutoSizeText( - text, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - maxLines: 8, - ), - ), - ), - ], - ), - ), + type: toastType, + style: ToastificationStyle.fillColored, + alignment: Alignment.topCenter, + title: Text( + title, + style: TextStyle( + color: textColor, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + description: Text( + text, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w400, + color: textColor, + ), + ), + showIcon: false, + primaryColor: primaryColor, + showProgressBar: false, + closeButton: ToastCloseButton(showType: CloseButtonShowType.none), + autoCloseDuration: const Duration(milliseconds: 2500), + animationDuration: const Duration(milliseconds: 400), + animationBuilder: (context, animation, alignment, child) { + return SlideTransition( + position: Tween( + begin: const Offset(0, -1), + end: Offset.zero, + ).animate(animation), + child: Opacity(opacity: animation.value, child: child), ); }, ); @@ -134,14 +172,6 @@ bool isDateBefore(String date1, String date2) { return d1.isBefore(d2); } -String processDate(DateTime date) { - return "${date.day.toString().padLeft(2, "0")}/${date.month.toString().padLeft(2, "0")}/${date.year}"; -} - -String processDateWithHour(DateTime date) { - return "${processDate(date)} ${date.hour.toString().padLeft(2, "0")}:${date.minute.toString().padLeft(2, "0")}"; -} - String processDatePrint(String d) { if (d == "") { return ""; @@ -150,26 +180,20 @@ String processDatePrint(String d) { return "${e[2].toString().padLeft(2, "0")}/${e[1].toString().padLeft(2, "0")}/${e[0]}"; } -String processDateBack(String d) { - if (d == "") { - return ""; - } - List e = d.split("/"); - if (e[2].contains(" ")) { - return "${e[2].split(" ")[0]}-${e[1].toString().padLeft(2, "0")}-${e[0]} ${e[2].split(" ")[1]}"; - } - return "${e[2].toString().padLeft(2, "0")}-${e[1].toString().padLeft(2, "0")}-${e[0]}"; +String processDateBack(String d, String locale) { + return DateFormat.yMd(locale).parse(d).toIso8601String().split("T")[0]; } -String processDateBackWithHour(String d) { - if (d == "") { - return ""; - } - List e = d.split(" "); - if (e.length == 1) { - return processDateBack(e[0]); +String processDateBackWithHour(String d, String locale) { + return DateFormat.yMd(locale).add_Hm().parse(d).toIso8601String(); +} + +String processDateBackWithHourMaybe(String d, String locale) { + try { + return DateFormat.yMd(locale).add_Hm().parse(d).toIso8601String(); + } catch (e) { + return DateFormat.yMd(locale).parse(d).toIso8601String(); } - return "${processDateBack(e[0])} ${e[1]}"; } List getDateInRecurrence(String recurrenceRule, DateTime start) { @@ -221,6 +245,7 @@ String formatRecurrenceRule( DateTime dateEnd, String recurrenceRule, bool allDay, + String locale, ) { final start = parseDate(dateStart); final end = parseDate(dateEnd); @@ -268,7 +293,7 @@ String formatRecurrenceRule( } else { r += "toute la journée"; } - r += " jusqu'au ${processDate(DateTime.parse(endDay))}"; + r += " jusqu'au ${DateFormat.yMd(locale).format(DateTime.parse(endDay))}"; } else { if (!allDay) { r += "de ${start[1]} à ${end[1]}"; @@ -318,12 +343,14 @@ Future _getTime(BuildContext context) async { return Theme( data: ThemeData.light().copyWith( colorScheme: const ColorScheme.light( - primary: ColorConstants.gradient1, - onPrimary: Colors.white, - surface: Colors.white, - onSurface: Colors.black, + primary: ColorConstants.main, + onPrimary: ColorConstants.background, + surface: ColorConstants.background, + onSurface: ColorConstants.tertiary, + ), + dialogTheme: DialogThemeData( + backgroundColor: ColorConstants.background, ), - dialogTheme: DialogThemeData(backgroundColor: Colors.white), ), child: child!, ); @@ -347,12 +374,14 @@ Future _getDate( return Theme( data: ThemeData.light().copyWith( colorScheme: const ColorScheme.light( - primary: ColorConstants.gradient1, - onPrimary: Colors.white, - surface: Colors.white, - onSurface: Colors.black, + primary: ColorConstants.main, + onPrimary: ColorConstants.background, + surface: ColorConstants.background, + onSurface: ColorConstants.tertiary, + ), + dialogTheme: DialogThemeData( + backgroundColor: ColorConstants.background, ), - dialogTheme: DialogThemeData(backgroundColor: Colors.white), ), child: child!, ); @@ -367,6 +396,7 @@ Future getOnlyDayDate( DateTime? firstDate, DateTime? lastDate, }) async { + final locale = Localizations.localeOf(context).toString(); final DateTime now = DateTime.now(); final DateTime? date = await _getDate( context, @@ -376,8 +406,8 @@ Future getOnlyDayDate( lastDate, ); - dateController.text = DateFormat( - 'dd/MM/yyyy', + dateController.text = DateFormat.yMd( + locale, ).format(date ?? initialDate ?? now); } @@ -388,6 +418,7 @@ Future getOnlyDayDateFunction( DateTime? firstDate, DateTime? lastDate, }) async { + final locale = Localizations.localeOf(context).toString(); final DateTime now = DateTime.now(); final DateTime? date = await _getDate( context, @@ -397,18 +428,19 @@ Future getOnlyDayDateFunction( lastDate, ); - setDate(DateFormat('dd/MM/yyyy').format(date ?? initialDate ?? now)); + setDate(DateFormat.yMMMd(locale).format(date ?? initialDate ?? now)); } Future getOnlyHourDate( BuildContext context, TextEditingController dateController, ) async { + final locale = Localizations.localeOf(context).toString(); final DateTime now = DateTime.now(); final TimeOfDay? time = await _getTime(context); - dateController.text = DateFormat( - 'HH:mm', + dateController.text = DateFormat.Hm( + locale, ).format(DateTimeField.combine(now, time)); } @@ -419,6 +451,7 @@ Future getFullDate( DateTime? firstDate, DateTime? lastDate, }) async { + final locale = Localizations.localeOf(context).toString(); final DateTime now = DateTime.now(); _getDate(context, now, initialDate, firstDate, lastDate).then(( DateTime? date, @@ -426,15 +459,15 @@ Future getFullDate( if (date != null && context.mounted) { _getTime(context).then((TimeOfDay? time) { if (time != null) { - dateController.text = DateFormat( - 'dd/MM/yyyy HH:mm', - ).format(DateTimeField.combine(date, time)); + dateController.text = DateFormat.yMd( + locale, + ).add_Hm().format(DateTimeField.combine(date, time)); } }); } else { - dateController.text = DateFormat( - 'dd/MM/yyyy HH:mm', - ).format(initialDate ?? now); + dateController.text = DateFormat.yMd( + locale, + ).add_Hm().format(initialDate ?? now); } }); } @@ -463,18 +496,20 @@ String getAppFlavor() { return appFlavor!.toLowerCase(); } - if (const String.fromEnvironment("flavor") != "") { - return const String.fromEnvironment("flavor"); + const flavor = String.fromEnvironment("FLAVOR"); + + if (flavor.isEmpty) { + throw StateError("App flavor not set"); } - throw StateError("App flavor not set"); + return flavor.toLowerCase(); } Plausible? getPlausible() { - final serverUrl = dotenv.env["PLAUSIBLE_HOST"]; - final domain = dotenv.env["PLAUSIBLE_DOMAIN"]; + const serverUrl = String.fromEnvironment("PLAUSIBLE_HOST"); + const domain = String.fromEnvironment("PLAUSIBLE_DOMAIN"); - if (serverUrl == null || domain == null) { + if (serverUrl == "" || domain == "") { return null; } @@ -486,29 +521,38 @@ Plausible? getPlausible() { } String getTitanHost() { - var host = dotenv.env["${getAppFlavor().toUpperCase()}_HOST"]; - - if (host == null || host == "") { - throw StateError("Could not find host corresponding to flavor"); + const backendHost = String.fromEnvironment("BACKEND_HOST"); + if (backendHost.isEmpty) { + throw StateError("Could not find BACKEND_HOST in config.json"); + } + if (backendHost[backendHost.length - 1] != "/") { + throw StateError("BACKEND_HOST in config.json should end with a /"); } - return host; + return backendHost; +} + +String getPaymentName() { + return const String.fromEnvironment("PAYMENT_NAME", defaultValue: "ProxiPay"); +} + +String getBaseSchoolName() { + const schoolName = String.fromEnvironment("SCHOOL_NAME"); + if (schoolName.isEmpty) { + throw StateError("Could not find SCHOOL_NAME in config.json"); + } + return schoolName; } String getTitanURL() { - switch (getAppFlavor()) { - case "dev": - return "http://localhost:3000"; - case "alpha": - return "https://titan.dev.eclair.ec-lyon.fr"; - case "prod": - return "https://myecl.fr"; - default: - throw StateError("Invalid app flavor"); + const titanUrl = String.fromEnvironment("TITAN_URL"); + if (titanUrl.isEmpty) { + throw StateError("Could not find TITAN_URL in config.json"); } + return titanUrl; } -String getTitanURLScheme() { +String getTitanPackageSuffix() { switch (getAppFlavor()) { case "dev": return "titan.dev"; @@ -522,7 +566,23 @@ String getTitanURLScheme() { } String getTitanPackageName() { - return "fr.myecl.${getTitanURLScheme()}"; + const appIdPrefix = String.fromEnvironment("APP_ID_PREFIX"); + if (appIdPrefix.isEmpty) { + throw StateError("Could not find APP_ID_PREFIX in config.json"); + } + return "$appIdPrefix.${getTitanPackageSuffix()}"; +} + +String getTitanURLScheme() { + return getTitanPackageName(); +} + +String getAppName() { + const appName = String.fromEnvironment("APP_NAME"); + if (appName.isEmpty) { + throw StateError("Could not find APP_NAME in config.json"); + } + return appName; } String getTitanLogo() { diff --git a/lib/tools/middlewares/admin_middleware.dart b/lib/tools/middlewares/admin_middleware.dart index ee7262c440..95f279711a 100644 --- a/lib/tools/middlewares/admin_middleware.dart +++ b/lib/tools/middlewares/admin_middleware.dart @@ -3,7 +3,7 @@ import 'package:titan/router.dart'; import 'package:qlevar_router/qlevar_router.dart'; class AdminMiddleware extends QMiddleware { - final StateProvider isAdminProvider; + final ProviderBase isAdminProvider; final Ref ref; AdminMiddleware(this.ref, this.isAdminProvider); diff --git a/lib/tools/middlewares/authenticated_middleware.dart b/lib/tools/middlewares/authenticated_middleware.dart index 7027dd4bc2..4f4c54ce37 100644 --- a/lib/tools/middlewares/authenticated_middleware.dart +++ b/lib/tools/middlewares/authenticated_middleware.dart @@ -2,9 +2,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:titan/auth/providers/openid_provider.dart'; +import 'package:titan/feed/router.dart'; import 'package:titan/login/router.dart'; import 'package:titan/router.dart'; -import 'package:titan/settings/providers/module_list_provider.dart'; import 'package:titan/tools/providers/path_forwarding_provider.dart'; import 'package:titan/version/providers/titan_version_provider.dart'; import 'package:titan/version/providers/version_verifier_provider.dart'; @@ -17,11 +17,16 @@ class AuthenticatedMiddleware extends QMiddleware { @override Future redirectGuard(String path) async { + if (path.startsWith(LoginRouter.root)) { + return null; + } + if (path == "/" || path.isEmpty) { + return LoginRouter.root; + } final pathForwardingNotifier = ref.watch(pathForwardingProvider.notifier); final versionVerifier = ref.watch(versionVerifierProvider); final titanVersion = ref.watch(titanVersionProvider); final isLoggedIn = ref.watch(isLoggedInProvider); - final modules = ref.read(modulesProvider); final check = versionVerifier.whenData( (value) => value.minimalTitanVersion <= titanVersion, ); @@ -35,11 +40,6 @@ class AuthenticatedMiddleware extends QMiddleware { if (!value) { return AppRouter.update; } - if (path == LoginRouter.root && - !pathForwardingNotifier.state.isLoggedIn && - !isLoggedIn) { - return null; - } if (!isLoggedIn) { return LoginRouter.root; } @@ -47,11 +47,8 @@ class AuthenticatedMiddleware extends QMiddleware { pathForwardingNotifier.login(); } if (pathForwardingNotifier.state.path == "/") { - if (modules.isEmpty) { - return AppRouter.noModule; - } - pathForwardingNotifier.forward(modules.first.root); - return modules.first.root; + pathForwardingNotifier.forward(FeedRouter.root); + return FeedRouter.root; } if (pathForwardingNotifier.state.path != path) { return pathForwardingNotifier.state.path; diff --git a/lib/tools/providers/locale_notifier.dart b/lib/tools/providers/locale_notifier.dart new file mode 100644 index 0000000000..a076bbef0f --- /dev/null +++ b/lib/tools/providers/locale_notifier.dart @@ -0,0 +1,32 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +final localeProvider = StateNotifierProvider( + (ref) => LocaleNotifier(), +); + +class LocaleNotifier extends StateNotifier { + static const _localeKey = 'locale'; + + LocaleNotifier() : super(null) { + _loadLocale(); + } + + Future _loadLocale() async { + final prefs = await SharedPreferences.getInstance(); + final localeCode = prefs.getString(_localeKey); + if (localeCode != null) { + state = Locale(localeCode); + } else { + state = Locale(Platform.localeName); + } + } + + Future setLocale(Locale locale) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_localeKey, locale.languageCode); + state = locale; + } +} diff --git a/lib/tools/providers/prefered_module_root_list_provider.dart b/lib/tools/providers/prefered_module_root_list_provider.dart new file mode 100644 index 0000000000..5428657e6a --- /dev/null +++ b/lib/tools/providers/prefered_module_root_list_provider.dart @@ -0,0 +1,56 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class PreferedModuleRootListNotifier extends StateNotifier> { + static const preferedModuleRootListKey = 'prefered_modules'; + + PreferedModuleRootListNotifier() : super([]) { + loadPreferedModulesRootList(); + } + + Future loadPreferedModulesRootList() async { + final prefs = await SharedPreferences.getInstance(); + final preferedModuleRootList = prefs.getString(preferedModuleRootListKey); + if (preferedModuleRootList != null && preferedModuleRootList.isNotEmpty) { + state = preferedModuleRootList.split(','); + } else { + state = []; + } + } + + Future addPreferedModulesRoot(String preferedModuleRoot) async { + final prefs = await SharedPreferences.getInstance(); + final currentListStr = prefs.getString(preferedModuleRootListKey); + final currentList = currentListStr != null && currentListStr.isNotEmpty + ? currentListStr.split(',') + : []; + + if (currentList.length >= 2) return; + if (!currentList.contains(preferedModuleRoot)) { + final updatedList = [...currentList, preferedModuleRoot]; + prefs.setString(preferedModuleRootListKey, updatedList.join(',')); + state = updatedList; + } + } + + Future removePreferedModulesRoot(String preferedModuleRoot) async { + final prefs = await SharedPreferences.getInstance(); + final currentListStr = prefs.getString(preferedModuleRootListKey); + final currentList = currentListStr != null && currentListStr.isNotEmpty + ? currentListStr.split(',') + : []; + + if (currentList.contains(preferedModuleRoot)) { + final updatedList = currentList + .where((item) => item != preferedModuleRoot) + .toList(); + prefs.setString(preferedModuleRootListKey, updatedList.join(',')); + state = updatedList; + } + } +} + +final preferedModuleListRootProvider = + StateNotifierProvider>( + (ref) => PreferedModuleRootListNotifier(), + ); diff --git a/lib/tools/providers/should_notify_provider.dart b/lib/tools/providers/should_notify_provider.dart deleted file mode 100644 index f845907b31..0000000000 --- a/lib/tools/providers/should_notify_provider.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:titan/user/providers/user_provider.dart'; - -final shouldNotifyProvider = Provider((ref) { - final asyncUser = ref.watch(asyncUserProvider); - return asyncUser.maybeWhen( - data: (user) => !isStudent(user.email) && isNotStaff(user.email), - orElse: () => false, - ); -}); diff --git a/lib/tools/providers/single_map_provider.dart b/lib/tools/providers/single_map_provider.dart new file mode 100644 index 0000000000..d7374d02f3 --- /dev/null +++ b/lib/tools/providers/single_map_provider.dart @@ -0,0 +1,51 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; + +class SingleMapNotifier extends StateNotifier?>> { + SingleMapNotifier() : super(?>{}); + + void loadTList(List tList) async { + Map?> tMap = {}; + for (T l in tList) { + tMap[l] = null; + } + state = tMap; + } + + void addT(T t) { + if (!state.containsKey(t)) { + state = {...state, t: null}; + } + } + + void setTData(T t, AsyncValue value) { + state[t] = value; + state = Map.of(state); + } + + void deleteT(T t) { + if (state.containsKey(t)) { + final newState = Map.of(state)..remove(t); + state = newState; + } + } + + void resetAll() { + state = state.map((key, _) => MapEntry(key, null)); + } + + Future autoLoad( + WidgetRef ref, + T t, + Future> Function(T t) loader, + ) async { + setTData(t, const AsyncLoading()); + tokenExpireWrapper(ref, () async { + loader(t).then((value) { + if (mounted) { + setTData(t, value); + } + }); + }); + } +} diff --git a/lib/tools/repository/logo_repository.dart b/lib/tools/repository/logo_repository.dart index 7c4cacfd9f..5294be83ca 100644 --- a/lib/tools/repository/logo_repository.dart +++ b/lib/tools/repository/logo_repository.dart @@ -38,6 +38,8 @@ abstract class LogoRepository extends Repository { } else { throw AppException(ErrorType.notFound, decoded["detail"]); } + } else if (response.statusCode == 404) { + return Uint8List(0); } else { Repository.logger.error( "GET $ext$id$suffix\n${response.statusCode} ${response.body}", diff --git a/lib/tools/trads/en_timeago.dart b/lib/tools/trads/en_timeago.dart new file mode 100644 index 0000000000..2bd77cf300 --- /dev/null +++ b/lib/tools/trads/en_timeago.dart @@ -0,0 +1,73 @@ +import 'package:timeago/timeago.dart'; + +/// English Messages +class CustomEnMessages implements LookupMessages { + @override + String prefixAgo() => ''; + @override + String prefixFromNow() => ''; + @override + String suffixAgo() => 'ago'; + @override + String suffixFromNow() => 'from now'; + @override + String lessThanOneMinute(int seconds) => '$seconds seconds'; + @override + String aboutAMinute(int minutes) => 'a minute'; + @override + String minutes(int minutes) => '$minutes minutes'; + @override + String aboutAnHour(int minutes) => 'about an hour'; + @override + String hours(int hours) => '$hours hours'; + @override + String aDay(int hours) => 'a day'; + @override + String days(int days) => '$days days'; + @override + String aboutAMonth(int days) => 'about a month'; + @override + String months(int months) => '$months months'; + @override + String aboutAYear(int year) => 'about a year'; + @override + String years(int years) => '$years years'; + @override + String wordSeparator() => ' '; +} + +/// English short Messages +class CustomEnShortMessages implements LookupMessages { + @override + String prefixAgo() => ''; + @override + String prefixFromNow() => ''; + @override + String suffixAgo() => ''; + @override + String suffixFromNow() => ''; + @override + String lessThanOneMinute(int seconds) => '${seconds}s'; + @override + String aboutAMinute(int minutes) => '1m'; + @override + String minutes(int minutes) => '${minutes}m'; + @override + String aboutAnHour(int minutes) => '~1h'; + @override + String hours(int hours) => '${hours}h'; + @override + String aDay(int hours) => '~1d'; + @override + String days(int days) => '${days}d'; + @override + String aboutAMonth(int days) => '~1mo'; + @override + String months(int months) => '${months}mo'; + @override + String aboutAYear(int year) => '~1y'; + @override + String years(int years) => '${years}y'; + @override + String wordSeparator() => ' '; +} diff --git a/lib/tools/trads/fr_timeago.dart b/lib/tools/trads/fr_timeago.dart new file mode 100644 index 0000000000..b7e389bc21 --- /dev/null +++ b/lib/tools/trads/fr_timeago.dart @@ -0,0 +1,73 @@ +import 'package:timeago/timeago.dart'; + +/// English Messages +class CustomFrMessages implements LookupMessages { + @override + String prefixAgo() => 'il y a'; + @override + String prefixFromNow() => "d'ici"; + @override + String suffixAgo() => ''; + @override + String suffixFromNow() => ''; + @override + String lessThanOneMinute(int seconds) => "$seconds secondes"; + @override + String aboutAMinute(int minutes) => 'environ une minute'; + @override + String minutes(int minutes) => 'environ $minutes minutes'; + @override + String aboutAnHour(int minutes) => 'environ une heure'; + @override + String hours(int hours) => '$hours heures'; + @override + String aDay(int hours) => 'environ un jour'; + @override + String days(int days) => 'environ $days jours'; + @override + String aboutAMonth(int days) => 'environ un mois'; + @override + String months(int months) => 'environ $months mois'; + @override + String aboutAYear(int year) => 'un an'; + @override + String years(int years) => '$years ans'; + @override + String wordSeparator() => ' '; +} + +/// English short Messages +class CustomFrShortMessages implements LookupMessages { + @override + String prefixAgo() => 'il y a'; + @override + String prefixFromNow() => "d'ici"; + @override + String suffixAgo() => ''; + @override + String suffixFromNow() => ''; + @override + String lessThanOneMinute(int seconds) => "$seconds secondes"; + @override + String aboutAMinute(int minutes) => 'une minute'; + @override + String minutes(int minutes) => '$minutes minutes'; + @override + String aboutAnHour(int minutes) => 'une heure'; + @override + String hours(int hours) => '$hours heures'; + @override + String aDay(int hours) => 'un jour'; + @override + String days(int days) => '$days jours'; + @override + String aboutAMonth(int days) => 'un mois'; + @override + String months(int months) => '$months mois'; + @override + String aboutAYear(int year) => 'un an'; + @override + String years(int years) => '$years ans'; + @override + String wordSeparator() => ' '; +} diff --git a/lib/tools/ui/builders/async_child.dart b/lib/tools/ui/builders/async_child.dart index 21f001c77f..74c0b69ab2 100644 --- a/lib/tools/ui/builders/async_child.dart +++ b/lib/tools/ui/builders/async_child.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:titan/tools/constants.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/tools/ui/widgets/loader.dart'; +import 'package:tuple/tuple.dart'; -class AsyncChild extends StatelessWidget { - final AsyncValue value; - final Widget Function(BuildContext context, T value) builder; +class AsyncChild

extends StatelessWidget { + final AsyncValue

value; + final Widget Function(BuildContext context, P value) builder; final Widget Function(Object? error, StackTrace? stack)? errorBuilder; final Widget Function(BuildContext context)? loadingBuilder; final Widget Function(BuildContext context, Widget child)? orElseBuilder; @@ -29,7 +30,7 @@ class AsyncChild extends StatelessWidget { errorBuilder ?? (error, stack) => Center( child: Text( - "${TextConstants.error}:$error", + "An error occured:$error", style: TextStyle(color: loaderColor), ), ); @@ -42,3 +43,217 @@ class AsyncChild extends StatelessWidget { ); } } + +Widget handleLoadingAndError( + List values, + BuildContext context, { + Widget Function(BuildContext context)? loadingBuilder, + Widget Function(Object? error, StackTrace? stack)? errorBuilder, + Widget Function(BuildContext context, Widget child)? orElseBuilder, + Color? loaderColor, +}) { + final nonNullOrElseBuilder = orElseBuilder ?? (context, child) => child; + if (values.any((value) => value.hasError)) { + final nonNullErrorBuilder = + errorBuilder ?? + (error, stack) => Center( + child: Text( + "${AppLocalizations.of(context)!.adminError}:$error", + style: TextStyle(color: loaderColor), + ), + ); + final firstError = values.firstWhere((value) => value.hasError); + final error = firstError.error; + final stackTrace = firstError.stackTrace; + return nonNullOrElseBuilder( + context, + nonNullErrorBuilder(error, stackTrace), + ); + } + if (values.any((value) => value.isLoading)) { + final nonNullLoadingBuilder = + loadingBuilder ?? (context) => Loader(color: loaderColor); + return nonNullOrElseBuilder(context, nonNullLoadingBuilder(context)); + } + return nonNullOrElseBuilder(context, const SizedBox.shrink()); +} + +class Async2Children extends StatelessWidget { + final Tuple2, AsyncValue> values; + final Widget Function(BuildContext context, P value1, Q value2) builder; + final Widget Function(Object? error, StackTrace? stack)? errorBuilder; + final Widget Function(BuildContext context)? loadingBuilder; + final Widget Function(BuildContext context, Widget child)? orElseBuilder; + final Color? loaderColor; + const Async2Children({ + super.key, + required this.values, + required this.builder, + this.errorBuilder, + this.loaderColor, + this.orElseBuilder, + this.loadingBuilder, + }); + @override + Widget build(BuildContext context) { + List listValues = [values.item1, values.item2]; + if (listValues.any((value) => value.hasError || value.isLoading)) { + return handleLoadingAndError( + listValues, + context, + loadingBuilder: loadingBuilder, + errorBuilder: errorBuilder, + orElseBuilder: orElseBuilder, + loaderColor: loaderColor, + ); + } + return builder(context, listValues[0].value as P, listValues[1].value as Q); + } +} + +class Async3Children extends StatelessWidget { + final Tuple3, AsyncValue, AsyncValue> values; + final Widget Function(BuildContext context, P value1, Q value2, R value3) + builder; + final Widget Function(Object? error, StackTrace? stack)? errorBuilder; + final Widget Function(BuildContext context)? loadingBuilder; + final Widget Function(BuildContext context, Widget child)? orElseBuilder; + final Color? loaderColor; + const Async3Children({ + super.key, + required this.values, + required this.builder, + this.errorBuilder, + this.loaderColor, + this.orElseBuilder, + this.loadingBuilder, + }); + @override + Widget build(BuildContext context) { + List listValues = [values.item1, values.item2, values.item3]; + if (listValues.any((value) => value.hasError || value.isLoading)) { + return handleLoadingAndError( + listValues, + context, + loadingBuilder: loadingBuilder, + errorBuilder: errorBuilder, + orElseBuilder: orElseBuilder, + loaderColor: loaderColor, + ); + } + return builder( + context, + listValues[0].value as P, + listValues[1].value as Q, + listValues[2].value as R, + ); + } +} + +class Async4Children extends StatelessWidget { + final Tuple4, AsyncValue, AsyncValue, AsyncValue> + values; + final Widget Function( + BuildContext context, + P value1, + Q value2, + R value3, + S value4, + ) + builder; + final Widget Function(Object? error, StackTrace? stack)? errorBuilder; + final Widget Function(BuildContext context)? loadingBuilder; + final Widget Function(BuildContext context, Widget child)? orElseBuilder; + final Color? loaderColor; + const Async4Children({ + super.key, + required this.values, + required this.builder, + this.errorBuilder, + this.loaderColor, + this.orElseBuilder, + this.loadingBuilder, + }); + @override + Widget build(BuildContext context) { + List listValues = [values.item1, values.item2, values.item3]; + if (listValues.any((value) => value.hasError || value.isLoading)) { + return handleLoadingAndError( + listValues, + context, + loadingBuilder: loadingBuilder, + errorBuilder: errorBuilder, + orElseBuilder: orElseBuilder, + loaderColor: loaderColor, + ); + } + return builder( + context, + listValues[0].value as P, + listValues[1].value as Q, + listValues[2].value as R, + listValues[3].value as S, + ); + } +} + +class Async5Children extends StatelessWidget { + final Tuple5< + AsyncValue

, + AsyncValue, + AsyncValue, + AsyncValue, + AsyncValue + > + values; + final Widget Function( + BuildContext context, + P value1, + Q value2, + R value3, + S value4, + T value5, + ) + builder; + final Widget Function(Object? error, StackTrace? stack)? errorBuilder; + final Widget Function(BuildContext context)? loadingBuilder; + final Widget Function(BuildContext context, Widget child)? orElseBuilder; + final Color? loaderColor; + const Async5Children({ + super.key, + required this.values, + required this.builder, + this.errorBuilder, + this.loaderColor, + this.orElseBuilder, + this.loadingBuilder, + }); + @override + Widget build(BuildContext context) { + List listValues = [ + values.item1, + values.item2, + values.item3, + values.item4, + values.item5, + ]; + if (listValues.any((value) => value.hasError || value.isLoading)) { + return handleLoadingAndError( + listValues, + context, + loadingBuilder: loadingBuilder, + errorBuilder: errorBuilder, + orElseBuilder: orElseBuilder, + loaderColor: loaderColor, + ); + } + return builder( + context, + listValues[0].value as P, + listValues[1].value as Q, + listValues[2].value as R, + listValues[3].value as S, + listValues[4].value as T, + ); + } +} diff --git a/lib/tools/ui/builders/auto_loader_child.dart b/lib/tools/ui/builders/auto_loader_child.dart index f057860c2a..f6aaddc23e 100644 --- a/lib/tools/ui/builders/auto_loader_child.dart +++ b/lib/tools/ui/builders/auto_loader_child.dart @@ -35,9 +35,11 @@ class AutoLoaderChild extends ConsumerWidget { final nonNullLoadingBuilder = loadingBuilder ?? (context) => Loader(color: loaderColor); if (group == null) { - loader == null - ? notifier.autoLoadList(ref, mapKey, listLoader!) - : notifier.autoLoad(ref, mapKey, loader!); + Future.microtask(() { + loader == null + ? notifier.autoLoadList(ref, mapKey, listLoader!) + : notifier.autoLoad(ref, mapKey, loader!); + }); return nonNullLoadingBuilder(context); } return AsyncChild( diff --git a/lib/tools/ui/builders/single_auto_loader_child.dart b/lib/tools/ui/builders/single_auto_loader_child.dart new file mode 100644 index 0000000000..0a33fb6e49 --- /dev/null +++ b/lib/tools/ui/builders/single_auto_loader_child.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/tools/providers/single_map_provider.dart'; +import 'package:titan/tools/ui/builders/async_child.dart'; +import 'package:titan/tools/ui/widgets/loader.dart'; + +class SingleAutoLoaderChild extends ConsumerWidget { + final AsyncValue? item; + final SingleMapNotifier notifier; + final T mapKey; + final Future> Function(T t) loader; + final Widget Function(BuildContext context, E value) dataBuilder; + final Widget Function(Object? error, StackTrace? stack)? errorBuilder; + final Widget Function(BuildContext context)? loadingBuilder; + final Widget Function(BuildContext context, Widget child)? orElseBuilder; + final Color? loaderColor; + + const SingleAutoLoaderChild({ + super.key, + required this.item, + required this.notifier, + required this.mapKey, + required this.loader, + required this.dataBuilder, + this.errorBuilder, + this.loadingBuilder, + this.orElseBuilder, + this.loaderColor, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final nonNullLoadingBuilder = + loadingBuilder ?? (context) => Loader(color: loaderColor); + if (item == null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + notifier.autoLoad(ref, mapKey, loader); + }); + return nonNullLoadingBuilder(context); + } + return AsyncChild( + value: item!, + builder: (context, value) { + return dataBuilder(context, value); + }, + loaderColor: loaderColor, + ); + } +} diff --git a/lib/tools/ui/builders/waiting_button.dart b/lib/tools/ui/builders/waiting_button.dart index 2e6551204c..0e5fd26f85 100644 --- a/lib/tools/ui/builders/waiting_button.dart +++ b/lib/tools/ui/builders/waiting_button.dart @@ -7,6 +7,7 @@ class WaitingButton extends HookWidget { final Widget Function(Widget) builder; final Color waitingColor; final Future Function()? onTap; + final bool isLoading; const WaitingButton({ super.key, @@ -14,6 +15,7 @@ class WaitingButton extends HookWidget { required this.onTap, required this.builder, this.waitingColor = Colors.white, + this.isLoading = false, }); @override @@ -31,15 +33,17 @@ class WaitingButton extends HookWidget { return GestureDetector( behavior: HitTestBehavior.opaque, - onTap: () async { - if (clicked.value) return; - clicked.value = true; - shrinkButtonSize(); - onTap?.call().then((_) { - restoreButtonSize(); - clicked.value = false; - }); - }, + onTap: isLoading || clicked.value + ? null + : () async { + if (clicked.value) return; + clicked.value = true; + shrinkButtonSize(); + onTap?.call().then((_) { + restoreButtonSize(); + clicked.value = false; + }); + }, onTapCancel: restoreButtonSize, child: AnimatedBuilder( animation: animationController, diff --git a/lib/tools/ui/layouts/app_template.dart b/lib/tools/ui/layouts/app_template.dart index 00f4dab03b..d9dfa6094b 100644 --- a/lib/tools/ui/layouts/app_template.dart +++ b/lib/tools/ui/layouts/app_template.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/auth/providers/openid_provider.dart'; -import 'package:titan/drawer/ui/drawer_template.dart'; +import 'package:titan/navigation/ui/navigation_template.dart'; import 'package:titan/version/providers/titan_version_provider.dart'; import 'package:titan/version/providers/version_verifier_provider.dart'; @@ -26,7 +26,7 @@ class AppTemplate extends HookConsumerWidget { if (!isLoggedIn) { return child; } - return DrawerTemplate(child: child); + return NavigationTemplate(child: child); }, orElse: () => child, ); diff --git a/lib/tools/ui/layouts/column_refresher.dart b/lib/tools/ui/layouts/column_refresher.dart index 4906fda47c..edc87ab4da 100644 --- a/lib/tools/ui/layouts/column_refresher.dart +++ b/lib/tools/ui/layouts/column_refresher.dart @@ -4,25 +4,31 @@ import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/navigation/ui/scroll_to_hide_navbar.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; class ColumnRefresher extends ConsumerWidget { final List children; final Future Function() onRefresh; + final ScrollController controller; const ColumnRefresher({ super.key, required this.onRefresh, required this.children, + required this.controller, }); @override Widget build(BuildContext context, WidgetRef ref) { if (kIsWeb) { - return ListView.builder( - itemCount: children.length, - itemBuilder: (BuildContext context, int index) => children[index], - physics: const AlwaysScrollableScrollPhysics( - parent: BouncingScrollPhysics(), + return ScrollToHideNavbar( + controller: controller, + child: ListView.builder( + itemCount: children.length, + itemBuilder: (BuildContext context, int index) => children[index], + physics: const AlwaysScrollableScrollPhysics( + parent: BouncingScrollPhysics(), + ), ), ); } @@ -33,29 +39,35 @@ class ColumnRefresher extends ConsumerWidget { onRefresh: () async { tokenExpireWrapper(ref, onRefresh); }, - child: ListView.builder( - itemCount: children.length, - itemBuilder: (BuildContext context, int index) => children[index], - physics: const AlwaysScrollableScrollPhysics( - parent: BouncingScrollPhysics(), + child: ScrollToHideNavbar( + controller: controller, + child: ListView.builder( + itemCount: children.length, + itemBuilder: (BuildContext context, int index) => children[index], + physics: const AlwaysScrollableScrollPhysics( + parent: BouncingScrollPhysics(), + ), ), ), ); - Widget buildIOSList(WidgetRef ref) => CustomScrollView( - shrinkWrap: true, - physics: const BouncingScrollPhysics(), - slivers: [ - CupertinoSliverRefreshControl( - onRefresh: () async { - tokenExpireWrapper(ref, onRefresh); - }, - ), - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) => children[index], - childCount: children.length, + Widget buildIOSList(WidgetRef ref) => ScrollToHideNavbar( + controller: controller, + child: CustomScrollView( + shrinkWrap: true, + physics: const BouncingScrollPhysics(), + slivers: [ + CupertinoSliverRefreshControl( + onRefresh: () async { + tokenExpireWrapper(ref, onRefresh); + }, ), - ), - ], + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => children[index], + childCount: children.length, + ), + ), + ], + ), ); } diff --git a/lib/tools/ui/layouts/horizontal_list_view.dart b/lib/tools/ui/layouts/horizontal_list_view.dart index b4ef4a40d9..366b2b18ec 100644 --- a/lib/tools/ui/layouts/horizontal_list_view.dart +++ b/lib/tools/ui/layouts/horizontal_list_view.dart @@ -26,7 +26,6 @@ class HorizontalListView extends StatelessWidget { lastChild = null, childDelegate = SingleChildScrollView( scrollDirection: Axis.horizontal, - clipBehavior: Clip.none, controller: scrollController, physics: const BouncingScrollPhysics(), child: Row(children: children!), @@ -47,7 +46,6 @@ class HorizontalListView extends StatelessWidget { children = null, childDelegate = ListView.builder( scrollDirection: Axis.horizontal, - clipBehavior: Clip.none, controller: scrollController, physics: const BouncingScrollPhysics(), itemCount: diff --git a/lib/tools/ui/layouts/refresher.dart b/lib/tools/ui/layouts/refresher.dart index a6235f1b28..76db521a99 100644 --- a/lib/tools/ui/layouts/refresher.dart +++ b/lib/tools/ui/layouts/refresher.dart @@ -4,21 +4,32 @@ import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/navigation/ui/scroll_to_hide_navbar.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; class Refresher extends HookConsumerWidget { final Widget child; final Future Function() onRefresh; - const Refresher({super.key, required this.onRefresh, required this.child}); + final ScrollController controller; + const Refresher({ + super.key, + required this.onRefresh, + required this.child, + required this.controller, + }); @override Widget build(BuildContext context, WidgetRef ref) { if (kIsWeb) { - return SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics( - parent: BouncingScrollPhysics(), + return ScrollToHideNavbar( + controller: controller, + child: SingleChildScrollView( + controller: controller, + physics: const AlwaysScrollableScrollPhysics( + parent: BouncingScrollPhysics(), + ), + child: child, ), - child: child, ); } return Platform.isAndroid ? buildAndroidList(ref) : buildIOSList(ref); @@ -29,36 +40,44 @@ class Refresher extends HookConsumerWidget { onRefresh: () async { tokenExpireWrapper(ref, onRefresh); }, - child: SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics( - parent: BouncingScrollPhysics(), - ), - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: constraints.maxHeight), - child: child, + child: ScrollToHideNavbar( + controller: controller, + child: SingleChildScrollView( + controller: controller, + physics: const AlwaysScrollableScrollPhysics( + parent: BouncingScrollPhysics(), + ), + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: child, + ), ), ), ), ); Widget buildIOSList(WidgetRef ref) => LayoutBuilder( - builder: (context, constraints) => CustomScrollView( - shrinkWrap: false, - physics: const BouncingScrollPhysics( - parent: AlwaysScrollableScrollPhysics(), - ), - slivers: [ - CupertinoSliverRefreshControl( - onRefresh: () async { - tokenExpireWrapper(ref, onRefresh); - }, + builder: (context, constraints) => ScrollToHideNavbar( + controller: controller, + child: CustomScrollView( + controller: controller, + shrinkWrap: false, + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), ), - SliverToBoxAdapter( - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: constraints.maxHeight), - child: child, + slivers: [ + CupertinoSliverRefreshControl( + onRefresh: () async { + tokenExpireWrapper(ref, onRefresh); + }, ), - ), - ], + SliverToBoxAdapter( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: child, + ), + ), + ], + ), ), ); } diff --git a/lib/tools/ui/styleguide/bottom_modal_template.dart b/lib/tools/ui/styleguide/bottom_modal_template.dart new file mode 100644 index 0000000000..71eadb2de2 --- /dev/null +++ b/lib/tools/ui/styleguide/bottom_modal_template.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/navigation/providers/navbar_animation.dart'; +import 'package:titan/tools/constants.dart'; + +enum BottomModalType { main, danger } + +class BottomModalTemplate extends StatelessWidget { + final Widget child; + final String title; + final String? description; + final List? actions; + final BottomModalType type; + final String? animationKey; + + const BottomModalTemplate({ + super.key, + required this.child, + this.type = BottomModalType.main, + this.animationKey, + required this.title, + this.description, + this.actions, + }); + + const BottomModalTemplate.danger({ + super.key, + required this.child, + this.animationKey, + required this.title, + this.description, + this.actions, + }) : type = BottomModalType.danger; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Container( + margin: const EdgeInsets.only(bottom: 12), + width: 120, + height: 4, + decoration: BoxDecoration( + color: ColorConstants.onTertiary, + borderRadius: BorderRadius.circular(2), + boxShadow: [ + BoxShadow( + color: ColorConstants.onTertiary.withAlpha(50), + blurRadius: 4, + spreadRadius: 1, + ), + ], + ), + ), + ), + Hero( + tag: animationKey ?? 'bottom_modal', + child: Container( + decoration: BoxDecoration( + color: type == BottomModalType.main + ? ColorConstants.background + : ColorConstants.main, + borderRadius: BorderRadius.vertical(top: Radius.circular(30)), + ), + padding: EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + title, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w900, + color: type == BottomModalType.main + ? ColorConstants.tertiary + : ColorConstants.background, + ), + ), + SizedBox(height: 20), + if (description != null) + Text( + description!, + style: TextStyle( + fontSize: 15, + color: type == BottomModalType.main + ? ColorConstants.tertiary + : ColorConstants.background, + ), + ), + child, + if (actions != null && actions!.isNotEmpty) + Column(children: actions!), + SizedBox(height: 20), + ], + ), + ), + ), + ], + ); + } +} + +Future showCustomBottomModal({ + required BuildContext context, + required Widget modal, + required WidgetRef ref, + Function? onCloseCallback, +}) async { + final navbarAnimationNotifier = ref.watch(navbarAnimationProvider.notifier); + navbarAnimationNotifier.hideForModal(); + + try { + await showModalBottomSheet( + elevation: 3, + backgroundColor: Colors.transparent, + isScrollControlled: true, + useRootNavigator: true, + context: context, + builder: (_) => modal, + ); + } finally { + navbarAnimationNotifier.showForModal(); + onCloseCallback?.call(); + } +} diff --git a/lib/tools/ui/styleguide/button.dart b/lib/tools/ui/styleguide/button.dart new file mode 100644 index 0000000000..cdd4c08d9b --- /dev/null +++ b/lib/tools/ui/styleguide/button.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:titan/tools/constants.dart'; + +enum ButtonType { main, danger, onDanger, secondary } + +class Button extends StatelessWidget { + final ButtonType type; + final String text; + final bool? disabled; + final double? fontSize; + final Function() onPressed; + + const Button({ + super.key, + this.type = ButtonType.main, + required this.text, + required this.onPressed, + this.disabled = false, + this.fontSize, + }); + + const Button.danger({ + super.key, + required this.text, + required this.onPressed, + this.disabled = false, + this.fontSize, + }) : type = ButtonType.danger; + + const Button.onDanger({ + super.key, + required this.text, + required this.onPressed, + this.disabled = false, + this.fontSize, + }) : type = ButtonType.onDanger; + + const Button.secondary({ + super.key, + required this.text, + required this.onPressed, + this.disabled = false, + this.fontSize, + }) : type = ButtonType.secondary; + + Color get backgroundColor { + switch (type) { + case ButtonType.main: + return ColorConstants.tertiary; + case ButtonType.danger: + return ColorConstants.main; + case ButtonType.onDanger: + return ColorConstants.onMain; + case ButtonType.secondary: + return ColorConstants.background; + } + } + + Color get borderColor { + switch (type) { + case ButtonType.main: + return ColorConstants.onTertiary; + case ButtonType.danger: + return ColorConstants.mainBorder; + case ButtonType.onDanger: + return ColorConstants.mainBorder; + case ButtonType.secondary: + return ColorConstants.onBackground; + } + } + + Color get textColor { + Color color; + switch (type) { + case ButtonType.main: + color = ColorConstants.background; + case ButtonType.onDanger: + color = ColorConstants.background; + case ButtonType.danger: + color = ColorConstants.background; + case ButtonType.secondary: + color = ColorConstants.tertiary; + } + if (disabled == true) { + return color.withAlpha(150); + } + return color; + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: disabled == true ? null : onPressed, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: borderColor), + ), + child: Center( + child: Text( + text, + textAlign: TextAlign.center, + style: TextStyle( + color: textColor, + fontSize: fontSize ?? 18, + fontWeight: FontWeight.w900, + ), + ), + ), + ), + ); + } +} diff --git a/lib/tools/ui/styleguide/confirm_modal.dart b/lib/tools/ui/styleguide/confirm_modal.dart new file mode 100644 index 0000000000..63e3ca67e4 --- /dev/null +++ b/lib/tools/ui/styleguide/confirm_modal.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; + +enum ModalType { main, danger } + +class ConfirmModal extends StatelessWidget { + final String title, description; + final String? yesText, noText; + final ModalType type; + final Function() onYes; + final Function()? onNo; + + const ConfirmModal({ + super.key, + required this.title, + required this.description, + required this.onYes, + this.type = ModalType.main, + this.onNo, + this.yesText, + this.noText, + }); + + const ConfirmModal.danger({ + super.key, + required this.title, + required this.description, + required this.onYes, + this.onNo, + this.yesText, + this.noText, + }) : type = ModalType.danger; + + @override + Widget build(BuildContext context) { + AppLocalizations localizeWithContext = AppLocalizations.of(context)!; + return BottomModalTemplate( + title: title, + description: description, + type: type == ModalType.main + ? BottomModalType.main + : BottomModalType.danger, + actions: [ + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: Button( + text: noText ?? localizeWithContext.globalCancel, + onPressed: () { + Navigator.of(context).pop(); + if (onNo != null) { + onNo!(); + } + }, + type: ButtonType.secondary, + fontSize: 18, + ), + ), + SizedBox(width: 10), + Expanded( + child: Button( + text: yesText ?? localizeWithContext.globalConfirm, + onPressed: () { + Navigator.of(context).pop(); + onYes(); + }, + type: type == ModalType.main + ? ButtonType.main + : ButtonType.onDanger, + fontSize: 18, + ), + ), + ], + ), + ], + child: SizedBox.shrink(), + ); + } +} diff --git a/lib/tools/ui/styleguide/date_entry.dart b/lib/tools/ui/styleguide/date_entry.dart new file mode 100644 index 0000000000..355d49f4b3 --- /dev/null +++ b/lib/tools/ui/styleguide/date_entry.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/ui/styleguide/list_item_template.dart'; + +class DateEntry extends StatelessWidget { + final String title; + final String? subtitle; + final Function()? onTap; + const DateEntry({super.key, required this.title, this.subtitle, this.onTap}); + + @override + Widget build(BuildContext context) { + return ListItemTemplate( + onTap: onTap, + title: title, + subtitle: subtitle, + trailing: const HeroIcon( + HeroIcons.calendar, + color: ColorConstants.tertiary, + ), + ); + } +} diff --git a/lib/tools/ui/styleguide/horizontal_multi_select.dart b/lib/tools/ui/styleguide/horizontal_multi_select.dart new file mode 100644 index 0000000000..1fbc51060d --- /dev/null +++ b/lib/tools/ui/styleguide/horizontal_multi_select.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:titan/tools/ui/styleguide/item_chip.dart'; + +class HorizontalMultiSelect extends HookWidget { + final List items; + final T? selectedItem; + final Widget Function(BuildContext context, T item, int index, bool selected) + itemBuilder; + final Widget? firstChild; + final Function(T item)? onItemSelected; + final Function(T item)? onLongPress; + final Function(T item1, T item2)? isEqual; + final Widget? title; + const HorizontalMultiSelect({ + super.key, + required this.items, + this.selectedItem, + required this.itemBuilder, + this.firstChild, + this.onItemSelected, + this.onLongPress, + this.isEqual, + this.title, + }); + + @override + Widget build(BuildContext context) { + return ListView.builder( + scrollDirection: Axis.horizontal, + clipBehavior: Clip.none, + physics: const BouncingScrollPhysics(), + itemCount: items.length + (firstChild != null ? 1 : 0), + itemBuilder: (context, index) { + if (index == 0 && firstChild != null) { + return firstChild!; + } + final item = items[index - (firstChild != null ? 1 : 0)]; + final selected = isEqual != null + ? selectedItem != null + ? isEqual!(item, selectedItem as T) + : false + : item == selectedItem; + return ItemChip( + selected: selected, + onTap: () => onItemSelected != null ? onItemSelected!(item) : null, + onLongPress: () => onLongPress != null ? onLongPress!(item) : null, + child: itemBuilder(context, item, index, selected), + ); + }, + ); + } +} diff --git a/lib/tools/ui/styleguide/icon_button.dart b/lib/tools/ui/styleguide/icon_button.dart new file mode 100644 index 0000000000..b359e181b2 --- /dev/null +++ b/lib/tools/ui/styleguide/icon_button.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/ui/builders/waiting_button.dart'; + +enum CustomIconButtonType { main, danger, secondary } + +class CustomIconButton extends StatelessWidget { + final CustomIconButtonType type; + final Widget icon; + final bool? disabled; + final Function() onPressed; + + const CustomIconButton({ + super.key, + this.type = CustomIconButtonType.main, + required this.icon, + required this.onPressed, + this.disabled = false, + }); + + const CustomIconButton.danger({ + super.key, + required this.icon, + required this.onPressed, + this.disabled = false, + }) : type = CustomIconButtonType.danger; + + const CustomIconButton.secondary({ + super.key, + required this.icon, + required this.onPressed, + this.disabled = false, + }) : type = CustomIconButtonType.secondary; + + Color get backgroundColor { + switch (type) { + case CustomIconButtonType.main: + return ColorConstants.tertiary; + case CustomIconButtonType.danger: + return ColorConstants.main; + case CustomIconButtonType.secondary: + return ColorConstants.background; + } + } + + Color get borderColor { + switch (type) { + case CustomIconButtonType.main: + return ColorConstants.onTertiary; + case CustomIconButtonType.danger: + return ColorConstants.mainBorder; + case CustomIconButtonType.secondary: + return ColorConstants.onBackground; + } + } + + Color get textColor { + Color color; + switch (type) { + case CustomIconButtonType.main: + color = ColorConstants.background; + case CustomIconButtonType.danger: + color = ColorConstants.background; + case CustomIconButtonType.secondary: + color = ColorConstants.tertiary; + } + if (disabled == true) { + return color.withAlpha(150); + } + return color; + } + + @override + Widget build(BuildContext context) { + return WaitingButton( + onTap: disabled == true ? null : () async => onPressed(), + builder: (child) => Container( + height: 32, + width: 32, + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: borderColor, width: 2), + ), + child: child, + ), + child: Center(child: icon), + ); + } +} diff --git a/lib/tools/ui/styleguide/image_entry.dart b/lib/tools/ui/styleguide/image_entry.dart new file mode 100644 index 0000000000..4940431689 --- /dev/null +++ b/lib/tools/ui/styleguide/image_entry.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/ui/styleguide/list_item_template.dart'; + +class ImageEntry extends StatelessWidget { + final String title; + final String? subtitle; + final Function()? onTap; + const ImageEntry({super.key, required this.title, this.subtitle, this.onTap}); + + @override + Widget build(BuildContext context) { + return ListItemTemplate( + onTap: onTap, + title: title, + subtitle: subtitle, + trailing: const HeroIcon(HeroIcons.photo, color: ColorConstants.tertiary), + ); + } +} diff --git a/lib/tools/ui/styleguide/item_chip.dart b/lib/tools/ui/styleguide/item_chip.dart new file mode 100644 index 0000000000..ec02d23fc5 --- /dev/null +++ b/lib/tools/ui/styleguide/item_chip.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:titan/tools/constants.dart'; + +class ItemChip extends StatelessWidget { + final bool selected; + final Function()? onTap; + final Function()? onLongPress; + final Widget child; + final Axis scrollDirection; + const ItemChip({ + super.key, + this.selected = false, + this.onTap, + this.onLongPress, + this.scrollDirection = Axis.horizontal, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + onLongPress: onLongPress, + child: Container( + margin: scrollDirection == Axis.horizontal + ? EdgeInsets.symmetric(horizontal: 5.0) + : EdgeInsets.symmetric(vertical: 5.0), + padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 20.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30.0), + border: Border.all(color: ColorConstants.onTertiary), + color: selected + ? ColorConstants.onTertiary + : ColorConstants.background, + ), + child: Center(child: child), + ), + ); + } +} diff --git a/lib/tools/ui/styleguide/list_item.dart b/lib/tools/ui/styleguide/list_item.dart new file mode 100644 index 0000000000..7c8b243f16 --- /dev/null +++ b/lib/tools/ui/styleguide/list_item.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/ui/styleguide/list_item_template.dart'; + +class ListItem extends StatelessWidget { + final String title; + final String? subtitle; + final Widget? icon; + final Function()? onTap; + + const ListItem({ + super.key, + required this.title, + this.subtitle, + this.icon, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return ListItemTemplate( + onTap: onTap, + title: title, + subtitle: subtitle, + icon: icon, + trailing: onTap != null + ? HeroIcon(HeroIcons.chevronRight, color: ColorConstants.tertiary) + : SizedBox.shrink(), + ); + } +} diff --git a/lib/tools/ui/styleguide/list_item_template.dart b/lib/tools/ui/styleguide/list_item_template.dart new file mode 100644 index 0000000000..d4f92ac086 --- /dev/null +++ b/lib/tools/ui/styleguide/list_item_template.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:titan/tools/constants.dart'; + +class ListItemTemplate extends StatelessWidget { + final String title; + final String? subtitle; + final Widget? icon; + final Function()? onTap; + final Widget? trailing; + + const ListItemTemplate({ + super.key, + required this.title, + this.subtitle, + this.icon, + this.onTap, + this.trailing, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (icon != null) ...[icon!, const SizedBox(width: 10)], + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: ColorConstants.tertiary, + ), + ), + if (subtitle != null) + Text( + subtitle!, + style: TextStyle( + fontSize: 12, + color: ColorConstants.onTertiary, + ), + ), + ], + ), + ), + const SizedBox(width: 10), + if (trailing != null) + trailing! + else + const HeroIcon( + HeroIcons.chevronRight, + color: ColorConstants.tertiary, + ), + ], + ), + ), + ); + } +} diff --git a/lib/tools/ui/styleguide/list_item_toggle.dart b/lib/tools/ui/styleguide/list_item_toggle.dart new file mode 100644 index 0000000000..1e29067702 --- /dev/null +++ b/lib/tools/ui/styleguide/list_item_toggle.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/ui/styleguide/icon_button.dart'; +import 'package:titan/tools/ui/styleguide/list_item_template.dart'; + +class ToggleListItem extends StatelessWidget { + final String title; + final String? subtitle; + final Widget? icon; + final Function()? onTap; + final bool selected; + + const ToggleListItem({ + super.key, + required this.title, + this.subtitle, + this.icon, + this.onTap, + this.selected = false, + }); + + @override + Widget build(BuildContext context) { + return ListItemTemplate( + onTap: onTap, + title: title, + subtitle: subtitle, + icon: icon, + trailing: selected + ? CustomIconButton( + type: CustomIconButtonType.main, + icon: const HeroIcon( + HeroIcons.minus, + color: ColorConstants.background, + ), + onPressed: onTap ?? () {}, + ) + : CustomIconButton.secondary( + icon: const HeroIcon( + HeroIcons.plus, + color: ColorConstants.tertiary, + ), + onPressed: onTap ?? () {}, + ), + ); + } +} diff --git a/lib/tools/ui/styleguide/navbar.dart b/lib/tools/ui/styleguide/navbar.dart new file mode 100644 index 0000000000..f50e8ef01f --- /dev/null +++ b/lib/tools/ui/styleguide/navbar.dart @@ -0,0 +1,257 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/navigation/class/module.dart'; +import 'package:titan/navigation/providers/navbar_visibility_provider.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/providers/path_forwarding_provider.dart'; + +class FloatingNavbarItem { + final Function()? onTap; + final Module module; + + FloatingNavbarItem({this.onTap, required this.module}); +} + +class FloatingNavbar extends HookConsumerWidget { + final List items; + const FloatingNavbar({super.key, required this.items}); + @override + Widget build(BuildContext context, WidgetRef ref) { + final pathProvider = ref.watch(pathForwardingProvider); + final navbarVisibilityNotifier = ref.read( + navbarVisibilityProvider.notifier, + ); + final previousIndex = useRef(0); + final currentState = useState(3); + + final currentPath = pathProvider.path; + final routeIndex = useState(3); + + final lastInteractionTime = useRef(DateTime.now()); + + void onUserInteraction() { + lastInteractionTime.value = DateTime.now(); + navbarVisibilityNotifier.showTemporarily(); + } + + useEffect(() { + if (currentPath.isNotEmpty) { + String currentPathRoot = "/"; + final parts = currentPath.split('/'); + if (parts.length > 1 && parts[1].isNotEmpty) { + currentPathRoot = '/${parts[1]}'; + } + routeIndex.value = items.indexWhere( + (item) => item.module.root == currentPathRoot, + ); + if (routeIndex.value < 0 || routeIndex.value >= items.length) { + routeIndex.value = 3; + } + } + return null; + }, [currentPath]); + + useEffect(() { + currentState.value = routeIndex.value; + previousIndex.value = routeIndex.value; + return null; + }, []); + + final borderRadius = 25.0; + + final animationController = useAnimationController( + duration: const Duration(milliseconds: 300), + ); + + useEffect(() { + return () { + if (animationController.isAnimating) { + animationController.stop(); + } + }; + }, []); + + final slideAnimation = useRef?>(null); + final itemWidthRef = useRef(0.0); + + useEffect(() { + if (currentPath.isNotEmpty && routeIndex.value != currentState.value) { + previousIndex.value = currentState.value; + currentState.value = routeIndex.value; + } + return null; + }, [currentPath, routeIndex]); + + useEffect(() { + if (previousIndex.value != currentState.value && itemWidthRef.value > 0) { + slideAnimation.value = + Tween( + begin: previousIndex.value * itemWidthRef.value, + end: currentState.value * itemWidthRef.value, + ).animate( + CurvedAnimation( + parent: animationController, + curve: Curves.easeOutCubic, + ), + ); + animationController.reset(); + animationController.forward(); + } + return null; + }, [currentState.value, itemWidthRef.value]); + + return Listener( + onPointerDown: (_) => onUserInteraction(), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Material( + elevation: 10, + shadowColor: ColorConstants.main.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(borderRadius), + color: ColorConstants.main, + child: Container( + height: borderRadius * 2, + padding: const EdgeInsets.symmetric(horizontal: 5), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(borderRadius), + ), + child: LayoutBuilder( + builder: (context, constraints) { + final availableWidth = constraints.maxWidth; + final itemWidth = availableWidth / items.length; + + itemWidthRef.value = itemWidth; + + return Stack( + children: [ + AnimatedBuilder( + animation: animationController, + builder: (context, _) { + final leftPosition = slideAnimation.value != null + ? slideAnimation.value!.value + : itemWidth * currentState.value; + + return Positioned( + left: leftPosition, + top: 4, + bottom: 4, + width: itemWidth, + child: Container( + decoration: BoxDecoration( + color: ColorConstants.background, + borderRadius: BorderRadius.circular(borderRadius), + ), + ), + ); + }, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: items.asMap().entries.map((entry) { + final index = entry.key; + final item = entry.value; + final isSelected = index == currentState.value; + + return Expanded( + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(borderRadius), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (index != currentState.value) { + if (animationController.isAnimating) { + animationController.stop(); + } + + previousIndex.value = currentState.value; + currentState.value = index; + + WidgetsBinding.instance.addPostFrameCallback(( + _, + ) { + item.onTap?.call(); + }); + } else { + item.onTap?.call(); + } + }, + child: Container( + padding: const EdgeInsets.all(8), + child: AnimatedBuilder( + animation: animationController, + builder: (context, child) { + Color textColor; + FontWeight textWeight; + + if (previousIndex.value == + currentState.value) { + textColor = isSelected + ? ColorConstants.main + : ColorConstants.background; + textWeight = isSelected + ? FontWeight.w600 + : FontWeight.normal; + } else { + bool isInvolved = + index == previousIndex.value || + index == currentState.value; + + if (!isInvolved) { + textColor = ColorConstants.background; + textWeight = FontWeight.normal; + } else if (index == currentState.value) { + final progress = + animationController.value; + textColor = Color.lerp( + ColorConstants.background, + ColorConstants.main, + progress, + )!; + textWeight = progress < 0.5 + ? FontWeight.normal + : FontWeight.w600; + } else { + final progress = + animationController.value; + textColor = Color.lerp( + ColorConstants.main, + ColorConstants.background, + progress, + )!; + textWeight = progress < 0.5 + ? FontWeight.w600 + : FontWeight.normal; + } + } + + return Center( + child: AutoSizeText( + item.module.getName(context), + style: TextStyle( + color: textColor, + fontSize: 14, + fontWeight: textWeight, + ), + ), + ); + }, + ), + ), + ), + ), + ); + }).toList(), + ), + ], + ); + }, + ), + ), + ), + ), + ); + } +} diff --git a/lib/tools/ui/styleguide/router.dart b/lib/tools/ui/styleguide/router.dart new file mode 100644 index 0000000000..b0609624d7 --- /dev/null +++ b/lib/tools/ui/styleguide/router.dart @@ -0,0 +1,22 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/class/module.dart'; +import 'package:titan/tools/ui/styleguide/styleguide_page.dart'; +import 'package:qlevar_router/qlevar_router.dart'; + +class StyleGuideRouter { + final Ref ref; + static const String root = '/styleguide'; + static final Module module = Module( + getName: (context) => AppLocalizations.of(context)!.moduleStyleGuide, + getDescription: (context) => + AppLocalizations.of(context)!.moduleStyleGuideDescription, + root: StyleGuideRouter.root, + ); + StyleGuideRouter(this.ref); + QRoute route() => QRoute( + name: "styleguide", + path: StyleGuideRouter.root, + builder: () => const StyleGuidePage(), + ); +} diff --git a/lib/tools/ui/styleguide/searchbar.dart b/lib/tools/ui/styleguide/searchbar.dart new file mode 100644 index 0000000000..9e6c56eb7f --- /dev/null +++ b/lib/tools/ui/styleguide/searchbar.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/constants.dart'; + +class CustomSearchBar extends HookWidget { + final String? hintText; + final Function()? onFilter; + final Function(String) onSearch; + final bool autofocus; + const CustomSearchBar({ + super.key, + this.hintText, + this.onFilter, + required this.onSearch, + this.autofocus = false, + }); + + @override + Widget build(BuildContext context) { + final textController = useTextEditingController(); + final focusNode = useFocusNode(); + + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50), + color: ColorConstants.background, + boxShadow: [ + BoxShadow( + color: ColorConstants.onTertiary.withAlpha(30), + blurRadius: 6, + spreadRadius: 1, + offset: const Offset(0, 2), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 3.0), + child: Row( + children: [ + GestureDetector( + onTap: () { + if (textController.text.isEmpty) { + focusNode.requestFocus(); + } else { + onSearch(textController.text); + } + }, + child: HeroIcon( + HeroIcons.magnifyingGlass, + color: ColorConstants.tertiary, + size: 24, + ), + ), + const SizedBox(width: 8), + Expanded( + child: TextField( + controller: textController, + focusNode: focusNode, + autofocus: autofocus, + onChanged: (value) { + onSearch(value); + }, + style: TextStyle(color: ColorConstants.tertiary, fontSize: 16), + cursorColor: ColorConstants.tertiary, + decoration: InputDecoration( + hintText: + hintText ?? + AppLocalizations.of(context)!.phonebookPhonebookSearch, + hintStyle: TextStyle( + color: ColorConstants.secondary, + fontSize: 16, + ), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(vertical: 12), + ), + ), + ), + if (textController.text.isNotEmpty) + IconButton( + icon: HeroIcon( + HeroIcons.xMark, + color: ColorConstants.tertiary, + size: 20, + ), + onPressed: () { + textController.clear(); + }, + splashRadius: 20, + ), + if (onFilter != null) + IconButton( + icon: HeroIcon( + HeroIcons.adjustmentsHorizontal, + color: ColorConstants.tertiary, + size: 20, + ), + onPressed: onFilter, + splashRadius: 20, + ), + ], + ), + ), + ); + } +} diff --git a/lib/tools/ui/styleguide/styleguide_page.dart b/lib/tools/ui/styleguide/styleguide_page.dart new file mode 100644 index 0000000000..fb5bddc753 --- /dev/null +++ b/lib/tools/ui/styleguide/styleguide_page.dart @@ -0,0 +1,1331 @@ +import 'package:flutter/material.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; +import 'package:titan/tools/ui/styleguide/date_entry.dart'; +import 'package:titan/tools/ui/styleguide/horizontal_multi_select.dart'; +import 'package:titan/tools/ui/styleguide/icon_button.dart'; +import 'package:titan/tools/ui/styleguide/image_entry.dart'; +import 'package:titan/tools/ui/styleguide/item_chip.dart'; +import 'package:titan/tools/ui/styleguide/list_item.dart'; +import 'package:titan/tools/ui/styleguide/list_item_template.dart'; +import 'package:titan/tools/ui/styleguide/list_item_toggle.dart'; +import 'package:titan/tools/ui/styleguide/navbar.dart'; +import 'package:titan/tools/ui/styleguide/router.dart'; +import 'package:titan/tools/ui/styleguide/searchbar.dart'; +import 'package:titan/tools/ui/styleguide/text_entry.dart'; +import 'package:titan/tools/ui/widgets/top_bar.dart'; + +class StyleGuidePage extends HookConsumerWidget { + const StyleGuidePage({super.key}); + + Widget sectionHeader(String title, String description) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorConstants.title, + ), + ), + const SizedBox(height: 8), + Text( + description, + style: const TextStyle(fontSize: 16, color: ColorConstants.tertiary), + ), + ], + ); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SafeArea( + child: Column( + children: [ + TopBar(root: StyleGuideRouter.root), + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Section title + const Text( + "Components", + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: ColorConstants.title, + ), + ), + const SizedBox(height: 30), + + // Floating Navbar Section + sectionHeader( + "1. Floating Navigation Bar", + "A customizable navigation bar with a floating design and rounded corners", + ), + + // Floating Navbar Example + Container( + margin: const EdgeInsets.symmetric(vertical: 20), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: FloatingNavbar( + items: [ + // FloatingNavbarItem( + // title: 'Home', + // onTap: () { + // ScaffoldMessenger.of(context).showSnackBar( + // const SnackBar(content: Text('Home tapped')), + // ); + // }, + // ), + // FloatingNavbarItem( + // title: 'Search', + // onTap: () { + // ScaffoldMessenger.of(context).showSnackBar( + // const SnackBar(content: Text('Search tapped')), + // ); + // }, + // ), + // FloatingNavbarItem( + // title: 'Favorites', + // onTap: () { + // ScaffoldMessenger.of(context).showSnackBar( + // const SnackBar( + // content: Text('Favorites tapped'), + // ), + // ); + // }, + // ), + // FloatingNavbarItem( + // title: 'Profile', + // onTap: () { + // ScaffoldMessenger.of(context).showSnackBar( + // const SnackBar(content: Text('Profile tapped')), + // ); + // }, + // ), + ], + ), + ), + + // Divider + const Divider(height: 40), + + // Buttons Section + sectionHeader( + "2. Buttons", + "Collection of styled buttons for different actions and states", + ), + + // Button Examples + Container( + margin: const EdgeInsets.symmetric(vertical: 20), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Main Button:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Button( + text: "Main Action", + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Main button pressed'), + ), + ); + }, + ), + + const SizedBox(height: 16), + const Text( + "Danger Button:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Button.danger( + text: "Delete", + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Danger button pressed'), + ), + ); + }, + ), + + const SizedBox(height: 16), + const Text( + "On Danger Button:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Button.onDanger( + text: "Confirm Delete", + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('On danger button pressed'), + ), + ); + }, + ), + + const SizedBox(height: 16), + const Text( + "Secondary Button:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Button.secondary( + text: "Cancel", + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Secondary button pressed'), + ), + ); + }, + ), + + const SizedBox(height: 16), + const Text( + "Disabled Button:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Button( + text: "Disabled", + disabled: true, + onPressed: () {}, + ), + ], + ), + ), + + // Divider + const Divider(height: 40), + + // List Items Section + sectionHeader( + "3. List Items", + "Consistent list items for displaying information with optional icon and subtitle", + ), + + // List Items Example + Container( + margin: const EdgeInsets.symmetric(vertical: 20), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Basic List Item:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + ListItem( + title: "Settings", + onTap: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Settings tapped'), + ), + ); + }, + ), + + const SizedBox(height: 16), + const Text( + "List Item with Subtitle:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + ListItem( + title: "Account", + subtitle: "Manage your account details", + onTap: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Account tapped')), + ); + }, + ), + + const SizedBox(height: 16), + const Text( + "List Item with Icon:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + ListItem( + title: "Notifications", + icon: const HeroIcon( + HeroIcons.bell, + color: ColorConstants.tertiary, + ), + onTap: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Notifications tapped'), + ), + ); + }, + ), + + const SizedBox(height: 16), + const Text( + "Complete List Item:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + ListItem( + title: "Profile", + subtitle: "Edit your personal information", + icon: const HeroIcon( + HeroIcons.user, + color: ColorConstants.tertiary, + ), + onTap: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Profile tapped')), + ); + }, + ), + ], + ), + ), + + // Divider + const Divider(height: 40), + + // SearchBar Section + sectionHeader( + "4. SearchBar", + "A customizable search component with filtering capabilities", + ), + + // SearchBar Examples + Container( + margin: const EdgeInsets.symmetric(vertical: 20), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Basic SearchBar:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + CustomSearchBar( + hintText: "Search something...", + onSearch: (query) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Searching for: "$query"'), + duration: const Duration(seconds: 1), + ), + ); + }, + onFilter: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Filter button clicked'), + duration: Duration(seconds: 1), + ), + ); + }, + ), + + const SizedBox(height: 24), + const Text( + "SearchBar with Filter Dialog:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + CustomSearchBar( + hintText: "Search users...", + onSearch: (query) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('User search: "$query"'), + duration: const Duration(seconds: 1), + ), + ); + }, + onFilter: () { + // Show filter dialog + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text("Filter Options"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: const Text("Name"), + onTap: () { + Navigator.pop(context); + ScaffoldMessenger.of( + context, + ).showSnackBar( + const SnackBar( + content: Text('Filter by Name'), + ), + ); + }, + ), + ListTile( + title: const Text("Role"), + onTap: () { + Navigator.pop(context); + ScaffoldMessenger.of( + context, + ).showSnackBar( + const SnackBar( + content: Text('Filter by Role'), + ), + ); + }, + ), + ListTile( + title: const Text("Age"), + onTap: () { + Navigator.pop(context); + ScaffoldMessenger.of( + context, + ).showSnackBar( + const SnackBar( + content: Text('Filter by Age'), + ), + ); + }, + ), + ], + ), + ), + ); + }, + ), + ], + ), + ), + + // Divider + const Divider(height: 40), + + // Bottom Modal Template Section + sectionHeader( + "5. Bottom Modal Template", + "A reusable bottom sheet modal with a handle and customizable content", + ), + + // Bottom Modal Example + Container( + margin: const EdgeInsets.symmetric(vertical: 20), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Bottom Modal Example:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Button( + text: "Show Bottom Modal", + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) { + return BottomModalTemplate( + title: "Example Modal", + description: + "This is a customizable bottom modal template that can contain any content.", + actions: [ + Button( + text: "Close Modal", + onPressed: () => Navigator.pop(context), + ), + ], + child: Container( + height: 150, + alignment: Alignment.center, + child: const Text( + "Modal Content Area", + style: TextStyle( + fontSize: 16, + color: ColorConstants.tertiary, + ), + ), + ), + ); + }, + ); + }, + ), + + const SizedBox(height: 16), + Button.danger( + text: "Show Danger Modal", + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) { + return BottomModalTemplate.danger( + title: "Confirm Deletion", + description: + "This action cannot be undone. All data will be permanently deleted.", + actions: [ + Button.onDanger( + text: "Delete Permanently", + onPressed: () => Navigator.pop(context), + ), + const SizedBox(height: 8), + Button.secondary( + text: "Cancel", + onPressed: () => Navigator.pop(context), + ), + ], + child: Container( + height: 100, + alignment: Alignment.center, + child: const Text( + "Danger Content Area", + style: TextStyle( + fontSize: 16, + color: ColorConstants.background, + ), + ), + ), + ); + }, + ); + }, + ), + + const SizedBox(height: 16), + Button.secondary( + text: "Show Member Management Modal", + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) { + return BottomModalTemplate( + title: "Member Management", + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + // Search bar + Padding( + padding: const EdgeInsets.all(16.0), + child: CustomSearchBar( + hintText: "Search members...", + onSearch: (query) {}, + onFilter: () {}, + ), + ), + + ListItem( + title: "Add member", + icon: const HeroIcon( + HeroIcons.plus, + color: ColorConstants.tertiary, + ), + onTap: () { + Navigator.pop(context); + ScaffoldMessenger.of( + context, + ).showSnackBar( + const SnackBar( + content: Text( + 'Add member tapped', + ), + ), + ); + }, + ), + + const Divider(), + + ListItem( + title: "Zoto - Prez", + subtitle: "Jules Barra", + onTap: () { + Navigator.pop(context); + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: + Colors.transparent, + builder: (context) { + return BottomModalTemplate( + title: "Member Details", + description: "Jules Barra", + actions: [ + Button.secondary( + text: "Modify Role", + onPressed: () { + Navigator.pop(context); + ScaffoldMessenger.of( + context, + ).showSnackBar( + const SnackBar( + content: Text( + 'Modify role tapped', + ), + ), + ); + }, + ), + const SizedBox(height: 8), + Button.danger( + text: "Remove Role", + onPressed: () { + Navigator.pop(context); + ScaffoldMessenger.of( + context, + ).showSnackBar( + const SnackBar( + content: Text( + 'Remove role tapped', + ), + ), + ); + }, + ), + ], + child: const Center( + child: Padding( + padding: EdgeInsets.all( + 24.0, + ), + child: Text( + "Role: Zoto - Prez", + style: TextStyle( + fontSize: 18, + fontWeight: + FontWeight.bold, + color: ColorConstants + .title, + ), + ), + ), + ), + ); + }, + ); + }, + ), + + ListItem( + title: "Corpo", + subtitle: + "Gère toutes les autres assos", + onTap: () { + // Show member details + }, + ), + + ListItem( + title: "Biero", + subtitle: "Nathan Guigui", + onTap: () { + // Show member details + }, + ), + ], + ), + ); + }, + ); + }, + ), + + const SizedBox(height: 16), + const Text( + "Usage Example:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(8), + ), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "showModalBottomSheet(", + style: TextStyle(fontFamily: "monospace"), + ), + Text( + " context: context,", + style: TextStyle(fontFamily: "monospace"), + ), + Text( + " isScrollControlled: true,", + style: TextStyle(fontFamily: "monospace"), + ), + Text( + " backgroundColor: Colors.transparent,", + style: TextStyle(fontFamily: "monospace"), + ), + Text( + " builder: (context) => BottomModalTemplate(", + style: TextStyle(fontFamily: "monospace"), + ), + Text( + " child: YourContent(),", + style: TextStyle(fontFamily: "monospace"), + ), + Text( + " ),", + style: TextStyle(fontFamily: "monospace"), + ), + Text( + ");", + style: TextStyle(fontFamily: "monospace"), + ), + ], + ), + ), + ], + ), + ), + + const SizedBox(height: 40), + + // Horizontal Multi Select Section + sectionHeader( + "6. Horizontal Multi Select", + "A horizontally scrollable list of selectable items with customizable appearance", + ), + + // Horizontal Multi Select Examples + Container( + margin: const EdgeInsets.symmetric(vertical: 20), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Basic Multi Select:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + SizedBox( + height: 50, + child: HorizontalMultiSelect( + items: const [ + "Apple", + "Banana", + "Cherry", + "Date", + "Fig", + "Grape", + ], + itemBuilder: (context, item, index, selected) { + return Text( + item, + style: TextStyle( + fontSize: 16, + color: selected + ? Colors.white + : Colors.black, + ), + ); + }, + onItemSelected: (item) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Selected: $item'), + duration: const Duration(seconds: 1), + ), + ); + }, + ), + ), + + const SizedBox(height: 24), + const Text( + "With Custom First Child:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + SizedBox( + height: 50, + child: HorizontalMultiSelect( + items: const [ + Colors.red, + Colors.orange, + Colors.yellow, + Colors.green, + Colors.blue, + Colors.purple, + ], + firstChild: ItemChip( + child: const HeroIcon( + HeroIcons.plus, + size: 24, + color: Colors.black, + ), + onTap: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Add new color'), + duration: Duration(seconds: 1), + ), + ); + }, + ), + itemBuilder: (context, color, index, selected) { + return Container( + width: 30, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(25), + ), + ); + }, + onItemSelected: (color) { + final colorName = color == Colors.red + ? "Red" + : color == Colors.orange + ? "Orange" + : color == Colors.yellow + ? "Yellow" + : color == Colors.green + ? "Green" + : color == Colors.blue + ? "Blue" + : "Purple"; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Selected: $colorName'), + duration: const Duration(seconds: 1), + ), + ); + }, + ), + ), + + const SizedBox(height: 24), + const Text( + "With Long Press Support:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + SizedBox( + height: 70, + child: HorizontalMultiSelect>( + items: const [ + {"name": "John", "role": "Admin"}, + {"name": "Emma", "role": "Editor"}, + {"name": "Michael", "role": "Viewer"}, + {"name": "Sarah", "role": "Admin"}, + {"name": "David", "role": "Editor"}, + ], + itemBuilder: (context, user, index, selected) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + user["name"], + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: selected + ? Colors.blue + : Colors.black, + ), + ), + Text( + user["role"], + style: TextStyle( + fontSize: 12, + color: selected + ? Colors.blue.shade700 + : Colors.grey.shade600, + ), + ), + ], + ); + }, + onItemSelected: (user) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Selected: ${user["name"]}'), + duration: const Duration(seconds: 1), + ), + ); + }, + onLongPress: (user) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Long press on ${user["name"]}', + ), + backgroundColor: Colors.orange, + duration: const Duration(seconds: 1), + ), + ); + }, + ), + ), + + const SizedBox(height: 16), + const Text( + "Usage Example:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(8), + ), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "HorizontalMultiSelect(", + style: TextStyle(fontFamily: "monospace"), + ), + Text( + " items: ['Item 1', 'Item 2', 'Item 3'],", + style: TextStyle(fontFamily: "monospace"), + ), + Text( + " itemBuilder: (context, item, index) {", + style: TextStyle(fontFamily: "monospace"), + ), + Text( + " return Text(item);", + style: TextStyle(fontFamily: "monospace"), + ), + Text( + " },", + style: TextStyle(fontFamily: "monospace"), + ), + Text( + " onItemSelected: (item) {", + style: TextStyle(fontFamily: "monospace"), + ), + Text( + " print('Selected: \$item');", + style: TextStyle(fontFamily: "monospace"), + ), + Text( + " },", + style: TextStyle(fontFamily: "monospace"), + ), + Text( + ")", + style: TextStyle(fontFamily: "monospace"), + ), + ], + ), + ), + ], + ), + ), + + const SizedBox(height: 40), + + // Icon Button Section + const Divider(height: 40), + + sectionHeader( + "7. Icon Buttons", + "Icon-only buttons in various styles for compact UI elements", + ), + + Container( + margin: const EdgeInsets.symmetric(vertical: 20), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Main Icon Button:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + CustomIconButton( + icon: const HeroIcon( + HeroIcons.plus, + color: ColorConstants.background, + ), + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Main icon button pressed'), + ), + ); + }, + ), + const SizedBox(width: 16), + CustomIconButton.danger( + icon: const HeroIcon( + HeroIcons.trash, + color: ColorConstants.background, + ), + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Danger icon button pressed', + ), + ), + ); + }, + ), + const SizedBox(width: 16), + CustomIconButton.secondary( + icon: const HeroIcon( + HeroIcons.pencil, + color: ColorConstants.tertiary, + ), + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Secondary icon button pressed', + ), + ), + ); + }, + ), + ], + ), + ], + ), + ), + + // List Item Template Section + const Divider(height: 40), + + sectionHeader( + "8. List Item Template", + "Base component for creating consistent list items with customizable content", + ), + + Container( + margin: const EdgeInsets.symmetric(vertical: 20), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Basic List Item Template:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + ListItemTemplate( + title: "Template Item", + subtitle: "Customizable list item template", + icon: const HeroIcon( + HeroIcons.documentText, + color: ColorConstants.tertiary, + ), + onTap: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Template item tapped'), + ), + ); + }, + ), + const SizedBox(height: 16), + const Text( + "With Custom Trailing Widget:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + ListItemTemplate( + title: "Custom Trailing", + subtitle: "With a badge indicator", + icon: const HeroIcon( + HeroIcons.bell, + color: ColorConstants.tertiary, + ), + trailing: Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: ColorConstants.main, + borderRadius: BorderRadius.circular(12), + ), + child: const Text( + "New", + style: TextStyle( + color: ColorConstants.background, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + onTap: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Custom trailing item tapped'), + ), + ); + }, + ), + ], + ), + ), + + // Toggle List Item Section + const Divider(height: 40), + + sectionHeader( + "9. Toggle List Item", + "Toggleable list items with dynamic icons for selection state", + ), + + Container( + margin: const EdgeInsets.symmetric(vertical: 20), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Unselected Item:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + ToggleListItem( + title: "Add to Group", + subtitle: "Tap to add this member", + icon: const HeroIcon( + HeroIcons.userGroup, + color: ColorConstants.tertiary, + ), + selected: false, + onTap: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Toggle item tapped'), + ), + ); + }, + ), + const SizedBox(height: 16), + const Text( + "Selected Item:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + ToggleListItem( + title: "Member Added", + subtitle: "This member is already in the group", + icon: const HeroIcon( + HeroIcons.userGroup, + color: ColorConstants.tertiary, + ), + selected: true, + onTap: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Selected toggle item tapped'), + ), + ); + }, + ), + ], + ), + ), + + // Text Entry Section + const Divider(height: 40), + + sectionHeader( + "10. Text Entry", + "Form input fields with validation support and customizable styling", + ), + + Container( + margin: const EdgeInsets.symmetric(vertical: 20), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Basic Text Entry:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + TextEntry( + label: "Name", + controller: TextEditingController(), + onChanged: (value) { + // Handle text change + }, + ), + const SizedBox(height: 16), + const Text( + "Number Entry:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + TextEntry( + label: "Amount", + controller: TextEditingController(), + isDouble: true, + keyboardType: TextInputType.number, + suffix: "€", + ), + const SizedBox(height: 16), + const Text( + "Multiline Entry:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + TextEntry( + label: "Description", + controller: TextEditingController(), + minLines: 3, + maxLines: 5, + keyboardType: TextInputType.multiline, + ), + ], + ), + ), + + // Specialized Entry Fields Section + const Divider(height: 40), + + sectionHeader( + "11. Specialized Entry Fields", + "Pre-configured entry fields for dates and images", + ), + + Container( + margin: const EdgeInsets.symmetric(vertical: 20), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Date Entry:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + DateEntry( + title: "Event Date", + subtitle: "Select a date for your event", + onTap: () { + showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime.now(), + lastDate: DateTime.now().add( + const Duration(days: 365), + ), + ); + }, + ), + const SizedBox(height: 16), + const Text( + "Image Entry:", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + ImageEntry( + title: "Profile Picture", + subtitle: "Tap to select an image", + onTap: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Image entry tapped'), + ), + ); + }, + ), + ], + ), + ), + + const SizedBox(height: 40), + ], + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/tools/ui/styleguide/text_entry.dart b/lib/tools/ui/styleguide/text_entry.dart new file mode 100644 index 0000000000..69f4bcb6d3 --- /dev/null +++ b/lib/tools/ui/styleguide/text_entry.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/constants.dart'; + +class TextEntry extends StatelessWidget { + final String label, suffix, prefix, noValueError; + final bool isInt, isDouble, isNegative; + final bool canBeEmpty; + final bool enabled; + final TextEditingController controller; + final TextInputType keyboardType; + final TextCapitalization textCapitalization; + final Color color, enabledColor, errorColor; + final Widget? suffixIcon; + final Function(String)? onChanged; + final String? Function(String)? validator; + final int? minLines, maxLines, maxLength; + final TextInputAction textInputAction; + + const TextEntry({ + super.key, + required this.label, + required this.controller, + this.onChanged, + this.validator, + this.minLines, + this.maxLines, + this.maxLength, + this.prefix = '', + this.suffix = '', + this.enabled = true, + this.isInt = false, + this.isDouble = false, + this.keyboardType = TextInputType.text, + this.textCapitalization = TextCapitalization.sentences, + this.canBeEmpty = false, + this.color = ColorConstants.onTertiary, + this.enabledColor = ColorConstants.onTertiary, + this.errorColor = ColorConstants.main, + this.noValueError = "No value", + this.suffixIcon, + this.isNegative = false, + this.textInputAction = TextInputAction.next, + }); + + @override + Widget build(BuildContext context) { + final localizeWithContext = AppLocalizations.of(context)!; + + return TextFormField( + minLines: minLines, + maxLines: maxLines, + maxLength: maxLength, + controller: controller, + keyboardType: keyboardType, + textCapitalization: textCapitalization, + cursorColor: color, + onChanged: onChanged, + textInputAction: (keyboardType == TextInputType.multiline) + ? TextInputAction.newline + : textInputAction, + enabled: enabled, + decoration: InputDecoration( + label: Text( + canBeEmpty ? localizeWithContext.globalOptionnal(label) : label, + style: TextStyle(color: color, height: 0.5), + ), + suffixIcon: suffixIcon, + suffix: suffixIcon == null && suffix.isEmpty + ? null + : (suffixIcon == null + ? Padding( + padding: const EdgeInsets.only(left: 10), + child: Text(suffix, style: TextStyle(color: color)), + ) + : null), + prefix: prefix.isEmpty + ? null + : Container( + padding: const EdgeInsets.only(right: 10), + child: Text(prefix, style: TextStyle(color: color)), + ), + floatingLabelStyle: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: enabledColor), + ), + errorBorder: UnderlineInputBorder( + borderSide: BorderSide(color: errorColor, width: 2.0), + ), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: color, width: 2.0), + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + if (canBeEmpty) { + return null; + } + return noValueError; + } + + if (isInt) { + final intValue = int.tryParse(value); + if (intValue == null || (intValue < 0 && !isNegative)) { + return localizeWithContext.toolInvalidNumber; + } + } + + if (isDouble) { + final doubleValue = double.tryParse(value.replaceAll(',', '.')); + if (doubleValue == null || (doubleValue < 0 && !isNegative)) { + return localizeWithContext.toolInvalidNumber; + } + } + + if (validator == null) { + return null; + } + return validator!(value); + }, + ); + } +} diff --git a/lib/tools/ui/widgets/admin_button.dart b/lib/tools/ui/widgets/admin_button.dart index 2b90fc97e8..6db064ab3b 100644 --- a/lib/tools/ui/widgets/admin_button.dart +++ b/lib/tools/ui/widgets/admin_button.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; -import 'package:titan/tools/constants.dart'; class AdminButton extends StatelessWidget { final VoidCallback onTap; @@ -13,7 +12,7 @@ class AdminButton extends StatelessWidget { required this.onTap, this.textColor = Colors.white, this.color = Colors.black, - this.text = TextConstants.admin, + this.text = "Admin", this.colors, }); diff --git a/lib/tools/ui/widgets/calendar.dart b/lib/tools/ui/widgets/calendar.dart index 70b2df7288..62d10611df 100644 --- a/lib/tools/ui/widgets/calendar.dart +++ b/lib/tools/ui/widgets/calendar.dart @@ -2,7 +2,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/drawer/providers/is_web_format_provider.dart'; +import 'package:titan/navigation/providers/is_web_format_provider.dart'; import 'package:titan/tools/constants.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; diff --git a/lib/tools/ui/widgets/custom_dialog_box.dart b/lib/tools/ui/widgets/custom_dialog_box.dart index 3c301a6c85..aa7cd17a7e 100644 --- a/lib/tools/ui/widgets/custom_dialog_box.dart +++ b/lib/tools/ui/widgets/custom_dialog_box.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; class Consts { @@ -113,7 +114,10 @@ class CustomDialogBox extends StatelessWidget { ), child: Center( child: Text( - noText ?? "Annuler", + noText ?? + AppLocalizations.of( + context, + )!.globalCancel, style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, @@ -148,7 +152,8 @@ class CustomDialogBox extends StatelessWidget { ), child: Center( child: Text( - yesText ?? "Confirmer", + yesText ?? + AppLocalizations.of(context)!.globalConfirm, style: TextStyle(fontWeight: FontWeight.bold), ), ), diff --git a/lib/tools/ui/widgets/date_entry.dart b/lib/tools/ui/widgets/date_entry.dart index 25c852de64..0c2c0b5881 100644 --- a/lib/tools/ui/widgets/date_entry.dart +++ b/lib/tools/ui/widgets/date_entry.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:titan/tools/constants.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/tools/ui/widgets/text_entry.dart'; class DateEntry extends StatelessWidget { final VoidCallback onTap; final String label; - final bool enabled; + final bool enabled, canBeEmpty; final TextEditingController controller; final Color color, enabledColor, errorColor; final Widget? suffixIcon; @@ -16,6 +16,7 @@ class DateEntry extends StatelessWidget { required this.controller, required this.onTap, this.enabled = true, + this.canBeEmpty = false, this.color = Colors.black, this.enabledColor = Colors.black, this.errorColor = Colors.red, @@ -29,12 +30,13 @@ class DateEntry extends StatelessWidget { child: TextEntry( label: label, controller: controller, - noValueError: TextConstants.noDateError, + noValueError: AppLocalizations.of(context)!.toolDateRequired, enabled: enabled, color: color, enabledColor: enabledColor, errorColor: errorColor, suffixIcon: suffixIcon, + canBeEmpty: canBeEmpty, ), ), ); diff --git a/lib/tools/ui/widgets/image_picker_on_tap.dart b/lib/tools/ui/widgets/image_picker_on_tap.dart index 779484f2ae..2e460a3e19 100644 --- a/lib/tools/ui/widgets/image_picker_on_tap.dart +++ b/lib/tools/ui/widgets/image_picker_on_tap.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/tools/constants.dart'; import 'package:titan/tools/functions.dart'; @@ -26,6 +27,8 @@ class ImagePickerOnTap extends StatelessWidget { @override Widget build(BuildContext context) { + final localizeWithContext = AppLocalizations.of(context)!; + return GestureDetector( onTap: () async { final crossFile = await picker.pickImage( @@ -37,7 +40,7 @@ class ImagePickerOnTap extends StatelessWidget { if (size > maxHyperionFileSize) { displayToastWithContext( TypeMsg.error, - TextConstants.imageSizeTooBig, + localizeWithContext.othersImageSizeTooBig, ); } else { if (kIsWeb) { diff --git a/lib/tools/ui/widgets/loader.dart b/lib/tools/ui/widgets/loader.dart index 1de61ddfea..5276035bdf 100644 --- a/lib/tools/ui/widgets/loader.dart +++ b/lib/tools/ui/widgets/loader.dart @@ -6,6 +6,20 @@ class Loader extends StatelessWidget { @override Widget build(BuildContext context) { - return Center(child: CircularProgressIndicator(color: color)); + return LayoutBuilder( + builder: (context, constraints) { + final size = constraints.hasBoundedWidth && constraints.hasBoundedHeight + ? constraints.biggest.shortestSide + : 24.0; + + return Center( + child: SizedBox( + width: size, + height: size, + child: CircularProgressIndicator(color: color), + ), + ); + }, + ); } } diff --git a/lib/tools/ui/widgets/text_entry.dart b/lib/tools/ui/widgets/text_entry.dart index 89bdae241c..790a72bc33 100644 --- a/lib/tools/ui/widgets/text_entry.dart +++ b/lib/tools/ui/widgets/text_entry.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/tools/constants.dart'; class TextEntry extends StatelessWidget { @@ -33,7 +34,7 @@ class TextEntry extends StatelessWidget { this.color = Colors.black, this.enabledColor = Colors.black, this.errorColor = ColorConstants.error, - this.noValueError = TextConstants.noValue, + this.noValueError = "This field is required", this.suffixIcon, this.isNegative = false, this.textInputAction = TextInputAction.next, @@ -41,6 +42,7 @@ class TextEntry extends StatelessWidget { @override Widget build(BuildContext context) { + final localizeWithContext = AppLocalizations.of(context)!; return TextFormField( minLines: minLines, maxLines: maxLines, @@ -54,7 +56,7 @@ class TextEntry extends StatelessWidget { enabled: enabled, decoration: InputDecoration( label: Text( - canBeEmpty ? '$label (optionnel)' : label, + canBeEmpty ? localizeWithContext.globalOptionnal(label) : label, style: TextStyle(color: color, height: 0.5), ), suffix: suffixIcon == null && suffix.isEmpty @@ -95,14 +97,14 @@ class TextEntry extends StatelessWidget { if (isInt) { final intValue = int.tryParse(value); if (intValue == null || (intValue < 0 && !isNegative)) { - return TextConstants.invalidNumber; + return AppLocalizations.of(context)!.toolInvalidNumber; } } if (isDouble) { final doubleValue = double.tryParse(value.replaceAll(',', '.')); if (doubleValue == null || (doubleValue < 0 && !isNegative)) { - return TextConstants.invalidNumber; + return AppLocalizations.of(context)!.toolInvalidNumber; } } diff --git a/lib/tools/ui/widgets/top_bar.dart b/lib/tools/ui/widgets/top_bar.dart index ed7e5badd0..91c913c381 100644 --- a/lib/tools/ui/widgets/top_bar.dart +++ b/lib/tools/ui/widgets/top_bar.dart @@ -2,24 +2,16 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/drawer/class/top_bar_callback.dart'; -import 'package:titan/drawer/providers/animation_provider.dart'; -import 'package:titan/drawer/providers/swipe_provider.dart'; -import 'package:titan/drawer/providers/top_bar_callback_provider.dart'; import 'package:qlevar_router/qlevar_router.dart'; class TopBar extends HookConsumerWidget { - final String title; final String root; - final VoidCallback? onMenu; final VoidCallback? onBack; final Widget? rightIcon; final TextStyle? textStyle; const TopBar({ super.key, - required this.title, required this.root, - this.onMenu, this.onBack, this.rightIcon, this.textStyle, @@ -27,43 +19,27 @@ class TopBar extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final animation = ref.watch(animationProvider); - final topBarCallBackNotifier = ref.watch(topBarCallBackProvider.notifier); - Future(() { - topBarCallBackNotifier.setCallBacks( - TopBarCallback(moduleRoot: root, onMenu: onMenu, onBack: onBack), - ); - }); return Column( children: [ - const SizedBox(height: 15), Row( children: [ SizedBox( width: 70, + height: 30, child: Builder( builder: (BuildContext appBarContext) { + if (QR.currentPath == root) { + return SizedBox.shrink(); + } return IconButton( onPressed: () { - if (QR.currentPath == root) { - if (animation != null) { - final controllerNotifier = ref.watch( - swipeControllerProvider(animation).notifier, - ); - controllerNotifier.toggle(); - onMenu?.call(); - } - } else { - QR.back(); - onBack?.call(); - } + QR.back(); + onBack?.call(); }, icon: HeroIcon( - QR.currentPath == root - ? HeroIcons.bars3BottomLeft - : HeroIcons.chevronLeft, + HeroIcons.chevronLeft, color: textStyle?.color ?? Colors.black, - size: 30, + size: 20, ), ); }, @@ -72,12 +48,12 @@ class TopBar extends HookConsumerWidget { Expanded( child: Center( child: AutoSizeText( - title, + "myemapp", maxLines: 1, style: textStyle ?? const TextStyle( - fontSize: 40, + fontSize: 15, fontWeight: FontWeight.w700, color: Colors.black, ), diff --git a/lib/tools/ui/widgets/vertical_clip_scroll.dart b/lib/tools/ui/widgets/vertical_clip_scroll.dart new file mode 100644 index 0000000000..ec7f029fb4 --- /dev/null +++ b/lib/tools/ui/widgets/vertical_clip_scroll.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +class VerticalClipScroll extends StatelessWidget { + final Widget child; + + const VerticalClipScroll({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return ClipPath( + clipper: _VerticalOnlyClipper(), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + clipBehavior: Clip.none, + child: Align(alignment: Alignment.topCenter, child: child), + ), + ); + } +} + +class _VerticalOnlyClipper extends CustomClipper { + @override + Path getClip(Size size) { + const big = 1000000.0; + return Path() + ..addRect(Rect.fromLTRB(-big, 0.0, size.width + big, size.height)); + } + + @override + bool shouldReclip(covariant _VerticalOnlyClipper oldClipper) => false; +} diff --git a/lib/user/class/applicant.dart b/lib/user/class/applicant.dart index 01a12da170..4dfc31be24 100644 --- a/lib/user/class/applicant.dart +++ b/lib/user/class/applicant.dart @@ -1,4 +1,4 @@ -import 'package:titan/admin/class/account_type.dart'; +import 'package:titan/super_admin/class/account_type.dart'; import 'package:titan/user/class/simple_users.dart'; class Applicant extends SimpleUser { diff --git a/lib/user/class/simple_users.dart b/lib/user/class/simple_users.dart index 453adde035..0f7866abc2 100644 --- a/lib/user/class/simple_users.dart +++ b/lib/user/class/simple_users.dart @@ -1,4 +1,4 @@ -import 'package:titan/admin/class/account_type.dart'; +import 'package:titan/super_admin/class/account_type.dart'; import 'package:titan/tools/functions.dart'; class SimpleUser { diff --git a/lib/user/class/user.dart b/lib/user/class/user.dart index 6f297ce3e4..60b64ca486 100644 --- a/lib/user/class/user.dart +++ b/lib/user/class/user.dart @@ -1,4 +1,4 @@ -import 'package:titan/admin/class/account_type.dart'; +import 'package:titan/super_admin/class/account_type.dart'; import 'package:titan/admin/class/simple_group.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/user/class/applicant.dart'; @@ -19,6 +19,7 @@ class User { required this.phone, required this.createdOn, required this.groups, + required this.isSuperAdmin, }); late final String name; late final String firstname; @@ -32,6 +33,7 @@ class User { late final String? phone; late final DateTime createdOn; late final List groups; + late final bool isSuperAdmin; User.fromJson(Map json) { name = capitaliseAll(json['name']); @@ -56,6 +58,7 @@ class User { groups = List.from( json['groups'], ).map((e) => SimpleGroup.fromJson(e)).toList(); + isSuperAdmin = json['is_super_admin'] ?? false; } Map toJson() { @@ -74,6 +77,7 @@ class User { data['phone'] = phone; data['created_on'] = processDateToAPI(createdOn); data['groups'] = groups.map((e) => e.toJson()).toList(); + data['is_super_admin'] = isSuperAdmin; return data; } @@ -90,6 +94,7 @@ class User { phone = null; createdOn = DateTime.now(); groups = []; + isSuperAdmin = false; } User copyWith({ @@ -105,6 +110,7 @@ class User { String? phone, DateTime? createdOn, List? groups, + bool? isSuperAdmin, }) { return User( name: name ?? this.name, @@ -119,6 +125,7 @@ class User { phone: phone, createdOn: createdOn ?? this.createdOn, groups: groups ?? this.groups, + isSuperAdmin: isSuperAdmin ?? this.isSuperAdmin, ); } diff --git a/lib/vote/class/members.dart b/lib/vote/class/members.dart index b3a7245037..666e4de08a 100644 --- a/lib/vote/class/members.dart +++ b/lib/vote/class/members.dart @@ -1,4 +1,4 @@ -import 'package:titan/admin/class/account_type.dart'; +import 'package:titan/super_admin/class/account_type.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/user/class/simple_users.dart'; diff --git a/lib/vote/providers/is_vote_admin_provider.dart b/lib/vote/providers/is_vote_admin_provider.dart index f7873020f9..9f18dc731a 100644 --- a/lib/vote/providers/is_vote_admin_provider.dart +++ b/lib/vote/providers/is_vote_admin_provider.dart @@ -5,5 +5,5 @@ final isVoteAdminProvider = StateProvider((ref) { final me = ref.watch(userProvider); return me.groups .map((e) => e.id) - .contains("6c6d7e88-fdb8-4e42-b2b5-3d3cfd12e7d6"); + .contains("2ca57402-605b-4389-a471-f2fea7b27db5"); // admin_vote }); diff --git a/lib/vote/router.dart b/lib/vote/router.dart index e6ca8b0e72..d7c6b0e619 100644 --- a/lib/vote/router.dart +++ b/lib/vote/router.dart @@ -1,7 +1,7 @@ -import 'package:either_dart/either.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:titan/drawer/class/module.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/navigation/class/module.dart'; import 'package:titan/tools/middlewares/admin_middleware.dart'; import 'package:titan/tools/middlewares/authenticated_middleware.dart'; import 'package:titan/tools/middlewares/deferred_middleware.dart'; @@ -26,10 +26,10 @@ class VoteRouter { static const String addSection = '/add_edit_section'; static const String detail = '/detail'; static final Module module = Module( - name: "Vote", - icon: const Left(HeroIcons.envelopeOpen), + getName: (context) => AppLocalizations.of(context)!.moduleVote, + getDescription: (context) => + AppLocalizations.of(context)!.moduleVoteDescription, root: VoteRouter.root, - selected: false, ); VoteRouter(this.ref); @@ -41,6 +41,10 @@ class VoteRouter { AuthenticatedMiddleware(ref), DeferredLoadingMiddleware(main_page.loadLibrary), ], + pageType: QCustomPage( + transitionsBuilder: (_, animation, _, child) => + FadeTransition(opacity: animation, child: child), + ), children: [ QRoute( path: admin, diff --git a/lib/vote/tools/constants.dart b/lib/vote/tools/constants.dart deleted file mode 100644 index be2ab035f0..0000000000 --- a/lib/vote/tools/constants.dart +++ /dev/null @@ -1,90 +0,0 @@ -class VoteTextConstants { - static const String add = 'Ajouter'; - static const String addMember = 'Ajouter un membre'; - static const String addedPretendance = 'Liste ajoutée'; - static const String addedSection = 'Section ajoutée'; - static const String addingError = 'Erreur lors de l\'ajout'; - static const String addPretendance = 'Ajouter une liste'; - static const String addSection = 'Ajouter une section'; - static const String all = "Tous"; - static const String alreadyAddedMember = 'Membre déjà ajouté'; - static const String alreadyVoted = "Vote enregistré"; - static const String chooseList = 'Choisir une liste'; - static const String clear = 'Réinitialiser'; - static const String clearVotes = "Réinitialiser les votes"; - static const String closedVote = 'Votes clos'; - static const String closeVote = 'Fermer les votes'; - static const String confirmVote = 'Confirmer le vote'; - static const String countVote = 'Dépouiller les votes'; - static const String deletedAll = "Tout supprimé"; - static const String deletedPipo = "Listes pipos supprimées"; - static const String deletedSection = 'Section supprimée'; - static const String deleteAll = "Supprimer tout"; - static const String deleteAllDescription = - "Voulez-vous vraiment supprimer tout ?"; - static const String deletePipo = "Supprimer les listes pipos"; - static const String deletePipoDescription = - "Voulez-vous vraiment supprimer les listes pipos ?"; - static const String deletePretendance = 'Supprimer la liste'; - static const String deletePretendanceDesc = - 'Voulez-vous vraiment supprimer cette liste ?'; - static const String deleteSection = 'Supprimer la section'; - static const String deleteSectionDescription = - 'Voulez-vous vraiment supprimer cette section ?'; - static const String deletingError = 'Erreur lors de la suppression'; - static const String description = 'Description'; - static const String edit = 'Modifier'; - static const String editedPretendance = 'Liste modifiée'; - static const String editedSection = 'Section modifiée'; - static const String editingError = 'Erreur lors de la modification'; - static const String errorClosingVotes = - 'Erreur lors de la fermeture des votes'; - static const String errorCountingVotes = - 'Erreur lors du dépouillement des votes'; - static const String errorResetingVotes = - 'Erreur lors de la réinitialisation des votes'; - static const String errorOpeningVotes = - 'Erreur lors de l\'ouverture des votes'; - static const String incorrectOrMissingFields = - 'Champs incorrects ou manquants'; - static const String members = 'Membres'; - static const String name = 'Nom'; - static const String noPretendanceList = 'Aucune liste de prétendance'; - static const String noSection = 'Aucune section'; - static const String canNotVote = 'Vous ne pouvez pas voter'; - static const String noSectionList = 'Aucune section'; - static const String notOpenedVote = 'Vote non ouvert'; - static const String onGoingCount = 'Dépouillement en cours'; - static const String openVote = 'Ouvrir les votes'; - static const String pipo = "Pipo"; - static const String pretendance = 'Listes'; - static const String pretendanceDeleted = 'Prétendance supprimée'; - static const String pretendanceNotDeleted = 'Erreur lors de la suppression'; - static const String program = 'Programme'; - static const String publish = 'Publier'; - static const String publishVoteDescription = - 'Voulez-vous vraiment publier les votes ?'; - static const String resetedVotes = 'Votes réinitialisés'; - static const String resetVote = 'Réinitialiser les votes'; - static const String resetVoteDescription = "Que voulez-vous faire ?"; - static const String role = 'Rôle'; - static const String sectionDescription = 'Description de la section'; - static const String section = 'Section'; - static const String sectionName = 'Nom de la section'; - static const String seeMore = 'Voir plus'; - static const String selected = 'Sélectionné'; - static const String showVotes = 'Voir les votes'; - static const String vote = 'Vote'; - static const String voteError = 'Erreur lors de l\'enregistrement du vote'; - static const String voteFor = 'Voter pour '; - static const String voteNotStarted = 'Vote non ouvert'; - static const String voters = 'Groupes votants'; - static const String voteSuccess = 'Vote enregistré'; - static const String votes = 'Voix'; - static const String votesClosed = 'Votes clos'; - static const String votesCounted = 'Votes dépouillés'; - static const String votesOpened = 'Votes ouverts'; - static const String warning = "Attention"; - static const String warningMessage = - "La sélection ne sera pas sauvegardée.\nVoulez-vous continuer ?"; -} diff --git a/lib/vote/ui/components/member_card.dart b/lib/vote/ui/components/member_card.dart index 0a9f7924fc..de7ae76e56 100644 --- a/lib/vote/ui/components/member_card.dart +++ b/lib/vote/ui/components/member_card.dart @@ -1,99 +1,59 @@ -import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:titan/tools/ui/layouts/card_button.dart'; -import 'package:titan/tools/ui/layouts/card_layout.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; +import 'package:titan/tools/ui/styleguide/list_item.dart'; import 'package:titan/vote/class/members.dart'; -class MemberCard extends StatelessWidget { +class MemberCard extends ConsumerWidget { final Member member; - final Function()? onEdit, onDelete; + final Function() onEdit, onDelete; final bool isAdmin; const MemberCard({ super.key, required this.member, - this.onEdit, - this.onDelete, + required this.onEdit, + required this.onDelete, this.isAdmin = false, }); @override - Widget build(BuildContext context) { - return CardLayout( - id: member.id, - width: 150, - height: isAdmin ? 145 : 110, - margin: const EdgeInsets.all(10), - padding: const EdgeInsets.symmetric(horizontal: 17.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 10), - AutoSizeText( - member.nickname ?? member.firstname, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - const SizedBox(height: 2), - AutoSizeText( - member.nickname != null - ? '${member.firstname} ${member.name}' - : member.name, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.bold, - color: Colors.grey.shade400, - ), - ), - const SizedBox(height: 2), - if (!isAdmin) const Spacer(), - AutoSizeText( - member.role, - maxLines: 1, - minFontSize: 10, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - if (isAdmin) const Spacer(), - if (isAdmin) - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - onTap: onEdit, - child: CardButton( - color: Colors.grey.shade200, - shadowColor: Colors.grey.withValues(alpha: 0.2), - child: const HeroIcon( - HeroIcons.pencil, - color: Colors.black, - ), - ), - ), - GestureDetector( - onTap: onDelete, - child: const CardButton( - color: Colors.black, - child: HeroIcon(HeroIcons.trash, color: Colors.white), + Widget build(BuildContext context, WidgetRef ref) { + return ListItem( + title: member.getName(), + subtitle: member.role, + onTap: isAdmin + ? () async { + FocusScope.of(context).unfocus(); + final ctx = context; + await Future.delayed(Duration(milliseconds: 150)); + if (!ctx.mounted) return; + + await showCustomBottomModal( + context: ctx, + ref: ref, + modal: BottomModalTemplate( + title: member.getName(), + description: member.role, + child: Column( + children: [ + const SizedBox(height: 20), + Button( + text: AppLocalizations.of(context)!.voteEdit, + onPressed: onEdit, + ), + const SizedBox(height: 20), + Button.danger( + text: AppLocalizations.of(context)!.voteDelete, + onPressed: onDelete, + ), + ], ), ), - ], - ), - SizedBox(height: isAdmin ? 10 : 15), - ], - ), + ); + } + : null, ); } } diff --git a/lib/vote/ui/pages/admin_page/admin_page.dart b/lib/vote/ui/pages/admin_page/admin_page.dart index 2f2c4b9d13..8bce2d5529 100644 --- a/lib/vote/ui/pages/admin_page/admin_page.dart +++ b/lib/vote/ui/pages/admin_page/admin_page.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/amap/tools/constants.dart'; +import 'package:qlevar_router/qlevar_router.dart'; import 'package:titan/tools/constants.dart'; -import 'package:titan/tools/ui/layouts/card_layout.dart'; +import 'package:titan/tools/ui/styleguide/icon_button.dart'; import 'package:titan/tools/ui/widgets/align_left_text.dart'; import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; import 'package:titan/tools/functions.dart'; @@ -13,20 +13,24 @@ import 'package:titan/tools/ui/builders/waiting_button.dart'; import 'package:titan/user/providers/user_list_provider.dart'; import 'package:titan/vote/class/contender.dart'; import 'package:titan/vote/providers/contender_list_provider.dart'; +import 'package:titan/vote/providers/contender_members.dart'; +import 'package:titan/vote/providers/contender_provider.dart'; import 'package:titan/vote/providers/result_provider.dart'; import 'package:titan/vote/providers/sections_contender_provider.dart'; import 'package:titan/vote/providers/sections_provider.dart'; import 'package:titan/vote/providers/show_graph_provider.dart'; import 'package:titan/vote/providers/status_provider.dart'; import 'package:titan/vote/repositories/status_repository.dart'; -import 'package:titan/vote/tools/constants.dart'; +import 'package:titan/vote/router.dart'; import 'package:titan/vote/ui/pages/admin_page/admin_button.dart'; +import 'package:titan/vote/ui/pages/admin_page/opening_vote.dart'; import 'package:titan/vote/ui/pages/admin_page/section_bar.dart'; import 'package:titan/vote/ui/pages/admin_page/section_contender_items.dart'; import 'package:titan/vote/ui/pages/admin_page/vote_bars.dart'; import 'package:titan/vote/ui/pages/admin_page/vote_count.dart'; import 'package:titan/vote/ui/pages/admin_page/voters_bar.dart'; import 'package:titan/vote/ui/vote.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AdminPage extends HookConsumerWidget { const AdminPage({super.key}); @@ -36,6 +40,8 @@ class AdminPage extends HookConsumerWidget { final sectionContenderListNotifier = ref.watch( sectionContenderProvider.notifier, ); + final membersNotifier = ref.read(contenderMembersProvider.notifier); + final contenderNotifier = ref.read(contenderProvider.notifier); final sectionsNotifier = ref.watch(sectionsProvider.notifier); final contenderList = ref.watch(contenderListProvider); final asyncStatus = ref.watch(statusProvider); @@ -51,6 +57,7 @@ class AdminPage extends HookConsumerWidget { return VoteTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await statusNotifier.loadStatus(); if (status == Status.counting || status == Status.published) { @@ -78,489 +85,342 @@ class AdminPage extends HookConsumerWidget { } }); }, - child: Padding( - padding: const EdgeInsets.only(top: 10.0), - child: Column( - children: [ - const SizedBox(height: 20), - const SectionBar(), - const SizedBox(height: 30), - const AlignLeftText( - VoteTextConstants.voters, - padding: EdgeInsets.symmetric(horizontal: 30.0), - color: Color.fromARGB(255, 149, 149, 149), - ), - const SizedBox(height: 30), - const VotersBar(), - const SizedBox(height: 30), - const AlignLeftText( - VoteTextConstants.pretendance, - padding: EdgeInsets.symmetric(horizontal: 30.0), - color: Colors.grey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Text( + AppLocalizations.of(context)!.adminAdministration, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorConstants.title, + ), ), - const SizedBox(height: 10), - const SectionContenderItems(), - const SizedBox(height: 20), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 30.0), - child: Align( - alignment: Alignment.centerLeft, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - VoteTextConstants.vote, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.grey, - ), - ), - if (showVotes && status == Status.counting) - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - GestureDetector( - onTap: () { - showVotesNotifier.toggle(false); - }, - child: const HeroIcon( - HeroIcons.eyeSlash, - size: 25.0, - color: Colors.black, - ), - ), - WaitingButton( - builder: (child) => AdminButton(child: child), - onTap: () async { - await showDialog( - context: context, - builder: (context) => CustomDialogBox( - title: VoteTextConstants.publish, - descriptions: VoteTextConstants - .publishVoteDescription, - onYes: () { - statusNotifier.publishVote(); - ref - .watch(resultProvider.notifier) - .loadResult(); - }, - ), - ); - }, - child: const Text( - VoteTextConstants.publish, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - ], - ), - ), - if (status == Status.counting || - status == Status.published) - WaitingButton( - builder: (child) => AdminButton(child: child), - onTap: () async { - await showDialog( - context: context, - builder: (context) { - return CustomDialogBox( - title: VoteTextConstants.resetVote, - descriptions: - VoteTextConstants.resetVoteDescription, - onYes: () async { - await tokenExpireWrapper(ref, () async { - final value = await statusNotifier - .resetVote(); - ref - .watch(contenderListProvider.notifier) - .loadContenderList(); - if (value) { - showVotesNotifier.toggle(false); - displayVoteToastWithContext( - TypeMsg.msg, - VoteTextConstants.resetedVotes, - ); - } else { - displayVoteToastWithContext( - TypeMsg.error, - VoteTextConstants.errorResetingVotes, - ); - } - }); - }, - ); - }, - ); - }, - child: const Text( - VoteTextConstants.clear, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - ], + ), + const SizedBox(height: 20), + AlignLeftText( + AppLocalizations.of(context)!.feedAssociation, + padding: const EdgeInsets.symmetric(horizontal: 20.0), + color: ColorConstants.tertiary, + ), + const SizedBox(height: 10), + const SectionBar(), + const SizedBox(height: 20), + AlignLeftText( + AppLocalizations.of(context)!.voteVoters, + padding: const EdgeInsets.symmetric(horizontal: 20.0), + color: ColorConstants.tertiary, + ), + const SizedBox(height: 10), + const VotersBar(), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + AppLocalizations.of(context)!.votePretendance, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: ColorConstants.tertiary, + ), ), - ), + const Spacer(), + if (status == Status.waiting) + CustomIconButton( + icon: HeroIcon( + HeroIcons.plus, + color: ColorConstants.background, + ), + onPressed: () { + contenderNotifier.setId(Contender.empty()); + membersNotifier.setMembers([]); + QR.to( + VoteRouter.root + + VoteRouter.admin + + VoteRouter.addEditContender, + ); + }, + ), + ], ), - const SizedBox(height: 20), - SizedBox( - height: - MediaQuery.of(context).size.height - - 500 + - (status == Status.waiting ? 0 : 50), - child: Column( + ), + const SizedBox(height: 10), + const SectionContenderItems(), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Align( + alignment: Alignment.centerLeft, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - if (status == Status.counting) - showVotes - ? const VoteBars() - : GestureDetector( + Text( + AppLocalizations.of(context)!.voteVote, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: ColorConstants.tertiary, + ), + ), + if (showVotes && status == Status.counting) + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + GestureDetector( onTap: () { - showVotesNotifier.toggle(true); + showVotesNotifier.toggle(false); }, - behavior: HitTestBehavior.opaque, - child: const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox(height: 40), - HeroIcon( - HeroIcons.eye, - size: 80.0, - color: Colors.black, - ), - SizedBox(height: 40), - Text( - VoteTextConstants.showVotes, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - ], - ), - ), - if (status == Status.published) const VoteBars(), - if (status == Status.closed) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 30.0, - vertical: 50, - ), - child: WaitingButton( - builder: (child) => CardLayout( - padding: const EdgeInsets.only(top: 10, bottom: 12), - width: double.infinity, - colors: [Colors.grey.shade900, Colors.black], - child: child, - ), - onTap: () async { - await tokenExpireWrapper(ref, () async { - final value = await statusNotifier.countVote(); - if (value) { - displayVoteToastWithContext( - TypeMsg.msg, - VoteTextConstants.votesCounted, - ); - } else { - displayVoteToastWithContext( - TypeMsg.error, - VoteTextConstants.errorCountingVotes, - ); - } - }); - }, - child: const Center( - child: Text( - VoteTextConstants.countVote, - style: TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.w700, + child: const HeroIcon( + HeroIcons.eyeSlash, + size: 25.0, + color: ColorConstants.tertiary, ), ), - ), - ), - ), - if (status == Status.open) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 30.0, - vertical: 50, - ), - child: WaitingButton( - builder: (child) => CardLayout( - padding: const EdgeInsets.only(top: 10, bottom: 12), - width: double.infinity, - colors: const [ - ColorConstants.gradient1, - ColorConstants.gradient2, - ], - child: child, - ), - onTap: () async { - await tokenExpireWrapper(ref, () async { - final value = await statusNotifier.closeVote(); - if (value) { - displayVoteToastWithContext( - TypeMsg.msg, - VoteTextConstants.votesClosed, - ); - } else { - displayVoteToastWithContext( - TypeMsg.error, - VoteTextConstants.errorClosingVotes, + WaitingButton( + builder: (child) => AdminButton(child: child), + onTap: () async { + await showDialog( + context: context, + builder: (context) => CustomDialogBox( + title: AppLocalizations.of( + context, + )!.votePublish, + descriptions: AppLocalizations.of( + context, + )!.votePublishVoteDescription, + onYes: () { + statusNotifier.publishVote(); + ref + .watch(resultProvider.notifier) + .loadResult(); + }, + ), ); - } - }); - }, - child: const Center( - child: Text( - VoteTextConstants.closeVote, - style: TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.w700, + }, + child: Text( + AppLocalizations.of(context)!.votePublish, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: ColorConstants.background, + ), ), ), - ), + ], ), ), - if (status == Status.waiting) - Expanded( - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 30.0, - vertical: 50, - ), - child: Column( - children: [ - WaitingButton( - waitingColor: Colors.black, - builder: (child) => CardLayout( - padding: const EdgeInsets.only( - top: 10, - bottom: 12, - ), - margin: const EdgeInsets.all(0), - color: Colors.white, - borderColor: Colors.black, - child: child, - ), - onTap: () async { + if (status == Status.counting || status == Status.published) + WaitingButton( + builder: (child) => AdminButton(child: child), + onTap: () async { + await showDialog( + context: context, + builder: (context) { + return CustomDialogBox( + title: AppLocalizations.of( + context, + )!.voteResetVote, + descriptions: AppLocalizations.of( + context, + )!.voteResetVoteDescription, + onYes: () async { await tokenExpireWrapper(ref, () async { + final resetedVotesMsg = AppLocalizations.of( + context, + )!.voteResetedVotes; + final resetedVotesErrorMsg = + AppLocalizations.of( + context, + )!.voteErrorResetingVotes; final value = await statusNotifier - .openVote(); + .resetVote(); ref .watch(contenderListProvider.notifier) .loadContenderList(); if (value) { + showVotesNotifier.toggle(false); displayVoteToastWithContext( TypeMsg.msg, - VoteTextConstants.votesOpened, + resetedVotesMsg, ); } else { displayVoteToastWithContext( TypeMsg.error, - VoteTextConstants.errorOpeningVotes, + resetedVotesErrorMsg, ); } }); }, - child: const Center( - child: Text( - VoteTextConstants.openVote, - style: TextStyle( - color: Colors.black, - fontSize: 20, - fontWeight: FontWeight.w700, - ), - ), - ), + ); + }, + ); + }, + child: Text( + AppLocalizations.of(context)!.voteClear, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: ColorConstants.background, + ), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 20), + Column( + children: [ + if (status == Status.counting) + showVotes + ? const VoteBars() + : GestureDetector( + onTap: () { + showVotesNotifier.toggle(true); + }, + behavior: HitTestBehavior.opaque, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 40), + const HeroIcon( + HeroIcons.eye, + size: 80.0, + color: ColorConstants.tertiary, ), - const SizedBox(height: 50), - SizedBox( - width: double.infinity, - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: WaitingButton( - builder: (child) => CardLayout( - padding: const EdgeInsets.only( - top: 10, - bottom: 12, - ), - margin: const EdgeInsets.all(0), - colors: const [ - AMAPColorConstants.redGradient1, - AMAPColorConstants.redGradient2, - ], - borderColor: Colors.white, - child: child, - ), - onTap: () async { - await showDialog( - context: context, - builder: (context) => CustomDialogBox( - title: - VoteTextConstants.deleteAll, - descriptions: VoteTextConstants - .deleteAllDescription, - onYes: () async { - await tokenExpireWrapper( - ref, - () async { - final value = await ref - .watch( - contenderListProvider - .notifier, - ) - .deleteContenders(); - if (value) { - displayVoteToastWithContext( - TypeMsg.msg, - VoteTextConstants - .deletedAll, - ); - } else { - displayVoteToastWithContext( - TypeMsg.error, - VoteTextConstants - .deletingError, - ); - } - }, - ); - }, - ), - ); - }, - child: const Center( - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Text( - VoteTextConstants.all, - style: TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.w700, - ), - ), - SizedBox(width: 10), - HeroIcon( - HeroIcons.trash, - color: Colors.white, - size: 20, - ), - ], - ), - ), - ), - ), - const SizedBox(width: 20), - Expanded( - child: WaitingButton( - builder: (child) => CardLayout( - padding: const EdgeInsets.only( - top: 10, - bottom: 12, - ), - margin: const EdgeInsets.all(0), - colors: const [ - AMAPColorConstants.redGradient1, - AMAPColorConstants.redGradient2, - ], - borderColor: Colors.white, - child: child, - ), - onTap: () async { - await showDialog( - context: context, - builder: (context) => CustomDialogBox( - title: - VoteTextConstants.deletePipo, - descriptions: VoteTextConstants - .deletePipoDescription, - onYes: () async { - await tokenExpireWrapper( - ref, - () async { - final value = await ref - .watch( - contenderListProvider - .notifier, - ) - .deleteContenders( - type: ListType.fake, - ); - if (value) { - displayVoteToastWithContext( - TypeMsg.msg, - VoteTextConstants - .deletedPipo, - ); - } else { - displayVoteToastWithContext( - TypeMsg.error, - VoteTextConstants - .deletingError, - ); - } - }, - ); - }, - ), - ); - }, - child: const Center( - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Text( - VoteTextConstants.pipo, - style: TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.w700, - ), - ), - SizedBox(width: 10), - HeroIcon( - HeroIcons.trash, - color: Colors.white, - size: 20, - ), - ], - ), - ), - ), - ), - ], + const SizedBox(height: 40), + Text( + AppLocalizations.of(context)!.voteShowVotes, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: ColorConstants.tertiary, ), ), ], ), ), + if (status == Status.published) const VoteBars(), + if (status == Status.closed) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 30.0, + vertical: 50, + ), + child: WaitingButton( + builder: (child) => Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 10, + ), + decoration: BoxDecoration( + color: ColorConstants.tertiary, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: ColorConstants.onTertiary), + ), + child: child, ), - if (status == Status.open) const VoteCount(), - ], - ), - ), - ], - ), + onTap: () async { + await tokenExpireWrapper(ref, () async { + final votesCountedMsg = AppLocalizations.of( + context, + )!.voteVotesCounted; + final errorCountingVotesMsg = AppLocalizations.of( + context, + )!.voteErrorCountingVotes; + final value = await statusNotifier.countVote(); + if (value) { + displayVoteToastWithContext( + TypeMsg.msg, + votesCountedMsg, + ); + } else { + displayVoteToastWithContext( + TypeMsg.error, + errorCountingVotesMsg, + ); + } + }); + }, + child: Center( + child: Text( + AppLocalizations.of(context)!.voteCountVote, + style: const TextStyle( + color: ColorConstants.background, + fontSize: 20, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ), + if (status == Status.open) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 30.0, + vertical: 50, + ), + child: WaitingButton( + builder: (child) => Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 10, + ), + decoration: BoxDecoration( + color: ColorConstants.main, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: ColorConstants.mainBorder), + ), + child: child, + ), + onTap: () async { + await tokenExpireWrapper(ref, () async { + final closeVotesMsg = AppLocalizations.of( + context, + )!.voteVotesClosed; + final errorClosingVotesMsg = AppLocalizations.of( + context, + )!.voteErrorClosingVotes; + final value = await statusNotifier.closeVote(); + if (value) { + displayVoteToastWithContext( + TypeMsg.msg, + closeVotesMsg, + ); + } else { + displayVoteToastWithContext( + TypeMsg.error, + errorClosingVotesMsg, + ); + } + }); + }, + child: Center( + child: Text( + AppLocalizations.of(context)!.voteCloseVote, + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ), + if (status == Status.waiting) const OpeningVote(), + if (status == Status.open) const VoteCount(), + ], + ), + ], ), ), ); diff --git a/lib/vote/ui/pages/admin_page/contender_card.dart b/lib/vote/ui/pages/admin_page/contender_card.dart index 4cf55d1e8e..aabd22250f 100644 --- a/lib/vote/ui/pages/admin_page/contender_card.dart +++ b/lib/vote/ui/pages/admin_page/contender_card.dart @@ -1,139 +1,63 @@ -import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; -import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:titan/tools/functions.dart'; -import 'package:titan/tools/ui/layouts/card_button.dart'; -import 'package:titan/tools/ui/layouts/card_layout.dart'; -import 'package:titan/tools/ui/builders/waiting_button.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/button.dart'; +import 'package:titan/tools/ui/styleguide/list_item.dart'; import 'package:titan/vote/class/contender.dart'; -import 'package:titan/vote/providers/contender_provider.dart'; -import 'package:titan/vote/providers/status_provider.dart'; -import 'package:titan/vote/repositories/status_repository.dart'; -import 'package:titan/vote/router.dart'; import 'package:titan/vote/ui/components/contender_logo.dart'; -import 'package:qlevar_router/qlevar_router.dart'; class ContenderCard extends HookConsumerWidget { final Contender contender; final bool isAdmin, isDetail; - final Function()? onEdit; - final Future Function()? onDelete; + final Function() onEdit; + final Future Function() onDelete; const ContenderCard({ super.key, required this.contender, - this.onEdit, - this.onDelete, + required this.onEdit, + required this.onDelete, this.isAdmin = false, this.isDetail = false, }); @override Widget build(BuildContext context, WidgetRef ref) { - final contenderNotifier = ref.watch(contenderProvider.notifier); - final status = ref - .watch(statusProvider) - .maybeWhen(data: (status) => status, orElse: () => Status.waiting); - return CardLayout( - id: contender.id, - width: 250, - height: - (contender.listType != ListType.blank && - status == Status.waiting && - isAdmin) - ? 180 - : 130, - padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 15), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ContenderLogo(contender), - const SizedBox(width: 10), - Expanded( - child: Column( - children: [ - AutoSizeText( - contender.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - Text( - capitalize(contender.listType.toString().split('.').last), - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.bold, - color: Colors.black, + return ListItem( + title: contender.name, + subtitle: contender.listType.name, + icon: ContenderLogo(contender), + onTap: isAdmin + ? () async { + FocusScope.of(context).unfocus(); + final ctx = context; + await Future.delayed(Duration(milliseconds: 150)); + if (!ctx.mounted) return; + + await showCustomBottomModal( + context: ctx, + ref: ref, + modal: BottomModalTemplate( + title: contender.name, + description: contender.program, + child: Column( + children: [ + const SizedBox(height: 20), + Button( + text: AppLocalizations.of(context)!.voteEdit, + onPressed: onEdit, ), - ), - const SizedBox(height: 3), - ], - ), - ), - const SizedBox(width: 5), - isDetail || contender.listType == ListType.blank - ? Container(width: 30) - : GestureDetector( - onTap: () { - contenderNotifier.setId(contender); - QR.to(VoteRouter.root + VoteRouter.detail); - }, - child: const HeroIcon( - HeroIcons.informationCircle, - color: Colors.black, - size: 25, + const SizedBox(height: 20), + Button.danger( + text: AppLocalizations.of(context)!.voteDelete, + onPressed: onDelete, ), - ), - ], - ), - Center( - child: Text( - contender.description, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Colors.grey.shade400, - ), - ), - ), - const Spacer(), - if (contender.listType != ListType.blank && - status == Status.waiting && - isAdmin) - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - onTap: onEdit, - child: CardButton( - color: Colors.grey.shade200, - shadowColor: Colors.grey.withValues(alpha: 0.2), - child: const HeroIcon( - HeroIcons.pencil, - color: Colors.black, - ), + ], ), ), - WaitingButton( - builder: (child) => - CardButton(color: Colors.black, child: child), - onTap: onDelete, - child: const HeroIcon(HeroIcons.trash, color: Colors.white), - ), - ], - ), - ], - ), + ); + } + : null, ); } } diff --git a/lib/vote/ui/pages/admin_page/opening_vote.dart b/lib/vote/ui/pages/admin_page/opening_vote.dart new file mode 100644 index 0000000000..95c7ed0e2c --- /dev/null +++ b/lib/vote/ui/pages/admin_page/opening_vote.dart @@ -0,0 +1,215 @@ +import 'package:flutter/material.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; +import 'package:titan/tools/ui/builders/waiting_button.dart'; +import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; +import 'package:titan/vote/class/contender.dart'; +import 'package:titan/vote/providers/contender_list_provider.dart'; +import 'package:titan/vote/providers/status_provider.dart'; + +class OpeningVote extends ConsumerWidget { + const OpeningVote({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final statusNotifier = ref.watch(statusProvider.notifier); + + void displayVoteToastWithContext(TypeMsg type, String msg) { + displayToast(context, type, msg); + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Column( + children: [ + WaitingButton( + waitingColor: ColorConstants.background, + builder: (child) => Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: ColorConstants.tertiary, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: ColorConstants.onTertiary), + ), + child: child, + ), + onTap: () async { + await tokenExpireWrapper(ref, () async { + final openVotesMsg = AppLocalizations.of( + context, + )!.voteVotesOpened; + final errorOpeningVotesMsg = AppLocalizations.of( + context, + )!.voteErrorOpeningVotes; + final value = await statusNotifier.openVote(); + ref.watch(contenderListProvider.notifier).loadContenderList(); + if (value) { + displayVoteToastWithContext(TypeMsg.msg, openVotesMsg); + } else { + displayVoteToastWithContext( + TypeMsg.error, + errorOpeningVotesMsg, + ); + } + }); + }, + child: Center( + child: Text( + AppLocalizations.of(context)!.voteOpenVote, + style: const TextStyle( + color: ColorConstants.background, + fontSize: 20, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + const SizedBox(height: 20), + WaitingButton( + builder: (child) => Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: ColorConstants.main, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: ColorConstants.mainBorder), + ), + child: child, + ), + onTap: () async { + await showDialog( + context: context, + builder: (context) => CustomDialogBox( + title: AppLocalizations.of(context)!.voteDeleteAll, + descriptions: AppLocalizations.of( + context, + )!.voteDeleteAllDescription, + onYes: () async { + final deleteAllVotesMsg = AppLocalizations.of( + context, + )!.voteDeletedAll; + final errorDeletingVotesMsg = AppLocalizations.of( + context, + )!.voteDeletingError; + await tokenExpireWrapper(ref, () async { + final value = await ref + .watch(contenderListProvider.notifier) + .deleteContenders(); + if (value) { + displayVoteToastWithContext( + TypeMsg.msg, + deleteAllVotesMsg, + ); + } else { + displayVoteToastWithContext( + TypeMsg.error, + errorDeletingVotesMsg, + ); + } + }); + }, + ), + ); + }, + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + AppLocalizations.of(context)!.voteAll, + style: const TextStyle( + color: ColorConstants.background, + fontSize: 20, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(width: 10), + const HeroIcon( + HeroIcons.trash, + color: ColorConstants.background, + size: 20, + ), + ], + ), + ), + ), + const SizedBox(height: 20), + WaitingButton( + builder: (child) => Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: ColorConstants.main, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: ColorConstants.mainBorder), + ), + child: child, + ), + onTap: () async { + await showDialog( + context: context, + builder: (context) => CustomDialogBox( + title: AppLocalizations.of(context)!.voteDeletePipo, + descriptions: AppLocalizations.of( + context, + )!.voteDeletePipoDescription, + onYes: () async { + final deletePipoVotesMsg = AppLocalizations.of( + context, + )!.voteDeletedPipo; + final errorDeletingPipoVotesMsg = AppLocalizations.of( + context, + )!.voteDeletingError; + await tokenExpireWrapper(ref, () async { + final value = await ref + .watch(contenderListProvider.notifier) + .deleteContenders(type: ListType.fake); + if (value) { + displayVoteToastWithContext( + TypeMsg.msg, + deletePipoVotesMsg, + ); + } else { + displayVoteToastWithContext( + TypeMsg.error, + errorDeletingPipoVotesMsg, + ); + } + }); + }, + ), + ); + }, + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + AppLocalizations.of(context)!.votePipo, + style: const TextStyle( + color: ColorConstants.background, + fontSize: 20, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(width: 10), + const HeroIcon( + HeroIcons.trash, + color: ColorConstants.background, + size: 20, + ), + ], + ), + ), + ), + const SizedBox(height: 20), + ], + ), + ); + } +} diff --git a/lib/vote/ui/pages/admin_page/section_bar.dart b/lib/vote/ui/pages/admin_page/section_bar.dart index b710adb538..a316a7e7b2 100644 --- a/lib/vote/ui/pages/admin_page/section_bar.dart +++ b/lib/vote/ui/pages/admin_page/section_bar.dart @@ -1,20 +1,20 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/tools/ui/styleguide/item_chip.dart'; import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; -import 'package:titan/tools/ui/layouts/item_chip.dart'; import 'package:titan/vote/providers/section_id_provider.dart'; import 'package:titan/vote/providers/sections_contender_provider.dart'; import 'package:titan/vote/providers/sections_provider.dart'; import 'package:titan/vote/providers/status_provider.dart'; import 'package:titan/vote/repositories/status_repository.dart'; import 'package:titan/vote/router.dart'; -import 'package:titan/vote/tools/constants.dart'; import 'package:titan/vote/ui/pages/admin_page/section_chip.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class SectionBar extends HookConsumerWidget { const SectionBar({super.key}); @@ -36,7 +36,7 @@ class SectionBar extends HookConsumerWidget { } return HorizontalListView.builder( - height: 40, + height: 50, items: sectionContender.keys.toList(), firstChild: (status == Status.waiting) ? ItemChip( @@ -62,20 +62,28 @@ class SectionBar extends HookConsumerWidget { await showDialog( context: context, builder: (context) => CustomDialogBox( - title: VoteTextConstants.deleteSection, - descriptions: VoteTextConstants.deleteSectionDescription, + title: AppLocalizations.of(context)!.voteDeleteSection, + descriptions: AppLocalizations.of( + context, + )!.voteDeleteSectionDescription, onYes: () async { + final deleteSectionSuccessMsg = AppLocalizations.of( + context, + )!.voteDeletedSection; + final deleteSectionErrorMsg = AppLocalizations.of( + context, + )!.voteDeletingError; final result = await sectionsNotifier.deleteSection(key); if (result) { sectionContenderListNotifier.deleteT(key); displayVoteToastWithContext( TypeMsg.msg, - VoteTextConstants.deletedSection, + deleteSectionSuccessMsg, ); } else { displayVoteToastWithContext( TypeMsg.error, - VoteTextConstants.deletingError, + deleteSectionErrorMsg, ); } }, diff --git a/lib/vote/ui/pages/admin_page/section_chip.dart b/lib/vote/ui/pages/admin_page/section_chip.dart index bb2798e522..6464e2e95b 100644 --- a/lib/vote/ui/pages/admin_page/section_chip.dart +++ b/lib/vote/ui/pages/admin_page/section_chip.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:titan/tools/constants.dart'; class SectionChip extends StatelessWidget { final bool selected, isAdmin; @@ -18,11 +19,14 @@ class SectionChip extends StatelessWidget { return GestureDetector( onTap: onTap, child: Container( - margin: const EdgeInsets.symmetric(horizontal: 10.0), + margin: EdgeInsets.symmetric(horizontal: 5.0), padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 20.0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(30.0), - color: selected ? Colors.black : Colors.grey.shade200, + border: Border.all(color: ColorConstants.onTertiary), + color: selected + ? ColorConstants.onTertiary + : ColorConstants.background, ), child: Row( children: [ diff --git a/lib/vote/ui/pages/admin_page/section_contender_items.dart b/lib/vote/ui/pages/admin_page/section_contender_items.dart index efb9761360..4dcd05ea8a 100644 --- a/lib/vote/ui/pages/admin_page/section_contender_items.dart +++ b/lib/vote/ui/pages/admin_page/section_contender_items.dart @@ -1,24 +1,18 @@ import 'package:flutter/material.dart'; -import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; -import 'package:titan/tools/ui/layouts/card_layout.dart'; import 'package:titan/tools/ui/widgets/custom_dialog_box.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; -import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; -import 'package:titan/vote/class/contender.dart'; import 'package:titan/vote/providers/contender_list_provider.dart'; import 'package:titan/vote/providers/contender_members.dart'; import 'package:titan/vote/providers/contender_provider.dart'; import 'package:titan/vote/providers/sections_contender_provider.dart'; import 'package:titan/vote/providers/sections_provider.dart'; -import 'package:titan/vote/providers/status_provider.dart'; -import 'package:titan/vote/repositories/status_repository.dart'; import 'package:titan/vote/router.dart'; -import 'package:titan/vote/tools/constants.dart'; -import 'package:titan/vote/ui/pages/admin_page/contender_card.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/vote/ui/pages/admin_page/contender_card.dart'; class SectionContenderItems extends HookConsumerWidget { const SectionContenderItems({super.key}); @@ -34,90 +28,83 @@ class SectionContenderItems extends HookConsumerWidget { ); final contenderNotifier = ref.read(contenderProvider.notifier); - final asyncStatus = ref.watch(statusProvider); - Status status = Status.open; - asyncStatus.whenData((value) => status = value); - void displayVoteToastWithContext(TypeMsg type, String msg) { displayToast(context, type, msg); } return AsyncChild( value: sectionContender[section]!, - builder: (context, data) => HorizontalListView.builder( - height: 190, - firstChild: (status == Status.waiting) - ? GestureDetector( - onTap: () { - contenderNotifier.setId(Contender.empty()); - membersNotifier.setMembers([]); - QR.to( - VoteRouter.root + - VoteRouter.admin + - VoteRouter.addEditContender, - ); - }, - child: const CardLayout( - width: 120, - height: 180, - child: Center( - child: HeroIcon( - HeroIcons.plus, - size: 40.0, - color: Colors.black, - ), - ), + builder: (context, data) => Column( + children: data + .map( + (e) => Padding( + padding: const EdgeInsets.symmetric( + vertical: 5.0, + horizontal: 20.0, ), - ) - : null, - items: data, - itemBuilder: (context, e, i) => ContenderCard( - contender: e, - isAdmin: true, - onEdit: () { - tokenExpireWrapper(ref, () async { - contenderNotifier.setId(e); - membersNotifier.setMembers(e.members); - QR.to( - VoteRouter.root + - VoteRouter.admin + - VoteRouter.addEditContender, - ); - }); - }, - onDelete: () async { - await showDialog( - context: context, - builder: (context) { - return CustomDialogBox( - title: VoteTextConstants.deletePretendance, - descriptions: VoteTextConstants.deletePretendanceDesc, - onYes: () { + child: ContenderCard( + contender: e, + isAdmin: true, + onEdit: () { tokenExpireWrapper(ref, () async { - final value = await contenderListNotifier.deleteContender( - e, + contenderNotifier.setId(e); + membersNotifier.setMembers(e.members); + QR.to( + VoteRouter.root + + VoteRouter.admin + + VoteRouter.addEditContender, ); - if (value) { - displayVoteToastWithContext( - TypeMsg.msg, - VoteTextConstants.pretendanceDeleted, - ); - contenderListNotifier.copy().then((value) { - sectionContenderListNotifier.setTData(section, value); - }); - } else { - displayVoteToastWithContext( - TypeMsg.error, - VoteTextConstants.pretendanceNotDeleted, - ); - } }); }, - ); - }, - ); - }, - ), + onDelete: () async { + await showDialog( + context: context, + builder: (context) { + return CustomDialogBox( + title: AppLocalizations.of( + context, + )!.voteDeletePretendance, + descriptions: AppLocalizations.of( + context, + )!.voteDeletePretendanceDesc, + onYes: () { + final pretendanceDeletedMsg = AppLocalizations.of( + context, + )!.votePretendanceDeleted; + final pretendanceNotDeletedMsg = + AppLocalizations.of( + context, + )!.votePretendanceNotDeleted; + tokenExpireWrapper(ref, () async { + final value = await contenderListNotifier + .deleteContender(e); + if (value) { + displayVoteToastWithContext( + TypeMsg.msg, + pretendanceDeletedMsg, + ); + contenderListNotifier.copy().then((value) { + sectionContenderListNotifier.setTData( + section, + value, + ); + }); + } else { + displayVoteToastWithContext( + TypeMsg.error, + pretendanceNotDeletedMsg, + ); + } + }); + }, + ); + }, + ); + }, + ), + ), + ) + .toList(), ), ); } diff --git a/lib/vote/ui/pages/admin_page/vote_bars.dart b/lib/vote/ui/pages/admin_page/vote_bars.dart index 3c7c37cea8..3514ad27db 100644 --- a/lib/vote/ui/pages/admin_page/vote_bars.dart +++ b/lib/vote/ui/pages/admin_page/vote_bars.dart @@ -5,7 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/vote/providers/result_provider.dart'; import 'package:titan/vote/providers/sections_contender_provider.dart'; import 'package:titan/vote/providers/sections_provider.dart'; -import 'package:titan/vote/tools/constants.dart'; +import 'package:titan/l10n/app_localizations.dart'; class VoteBars extends HookConsumerWidget { const VoteBars({super.key}); @@ -147,7 +147,7 @@ class VoteBars extends HookConsumerWidget { ), const SizedBox(height: 4), Text( - '${voteValue[sectionIds[value.toInt()]] ?? 0} ${VoteTextConstants.votes}', + '${voteValue[sectionIds[value.toInt()]] ?? 0} ${AppLocalizations.of(context)!.voteVotes}', style: const TextStyle( color: Colors.black, fontWeight: FontWeight.bold, diff --git a/lib/vote/ui/pages/admin_page/vote_count.dart b/lib/vote/ui/pages/admin_page/vote_count.dart index e089f1a43e..c7ba2bb469 100644 --- a/lib/vote/ui/pages/admin_page/vote_count.dart +++ b/lib/vote/ui/pages/admin_page/vote_count.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/l10n/app_localizations.dart'; import 'package:titan/tools/ui/builders/auto_loader_child.dart'; import 'package:titan/vote/providers/section_vote_count_provide.dart'; import 'package:titan/vote/providers/sections_provider.dart'; @@ -28,7 +29,7 @@ class VoteCount extends HookConsumerWidget { padding: const EdgeInsets.symmetric(horizontal: 30.0, vertical: 50), child: Center( child: Text( - '$data votes', + '$data ${AppLocalizations.of(context)!.voteVotes}', style: const TextStyle( color: Colors.white, fontSize: 20, diff --git a/lib/vote/ui/pages/admin_page/voter_chip.dart b/lib/vote/ui/pages/admin_page/voter_chip.dart index cb1d155c02..32d2b83cb2 100644 --- a/lib/vote/ui/pages/admin_page/voter_chip.dart +++ b/lib/vote/ui/pages/admin_page/voter_chip.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:titan/tools/ui/layouts/item_chip.dart'; +import 'package:titan/tools/ui/styleguide/item_chip.dart'; class VoterChip extends StatelessWidget { final bool selected; diff --git a/lib/vote/ui/pages/admin_page/voters_bar.dart b/lib/vote/ui/pages/admin_page/voters_bar.dart index 81dac4a033..f43fe98e15 100644 --- a/lib/vote/ui/pages/admin_page/voters_bar.dart +++ b/lib/vote/ui/pages/admin_page/voters_bar.dart @@ -22,7 +22,7 @@ class VotersBar extends HookConsumerWidget { Status status = Status.open; asyncStatus.whenData((value) => status = value); return SizedBox( - height: 40, + height: 50, child: groups.when( data: (data) => SingleChildScrollView( scrollDirection: Axis.horizontal, diff --git a/lib/vote/ui/pages/contender_pages/add_edit_contender.dart b/lib/vote/ui/pages/contender_pages/add_edit_contender.dart index 5c392c4457..ac47de26a1 100644 --- a/lib/vote/ui/pages/contender_pages/add_edit_contender.dart +++ b/lib/vote/ui/pages/contender_pages/add_edit_contender.dart @@ -4,20 +4,17 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:titan/navigation/ui/scroll_to_hide_navbar.dart'; +import 'package:titan/settings/ui/pages/main_page/picture_button.dart'; import 'package:titan/tools/constants.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/token_expire_wrapper.dart'; import 'package:titan/tools/ui/widgets/align_left_text.dart'; -import 'package:titan/tools/ui/layouts/card_button.dart'; import 'package:titan/tools/ui/layouts/horizontal_list_view.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; import 'package:titan/tools/ui/widgets/image_picker_on_tap.dart'; import 'package:titan/tools/ui/widgets/text_entry.dart'; -import 'package:titan/user/class/simple_users.dart'; -import 'package:titan/user/providers/user_list_provider.dart'; -import 'package:titan/vote/class/members.dart'; import 'package:titan/vote/class/contender.dart'; -import 'package:titan/vote/providers/display_results.dart'; import 'package:titan/vote/providers/contender_logo_provider.dart'; import 'package:titan/vote/providers/contender_logos_provider.dart'; import 'package:titan/vote/providers/contender_members.dart'; @@ -25,12 +22,12 @@ import 'package:titan/vote/providers/contender_list_provider.dart'; import 'package:titan/vote/providers/contender_provider.dart'; import 'package:titan/vote/providers/sections_contender_provider.dart'; import 'package:titan/vote/providers/sections_provider.dart'; -import 'package:titan/vote/tools/constants.dart'; import 'package:titan/vote/ui/components/member_card.dart'; -import 'package:titan/vote/ui/pages/contender_pages/search_result.dart'; +import 'package:titan/vote/ui/pages/contender_pages/contender_member.dart'; import 'package:titan/vote/ui/pages/admin_page/section_chip.dart'; import 'package:titan/vote/ui/vote.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AddEditContenderPage extends HookConsumerWidget { const AddEditContenderPage({super.key}); @@ -38,7 +35,6 @@ class AddEditContenderPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final key = GlobalKey(); - final addMemberKey = GlobalKey(); final section = useState(ref.watch(sectionProvider)); final contenderListNotifier = ref.read(contenderListProvider.notifier); final sectionsNotifier = ref.read(sectionContenderProvider.notifier); @@ -47,18 +43,13 @@ class AddEditContenderPage extends HookConsumerWidget { final name = useTextEditingController(text: contender.name); final description = useTextEditingController(text: contender.description); final listType = useState(contender.listType); - final usersNotifier = ref.read(userList.notifier); - final queryController = useTextEditingController(); - final role = useTextEditingController(); final program = useTextEditingController(text: contender.program); - final member = useState(SimpleUser.empty()); final members = ref.watch(contenderMembersProvider); final membersNotifier = ref.read(contenderMembersProvider.notifier); final contenderLogosNotifier = ref.read(contenderLogosProvider.notifier); final logoNotifier = ref.read(contenderLogoProvider.notifier); final logo = useState(null); final logoFile = useState(null); - final showNotifier = ref.read(displayResult.notifier); final contenderLogos = ref.watch(contenderLogosProvider); if (contenderLogos[contender.id] != null) { @@ -76,380 +67,278 @@ class AddEditContenderPage extends HookConsumerWidget { } return VoteTemplate( - child: SingleChildScrollView( - physics: const BouncingScrollPhysics(), - child: Form( - key: key, - child: Column( - children: [ - const AlignLeftText( - VoteTextConstants.addPretendance, - padding: EdgeInsets.only( - top: 40, - left: 30, - right: 30, - bottom: 50, + child: ScrollToHideNavbar( + controller: useScrollController(), + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Form( + key: key, + child: Column( + children: [ + const SizedBox(height: 20), + AlignLeftText( + AppLocalizations.of(context)!.voteAddPretendance, + padding: EdgeInsets.symmetric(horizontal: 20.0), + color: ColorConstants.title, + fontSize: 24, + fontWeight: FontWeight.bold, ), - color: Colors.grey, - ), - Center( - child: Stack( - clipBehavior: Clip.none, - children: [ - Container( - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.1), - spreadRadius: 5, - blurRadius: 10, - offset: const Offset(2, 3), - ), - ], - ), - child: logoFile.value != null - ? Container( - width: 160, - height: 160, - decoration: BoxDecoration( - shape: BoxShape.circle, - image: DecorationImage( - image: logo.value != null - ? Image.memory( - logo.value!, - fit: BoxFit.cover, - ).image - : logoFile.value!.image, - fit: BoxFit.cover, - ), - ), - ) - : const HeroIcon( - HeroIcons.userCircle, - size: 160, - color: Colors.grey, + SizedBox(height: 20), + Center( + child: Stack( + clipBehavior: Clip.none, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + spreadRadius: 5, + blurRadius: 10, + offset: const Offset(2, 3), ), - ), - Positioned( - bottom: 0, - left: 0, - child: ImagePickerOnTap( - picker: picker, - imageBytesNotifier: logo, - imageNotifier: logoFile, - displayToastWithContext: displayVoteToastWithContext, - child: const CardButton( - colors: [ - ColorConstants.gradient1, - ColorConstants.gradient2, ], - child: HeroIcon(HeroIcons.photo, color: Colors.white), ), + child: logoFile.value != null + ? Container( + width: 160, + height: 160, + decoration: BoxDecoration( + shape: BoxShape.circle, + image: DecorationImage( + image: logo.value != null + ? Image.memory( + logo.value!, + fit: BoxFit.cover, + ).image + : logoFile.value!.image, + fit: BoxFit.cover, + ), + ), + ) + : const HeroIcon( + HeroIcons.userCircle, + size: 160, + color: Colors.grey, + ), ), - ), - ], - ), - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 30.0), - child: TextEntry( - label: VoteTextConstants.name, - controller: name, - ), - ), - const SizedBox(height: 50), - HorizontalListView.builder( - height: 40, - items: ListType.values - .where((e) => e != ListType.blank) - .toList(), - itemBuilder: (context, e, i) => SectionChip( - label: capitalize(e.toString().split('.').last), - selected: listType.value == e, - onTap: () async { - listType.value = e; - }, + Positioned( + bottom: 0, + left: 0, + child: ImagePickerOnTap( + picker: picker, + imageBytesNotifier: logo, + imageNotifier: logoFile, + displayToastWithContext: displayVoteToastWithContext, + child: const PictureButton(icon: HeroIcons.photo), + ), + ), + ], + ), ), - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 30.0), - child: TextEntry( - keyboardType: TextInputType.multiline, - controller: description, - label: VoteTextConstants.description, + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: TextEntry( + label: AppLocalizations.of(context)!.voteName, + controller: name, + ), ), - ), - const SizedBox(height: 30), - HorizontalListView.builder( - height: 155, - items: members, - itemBuilder: (context, e, i) => MemberCard( - member: e, - isAdmin: true, - onDelete: () async { - membersNotifier.removeMember(e); - }, + const SizedBox(height: 20), + HorizontalListView.builder( + height: 50, + items: ListType.values + .where((e) => e != ListType.blank) + .toList(), + itemBuilder: (context, e, i) => SectionChip( + label: capitalize(e.toString().split('.').last), + selected: listType.value == e, + onTap: () async { + listType.value = e; + }, + ), ), - ), - const SizedBox(height: 30), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 30.0), - child: InputDecorator( - decoration: InputDecoration( - floatingLabelStyle: const TextStyle( - color: Colors.black, - fontWeight: FontWeight.bold, - ), - focusedBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: Colors.black, width: 2.0), - ), - labelText: VoteTextConstants.addMember, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10.0), - ), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: TextEntry( + keyboardType: TextInputType.multiline, + controller: description, + label: AppLocalizations.of(context)!.voteDescription, ), - child: Form( - key: addMemberKey, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0), - child: Column( - children: [ - TextEntry( - label: VoteTextConstants.members, - onChanged: (newQuery) { - showNotifier.setId(true); - tokenExpireWrapper(ref, () async { - if (queryController.text.isNotEmpty) { - await usersNotifier.filterUsers( - queryController.text, - ); - } else { - usersNotifier.clear(); - } - }); - }, - color: Colors.black, - controller: queryController, - ), - const SizedBox(height: 10), - SearchResult( - borrower: member, - queryController: queryController, - ), - TextEntry( - label: VoteTextConstants.role, - controller: role, - ), - const SizedBox(height: 30), - GestureDetector( - onTap: () async { - if (addMemberKey.currentState == null) { - return; - } - if (member.value.id == '' || - role.text == '') { - return; - } - if (addMemberKey.currentState!.validate()) { - final value = await membersNotifier - .addMember( - Member.fromSimpleUser( - member.value, - role.text, - ), - ); - if (value) { - role.text = ''; - member.value = SimpleUser.empty(); - queryController.text = ''; - } else { - displayVoteToastWithContext( - TypeMsg.error, - VoteTextConstants.alreadyAddedMember, - ); - } - } - }, - child: Container( - width: double.infinity, - padding: const EdgeInsets.only( - top: 8, - bottom: 12, - ), - alignment: Alignment.center, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: Colors.grey.withValues( - alpha: 0.5, - ), - spreadRadius: 5, - blurRadius: 10, - offset: const Offset( - 3, - 3, - ), // changes position of shadow - ), - ], - ), - child: const Text( - VoteTextConstants.add, - style: TextStyle( - color: Colors.white, - fontSize: 25, - fontWeight: FontWeight.bold, - ), - ), + ), + const SizedBox(height: 20), + ContenderMember(), + const SizedBox(height: 10), + members.isEmpty + ? Center( + child: Text( + AppLocalizations.of(context)!.adminNoMember, + ), + ) + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Column( + children: members + .map( + (e) => MemberCard( + member: e, + isAdmin: true, + onEdit: () {}, // TODO: maybe hide + onDelete: () async { + membersNotifier.removeMember(e); + }, ), - ), - ], - ), + ) + .toList(), ), - ], - ), + ), + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: TextEntry( + keyboardType: TextInputType.multiline, + label: AppLocalizations.of(context)!.voteProgram, + controller: program, ), ), - ), - const SizedBox(height: 50), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 30.0), - child: TextEntry( - keyboardType: TextInputType.multiline, - label: VoteTextConstants.program, - controller: program, - ), - ), - const SizedBox(height: 50), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 30.0), - child: WaitingButton( - builder: (child) => Container( - width: double.infinity, - padding: const EdgeInsets.only(top: 8, bottom: 12), - alignment: Alignment.center, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: Colors.grey.withValues(alpha: 0.5), - spreadRadius: 5, - blurRadius: 10, - offset: const Offset(3, 3), - ), - ], + const SizedBox(height: 30), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: WaitingButton( + builder: (child) => Container( + width: double.infinity, + padding: const EdgeInsets.only(top: 8, bottom: 12), + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: Colors.grey.withValues(alpha: 0.5), + spreadRadius: 5, + blurRadius: 10, + offset: const Offset(3, 3), + ), + ], + ), + child: child, ), - child: child, - ), - onTap: () async { - if (key.currentState == null) { - return; - } - if (key.currentState!.validate()) { - await tokenExpireWrapper(ref, () async { - final contenderList = ref.watch(contenderListProvider); - Contender newContender = Contender( - name: name.text, - id: isEdit ? contender.id : '', - description: description.text, - listType: listType.value, - members: members, - section: section.value, - program: program.text, - ); - final value = isEdit - ? await contenderListNotifier.updateContender( - newContender, - ) - : await contenderListNotifier.addContender( - newContender, - ); - if (value) { - QR.back(); - if (isEdit) { + onTap: () async { + if (key.currentState == null) { + return; + } + if (key.currentState!.validate()) { + await tokenExpireWrapper(ref, () async { + final contenderList = ref.watch( + contenderListProvider, + ); + Contender newContender = Contender( + name: name.text, + id: isEdit ? contender.id : '', + description: description.text, + listType: listType.value, + members: members, + section: section.value, + program: program.text, + ); + final editedPretendanceMsg = isEdit + ? AppLocalizations.of( + context, + )!.voteEditedPretendance + : AppLocalizations.of( + context, + )!.voteAddedPretendance; + final editingPretendanceErrorMsg = + AppLocalizations.of(context)!.voteEditingError; + final value = isEdit + ? await contenderListNotifier.updateContender( + newContender, + ) + : await contenderListNotifier.addContender( + newContender, + ); + if (value) { + QR.back(); displayVoteToastWithContext( TypeMsg.msg, - VoteTextConstants.editedPretendance, + editedPretendanceMsg, ); - contenderList.maybeWhen( - data: (list) { - final logoBytes = logo.value; - if (logoBytes != null) { - contenderLogosNotifier.autoLoad( - ref, - contender.id, - (contenderId) => logoNotifier.updateLogo( - contenderId, - logoBytes, - ), - ); - } - }, - orElse: () {}, + if (isEdit) { + contenderList.maybeWhen( + data: (list) { + final logoBytes = logo.value; + if (logoBytes != null) { + contenderLogosNotifier.autoLoad( + ref, + contender.id, + (contenderId) => logoNotifier.updateLogo( + contenderId, + logoBytes, + ), + ); + } + }, + orElse: () {}, + ); + } else { + contenderList.maybeWhen( + data: (list) { + final newContender = list.last; + final logoBytes = logo.value; + if (logoBytes != null) { + contenderLogosNotifier.autoLoad( + ref, + newContender.id, + (contenderId) => logoNotifier.updateLogo( + contenderId, + logoBytes, + ), + ); + } + }, + orElse: () {}, + ); + } + membersNotifier.clearMembers(); + sectionsNotifier.setTData( + section.value, + await contenderListNotifier.copy(), ); } else { displayVoteToastWithContext( - TypeMsg.msg, - VoteTextConstants.addedPretendance, - ); - contenderList.maybeWhen( - data: (list) { - final newContender = list.last; - final logoBytes = logo.value; - if (logoBytes != null) { - contenderLogosNotifier.autoLoad( - ref, - newContender.id, - (contenderId) => logoNotifier.updateLogo( - contenderId, - logoBytes, - ), - ); - } - }, - orElse: () {}, + TypeMsg.error, + editingPretendanceErrorMsg, ); } - membersNotifier.clearMembers(); - sectionsNotifier.setTData( - section.value, - await contenderListNotifier.copy(), - ); - } else { - displayVoteToastWithContext( - TypeMsg.error, - VoteTextConstants.editingError, - ); - } - }); - } else { - displayToast( - context, - TypeMsg.error, - VoteTextConstants.incorrectOrMissingFields, - ); - } - }, - child: const Text( - VoteTextConstants.edit, - style: TextStyle( - color: Colors.white, - fontSize: 25, - fontWeight: FontWeight.bold, + }); + } else { + displayToast( + context, + TypeMsg.error, + AppLocalizations.of( + context, + )!.voteIncorrectOrMissingFields, + ); + } + }, + child: Text( + isEdit + ? AppLocalizations.of(context)!.voteEdit + : AppLocalizations.of(context)!.voteAdd, + style: const TextStyle( + color: Colors.white, + fontSize: 25, + fontWeight: FontWeight.bold, + ), ), ), ), - ), - const SizedBox(height: 30), - ], + const SizedBox(height: 30), + ], + ), ), ), ), diff --git a/lib/vote/ui/pages/contender_pages/contender_member.dart b/lib/vote/ui/pages/contender_pages/contender_member.dart new file mode 100644 index 0000000000..3e5c7ed4af --- /dev/null +++ b/lib/vote/ui/pages/contender_pages/contender_member.dart @@ -0,0 +1,166 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/functions.dart'; +import 'package:titan/tools/token_expire_wrapper.dart'; +import 'package:titan/tools/ui/styleguide/bottom_modal_template.dart'; +import 'package:titan/tools/ui/styleguide/icon_button.dart'; +import 'package:titan/tools/ui/styleguide/text_entry.dart'; +import 'package:titan/user/class/simple_users.dart'; +import 'package:titan/user/providers/user_list_provider.dart'; +import 'package:titan/vote/class/members.dart'; +import 'package:titan/vote/providers/contender_members.dart'; +import 'package:titan/vote/providers/display_results.dart'; +import 'package:titan/vote/ui/pages/contender_pages/search_result.dart'; + +class ContenderMember extends HookConsumerWidget { + const ContenderMember({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final addMemberKey = GlobalKey(); + final usersNotifier = ref.read(userList.notifier); + final queryController = useTextEditingController(); + final role = useTextEditingController(); + final membersNotifier = ref.read(contenderMembersProvider.notifier); + final member = useState(SimpleUser.empty()); + + void displayVoteToastWithContext(TypeMsg type, String msg) { + displayToast(context, type, msg); + } + + final showNotifier = ref.read(displayResult.notifier); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + AppLocalizations.of(context)!.voteMembers, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: ColorConstants.tertiary, + ), + ), + const Spacer(), + CustomIconButton( + icon: HeroIcon(HeroIcons.plus, color: ColorConstants.background), + onPressed: () async { + await showCustomBottomModal( + context: context, + ref: ref, + modal: BottomModalTemplate( + title: AppLocalizations.of(context)!.voteAddMember, + child: Form( + key: addMemberKey, + child: Column( + children: [ + Column( + children: [ + TextEntry( + label: AppLocalizations.of(context)!.voteMembers, + onChanged: (newQuery) { + showNotifier.setId(true); + tokenExpireWrapper(ref, () async { + if (queryController.text.isNotEmpty) { + await usersNotifier.filterUsers( + queryController.text, + ); + } else { + usersNotifier.clear(); + } + }); + }, + color: Colors.black, + controller: queryController, + ), + const SizedBox(height: 10), + SearchResult( + borrower: member, + queryController: queryController, + ), + TextEntry( + label: AppLocalizations.of(context)!.voteRole, + controller: role, + ), + const SizedBox(height: 30), + GestureDetector( + onTap: () async { + if (addMemberKey.currentState == null) { + return; + } + if (member.value.id == '' || role.text == '') { + return; + } + final alreadyAddedMemberMsg = + AppLocalizations.of( + context, + )!.voteAlreadyAddedMember; + if (addMemberKey.currentState!.validate()) { + final value = await membersNotifier.addMember( + Member.fromSimpleUser( + member.value, + role.text, + ), + ); + if (value) { + role.text = ''; + member.value = SimpleUser.empty(); + queryController.text = ''; + QR.back(); + } else { + displayVoteToastWithContext( + TypeMsg.error, + alreadyAddedMemberMsg, + ); + } + } + }, + child: Container( + width: double.infinity, + padding: const EdgeInsets.only( + top: 8, + bottom: 12, + ), + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: Colors.grey.withValues(alpha: 0.5), + spreadRadius: 5, + blurRadius: 10, + offset: const Offset(3, 3), + ), + ], + ), + child: Text( + AppLocalizations.of(context)!.voteAdd, + style: const TextStyle( + color: Colors.white, + fontSize: 25, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/vote/ui/pages/detail_page/detail_page.dart b/lib/vote/ui/pages/detail_page/detail_page.dart index 745715299b..728d185312 100644 --- a/lib/vote/ui/pages/detail_page/detail_page.dart +++ b/lib/vote/ui/pages/detail_page/detail_page.dart @@ -129,7 +129,13 @@ class DetailPage extends HookConsumerWidget { physics: const BouncingScrollPhysics(), child: Wrap( children: contender.members - .map((e) => MemberCard(member: e)) + .map( + (e) => MemberCard( + member: e, + onEdit: () {}, + onDelete: () {}, + ), + ) .toList(), ), ) @@ -152,7 +158,13 @@ class DetailPage extends HookConsumerWidget { ), Padding( padding: const EdgeInsets.all(20.0), - child: Center(child: ContenderCard(contender: contender)), + child: Center( + child: ContenderCard( + contender: contender, + onDelete: () async {}, + onEdit: () {}, + ), + ), ), ], ), diff --git a/lib/vote/ui/pages/main_page/contender_card.dart b/lib/vote/ui/pages/main_page/contender_card.dart index 68deb67220..96a0da6bf0 100644 --- a/lib/vote/ui/pages/main_page/contender_card.dart +++ b/lib/vote/ui/pages/main_page/contender_card.dart @@ -11,9 +11,9 @@ import 'package:titan/vote/providers/selected_contender_provider.dart'; import 'package:titan/vote/providers/status_provider.dart'; import 'package:titan/vote/repositories/status_repository.dart'; import 'package:titan/vote/router.dart'; -import 'package:titan/vote/tools/constants.dart'; import 'package:titan/vote/ui/components/contender_logo.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class ContenderCard extends HookConsumerWidget { final Contender contender; @@ -258,9 +258,9 @@ class ContenderCard extends HookConsumerWidget { ), ), ) - : const Text( - VoteTextConstants.selected, - style: TextStyle( + : Text( + AppLocalizations.of(context)!.voteSelected, + style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.black, diff --git a/lib/vote/ui/pages/main_page/list_contender_card.dart b/lib/vote/ui/pages/main_page/list_contender_card.dart index 5a5f20977f..461bd0b563 100644 --- a/lib/vote/ui/pages/main_page/list_contender_card.dart +++ b/lib/vote/ui/pages/main_page/list_contender_card.dart @@ -11,8 +11,8 @@ import 'package:titan/vote/providers/sections_provider.dart'; import 'package:titan/vote/providers/status_provider.dart'; import 'package:titan/vote/providers/voted_section_provider.dart'; import 'package:titan/vote/repositories/status_repository.dart'; -import 'package:titan/vote/tools/constants.dart'; import 'package:titan/vote/ui/pages/main_page/contender_card.dart'; +import 'package:titan/l10n/app_localizations.dart'; class ListContenderCard extends HookConsumerWidget { final AnimationController animation; @@ -103,10 +103,12 @@ class ListContenderCard extends HookConsumerWidget { }).toList(), ), ) - : const SizedBox( + : SizedBox( height: 150, child: Center( - child: Text(VoteTextConstants.noPretendanceList), + child: Text( + AppLocalizations.of(context)!.voteNoPretendanceList, + ), ), ), ), @@ -180,7 +182,7 @@ class ListContenderCard extends HookConsumerWidget { ), const SizedBox(width: 10), Text( - VoteTextConstants.seeMore, + AppLocalizations.of(context)!.voteSeeMore, style: TextStyle( fontSize: 18, color: Colors.grey.shade100, diff --git a/lib/vote/ui/pages/main_page/list_side_item.dart b/lib/vote/ui/pages/main_page/list_side_item.dart index 7a4af88616..c17886b1f8 100644 --- a/lib/vote/ui/pages/main_page/list_side_item.dart +++ b/lib/vote/ui/pages/main_page/list_side_item.dart @@ -7,8 +7,8 @@ import 'package:titan/vote/providers/section_id_provider.dart'; import 'package:titan/vote/providers/sections_provider.dart'; import 'package:titan/vote/providers/selected_contender_provider.dart'; import 'package:titan/vote/providers/voted_section_provider.dart'; -import 'package:titan/vote/tools/constants.dart'; import 'package:titan/vote/ui/pages/main_page/side_item.dart'; +import 'package:titan/l10n/app_localizations.dart'; class ListSideItem extends HookConsumerWidget { final List

sectionList; @@ -47,8 +47,10 @@ class ListSideItem extends HookConsumerWidget { showDialog( context: context, builder: (context) => CustomDialogBox( - title: VoteTextConstants.warning, - descriptions: VoteTextConstants.warningMessage, + title: AppLocalizations.of(context)!.voteWarning, + descriptions: AppLocalizations.of( + context, + )!.voteWarningMessage, onYes: () { selectedContenderNotifier.clear(); animation.forward(from: 0); diff --git a/lib/vote/ui/pages/main_page/main_page.dart b/lib/vote/ui/pages/main_page/main_page.dart index 09ae09597c..8ac787b965 100644 --- a/lib/vote/ui/pages/main_page/main_page.dart +++ b/lib/vote/ui/pages/main_page/main_page.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:heroicons/heroicons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:titan/tools/constants.dart'; +import 'package:titan/tools/ui/styleguide/icon_button.dart'; import 'package:titan/tools/ui/widgets/admin_button.dart'; import 'package:titan/tools/ui/builders/async_child.dart'; import 'package:titan/tools/ui/layouts/refresher.dart'; @@ -16,13 +19,13 @@ import 'package:titan/vote/providers/status_provider.dart'; import 'package:titan/vote/providers/voted_section_provider.dart'; import 'package:titan/vote/repositories/status_repository.dart'; import 'package:titan/vote/router.dart'; -import 'package:titan/vote/tools/constants.dart'; import 'package:titan/vote/ui/pages/main_page/list_contender_card.dart'; import 'package:titan/vote/ui/pages/main_page/list_side_item.dart'; import 'package:titan/vote/ui/pages/main_page/section_title.dart'; import 'package:titan/vote/ui/pages/main_page/vote_button.dart'; import 'package:titan/vote/ui/vote.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class VoteMainPage extends HookConsumerWidget { const VoteMainPage({super.key}); @@ -56,42 +59,50 @@ class VoteMainPage extends HookConsumerWidget { if (!canVote) { return VoteTemplate( - child: SizedBox( - height: MediaQuery.of(context).size.height - 100, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 30.0), - child: Column( - children: [ - if (isAdmin) - Row( - children: [ - const Spacer(), - Container( - margin: const EdgeInsets.only(right: 20), - child: AdminButton( - onTap: () { - QR.to(VoteRouter.root + VoteRouter.admin); - }, - ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Column( + children: [ + const SizedBox(height: 20), + Row( + children: [ + if (isAdmin) + Text( + AppLocalizations.of(context)!.voteVote, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorConstants.title, ), - ], - ), - const Expanded( - child: Center( - child: Text( - VoteTextConstants.canNotVote, - style: TextStyle(fontSize: 20), ), + const Spacer(), + CustomIconButton( + icon: HeroIcon( + HeroIcons.userGroup, + color: ColorConstants.background, + ), + onPressed: () { + QR.to(VoteRouter.root + VoteRouter.admin); + }, + ), + ], + ), + Expanded( + child: Center( + child: Text( + AppLocalizations.of(context)!.voteCanNotVote, + style: const TextStyle(fontSize: 20), ), ), - ], - ), + ), + ], ), ), ); } return VoteTemplate( child: Refresher( + controller: ScrollController(), onRefresh: () async { await statusNotifier.loadStatus(); if (s == Status.open) { @@ -128,82 +139,81 @@ class VoteMainPage extends HookConsumerWidget { } }); }, - child: SizedBox( - height: MediaQuery.of(context).size.height - 100, - child: Padding( - padding: const EdgeInsets.only(left: 30.0), - child: Column( - children: [ - SizedBox(height: isAdmin ? 10 : 15), - AsyncChild( - value: sections, - builder: (context, sectionList) => Column( - children: [ - SizedBox( - height: - MediaQuery.of(context).size.height - - (s == Status.open - ? isAdmin - ? 215 - : 220 - : isAdmin - ? 150 - : 155), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - ListSideItem( - sectionList: sectionList, - animation: animation, - ), - Expanded( - child: SizedBox( - width: double.infinity, - child: Column( - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - SectionTitle(sectionList: sectionList), - if (isAdmin) - Container( - margin: const EdgeInsets.only( - right: 20, - ), - child: AdminButton( - onTap: () { - QR.to( - VoteRouter.root + - VoteRouter.admin, - ); - }, - ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Column( + children: [ + const SizedBox(height: 20), + Text( + AppLocalizations.of(context)!.voteVote, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorConstants.title, + ), + ), + AsyncChild( + value: sections, + builder: (context, sectionList) => Column( + children: [ + SizedBox( + height: + MediaQuery.of(context).size.height - + (s == Status.open ? 220 : 155), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + ListSideItem( + sectionList: sectionList, + animation: animation, + ), + Expanded( + child: SizedBox( + width: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + SectionTitle(sectionList: sectionList), + if (isAdmin) + Container( + margin: const EdgeInsets.only( + right: 20, ), - ], - ), - const SizedBox(height: 15), - Expanded( - child: ListContenderCard( - animation: animation, - ), + child: AdminButton( + onTap: () { + QR.to( + VoteRouter.root + + VoteRouter.admin, + ); + }, + ), + ), + ], + ), + const SizedBox(height: 15), + Expanded( + child: ListContenderCard( + animation: animation, ), - ], - ), + ), + ], ), ), - ], - ), + ), + ], ), - const SizedBox(height: 20), - if (sectionList.isNotEmpty && s == Status.open) - const VoteButton(), - const SizedBox(height: 20), - ], - ), + ), + const SizedBox(height: 20), + if (sectionList.isNotEmpty && s == Status.open) + const VoteButton(), + const SizedBox(height: 20), + ], ), - ], - ), + ), + ], ), ), ), diff --git a/lib/vote/ui/pages/main_page/section_title.dart b/lib/vote/ui/pages/main_page/section_title.dart index d571d9e83a..f86970e94d 100644 --- a/lib/vote/ui/pages/main_page/section_title.dart +++ b/lib/vote/ui/pages/main_page/section_title.dart @@ -3,7 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:titan/tools/ui/widgets/align_left_text.dart'; import 'package:titan/vote/class/section.dart'; import 'package:titan/vote/providers/sections_provider.dart'; -import 'package:titan/vote/tools/constants.dart'; +import 'package:titan/l10n/app_localizations.dart'; class SectionTitle extends HookConsumerWidget { final List
sectionList; @@ -15,7 +15,7 @@ class SectionTitle extends HookConsumerWidget { return AlignLeftText( section.id != Section.empty().id ? section.name - : VoteTextConstants.noSection, + : AppLocalizations.of(context)!.voteNoSection, padding: const EdgeInsets.only(left: 20), fontSize: 20, fontWeight: FontWeight.w700, diff --git a/lib/vote/ui/pages/main_page/vote_button.dart b/lib/vote/ui/pages/main_page/vote_button.dart index b9e206f20b..9c5ebd91e9 100644 --- a/lib/vote/ui/pages/main_page/vote_button.dart +++ b/lib/vote/ui/pages/main_page/vote_button.dart @@ -10,7 +10,7 @@ import 'package:titan/vote/providers/status_provider.dart'; import 'package:titan/vote/providers/voted_section_provider.dart'; import 'package:titan/vote/providers/votes_provider.dart'; import 'package:titan/vote/repositories/status_repository.dart'; -import 'package:titan/vote/tools/constants.dart'; +import 'package:titan/l10n/app_localizations.dart'; class VoteButton extends HookConsumerWidget { const VoteButton({super.key}); @@ -54,9 +54,15 @@ class VoteButton extends HookConsumerWidget { context: context, builder: (context) { return CustomDialogBox( - title: VoteTextConstants.vote, - descriptions: VoteTextConstants.confirmVote, + title: AppLocalizations.of(context)!.voteVote, + descriptions: AppLocalizations.of(context)!.voteConfirmVote, onYes: () { + final voteSuccessMsg = AppLocalizations.of( + context, + )!.voteVoteSuccess; + final voteErrorMsg = AppLocalizations.of( + context, + )!.voteVoteError; tokenExpireWrapper(ref, () async { final result = await votesNotifier.addVote( Votes(id: selectedContender.id), @@ -66,12 +72,12 @@ class VoteButton extends HookConsumerWidget { selectedContenderNotifier.clear(); displayVoteToastWithContext( TypeMsg.msg, - VoteTextConstants.voteSuccess, + voteSuccessMsg, ); } else { displayVoteToastWithContext( TypeMsg.error, - VoteTextConstants.voteError, + voteErrorMsg, ); } }); @@ -107,16 +113,17 @@ class VoteButton extends HookConsumerWidget { child: Center( child: Text( selectedContender.id != "" - ? VoteTextConstants.voteFor + selectedContender.name + ? AppLocalizations.of(context)!.voteVoteFor + + selectedContender.name : alreadyVotedSection.contains(section.id) - ? VoteTextConstants.alreadyVoted + ? AppLocalizations.of(context)!.voteAlreadyVoted : s == Status.open - ? VoteTextConstants.chooseList + ? AppLocalizations.of(context)!.voteChooseList : s == Status.waiting - ? VoteTextConstants.notOpenedVote + ? AppLocalizations.of(context)!.voteNotOpenedVote : s == Status.closed - ? VoteTextConstants.closedVote - : VoteTextConstants.onGoingCount, + ? AppLocalizations.of(context)!.voteClosedVote + : AppLocalizations.of(context)!.voteOnGoingCount, style: TextStyle( color: (selectedContender.id == "" && s != Status.open) || diff --git a/lib/vote/ui/pages/section_pages/add_section.dart b/lib/vote/ui/pages/section_pages/add_section.dart index 2b7777ef98..21fd849aa1 100644 --- a/lib/vote/ui/pages/section_pages/add_section.dart +++ b/lib/vote/ui/pages/section_pages/add_section.dart @@ -10,9 +10,9 @@ import 'package:titan/tools/ui/widgets/text_entry.dart'; import 'package:titan/vote/class/section.dart'; import 'package:titan/vote/providers/sections_contender_provider.dart'; import 'package:titan/vote/providers/sections_provider.dart'; -import 'package:titan/vote/tools/constants.dart'; import 'package:titan/vote/ui/vote.dart'; import 'package:qlevar_router/qlevar_router.dart'; +import 'package:titan/l10n/app_localizations.dart'; class AddSectionPage extends HookConsumerWidget { const AddSectionPage({super.key}); @@ -37,8 +37,8 @@ class AddSectionPage extends HookConsumerWidget { child: Column( children: [ const SizedBox(height: 50), - const AlignLeftText( - VoteTextConstants.addSection, + AlignLeftText( + AppLocalizations.of(context)!.voteAddSection, color: Colors.grey, ), Form( @@ -48,17 +48,23 @@ class AddSectionPage extends HookConsumerWidget { const SizedBox(height: 30), TextEntry( controller: name, - label: VoteTextConstants.sectionName, + label: AppLocalizations.of(context)!.voteSectionName, ), const SizedBox(height: 30), TextEntry( controller: description, - label: VoteTextConstants.sectionDescription, + label: AppLocalizations.of(context)!.voteSectionDescription, ), const SizedBox(height: 50), WaitingButton( builder: (child) => AddEditButtonLayout(child: child), onTap: () async { + final addedSectionMsg = AppLocalizations.of( + context, + )!.voteAddedSection; + final addingErrorMsg = AppLocalizations.of( + context, + )!.voteAddingError; await tokenExpireWrapper(ref, () async { final value = await sectionListNotifier.addSection( Section( @@ -74,19 +80,19 @@ class AddSectionPage extends HookConsumerWidget { }); displayVoteToastWithContext( TypeMsg.msg, - VoteTextConstants.addedSection, + addedSectionMsg, ); } else { displayVoteToastWithContext( TypeMsg.error, - VoteTextConstants.addingError, + addingErrorMsg, ); } }); }, - child: const Text( - VoteTextConstants.add, - style: TextStyle( + child: Text( + AppLocalizations.of(context)!.voteAdd, + style: const TextStyle( color: Colors.white, fontSize: 25, fontWeight: FontWeight.bold, diff --git a/lib/vote/ui/vote.dart b/lib/vote/ui/vote.dart index dfb41af8f6..a6f5f113e9 100644 --- a/lib/vote/ui/vote.dart +++ b/lib/vote/ui/vote.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:titan/tools/ui/widgets/top_bar.dart'; import 'package:titan/vote/router.dart'; -import 'package:titan/vote/tools/constants.dart'; +import 'package:titan/tools/constants.dart'; class VoteTemplate extends StatelessWidget { final Widget child; @@ -9,12 +9,18 @@ class VoteTemplate extends StatelessWidget { @override Widget build(BuildContext context) { - return SafeArea( - child: Column( - children: [ - const TopBar(title: VoteTextConstants.vote, root: VoteRouter.root), - Expanded(child: child), - ], + return Scaffold( + body: Container( + decoration: const BoxDecoration(color: ColorConstants.background), + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const TopBar(root: VoteRouter.root), + Expanded(child: child), + ], + ), + ), ), ); } diff --git a/pubspec.lock b/pubspec.lock index 489f1b2d7d..ddea27696e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -265,14 +265,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" - dotenv: - dependency: "direct dev" - description: - name: dotenv - sha256: "379e64b6fc82d3df29461d349a1796ecd2c436c480d4653f3af6872eccbc90e1" - url: "https://pub.dev" - source: hosted - version: "4.2.0" either_dart: dependency: "direct main" description: @@ -433,14 +425,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - flash: - dependency: "direct main" - description: - name: flash - sha256: f8b6ed7410df7581b35824967427b79b4e66d59c8f24012ed10f26667a4d44b1 - url: "https://pub.dev" - source: hosted - version: "3.1.1" flutter: dependency: "direct main" description: flutter @@ -462,14 +446,6 @@ packages: url: "https://pub.dev" source: hosted version: "9.0.0" - flutter_dotenv: - dependency: "direct main" - description: - name: flutter_dotenv - sha256: b7c7be5cd9f6ef7a78429cabd2774d3c4af50e79cb2b7593e3d5d763ef95c61b - url: "https://pub.dev" - source: hosted - version: "5.2.1" flutter_hooks: dependency: "direct main" description: @@ -641,10 +617,10 @@ packages: dependency: "direct main" description: name: google_fonts - sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 + sha256: c30eef5e7cd26eb89cc8065b4390ac86ce579f2fcdbe35220891c6278b5460da url: "https://pub.dev" source: hosted - version: "6.2.1" + version: "8.0.1" gtk: dependency: transitive description: @@ -693,6 +669,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + iconsax_flutter: + dependency: transitive + description: + name: iconsax_flutter + sha256: d14b4cec8586025ac15276bdd40f6eea308cb85748135965bb6255f14beb2564 + url: "https://pub.dev" + source: hosted + version: "1.0.1" image: dependency: "direct main" description: @@ -841,26 +825,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -953,10 +937,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -969,10 +953,10 @@ packages: dependency: "direct main" description: name: mobile_scanner - sha256: "728828a798d1a2ee506beb652ca23d974c542c96ed03dcbd5eaf97bef96cdaad" + sha256: "023a71afb4d7cfb5529d0f2636aa8b43db66257905b9486d702085989769c5f2" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "7.1.3" mocktail: dependency: "direct dev" description: @@ -1077,6 +1061,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + pausable_timer: + dependency: transitive + description: + name: pausable_timer + sha256: "6ef1a95441ec3439de6fb63f39a011b67e693198e7dae14e20675c3c00e86074" + url: "https://pub.dev" + source: hosted + version: "3.1.0+3" pdfx: dependency: "direct main" description: @@ -1398,10 +1390,10 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.7" timeago: dependency: "direct main" description: @@ -1410,6 +1402,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.7.1" + timeago_flutter: + dependency: "direct main" + description: + name: timeago_flutter + sha256: "0fd70e79f35f5ea6507f04b3852ba3df3de595901cc1a916e4abc452115b09ac" + url: "https://pub.dev" + source: hosted + version: "3.7.0" timezone: dependency: "direct main" description: @@ -1418,6 +1418,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.10.0" + toastification: + dependency: "direct main" + description: + name: toastification + sha256: "69db2bff425b484007409650d8bcd5ed1ce2e9666293ece74dcd917dacf23112" + url: "https://pub.dev" + source: hosted + version: "3.0.3" tuple: dependency: "direct main" description: @@ -1558,10 +1566,10 @@ packages: dependency: "direct dev" description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -1667,5 +1675,5 @@ packages: source: hosted version: "1.0.0" sdks: - dart: ">=3.8.0 <4.0.0" - flutter: ">=3.32.0" + dart: ">=3.9.2 <4.0.0" + flutter: "3.38.9" diff --git a/pubspec.yaml b/pubspec.yaml index 05a55334ff..7073df148e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,11 +2,11 @@ name: titan description: Titan est le frontend d'une application de gestion de la vie associative publish_to: "none" -version: 1.2.7+154 +version: 2.3.2+196 environment: - sdk: ^3.8.0 - flutter: 3.32.0 + sdk: ^3.9.2 + flutter: 3.38.9 dependencies: app_links: ^6.4.0 @@ -24,11 +24,9 @@ dependencies: firebase_core: ^3.9.0 firebase_messaging: ^15.2.2 fl_chart: ^1.0.0 - flash: ^3.1.1 flutter: sdk: flutter flutter_appauth: ^9.0.0 - flutter_dotenv: ^5.0.2 flutter_hooks: ^0.21.2 flutter_lints: ^6.0.0 flutter_local_notifications: ^19.1.0 @@ -38,7 +36,7 @@ dependencies: flutter_riverpod: ^2.1.1 flutter_secure_storage: ^10.0.0-beta flutter_svg: ^2.0.7 - google_fonts: ^6.2.1 + google_fonts: ^8.0.1 heroicons: ^0.11.0 hooks_riverpod: ^2.1.1 http: ^1.0.0 @@ -52,7 +50,7 @@ dependencies: local_auth: ^2.3.0 local_auth_android: ^1.0.46 local_auth_darwin: ^1.4.1 - mobile_scanner: 6.0.2 + mobile_scanner: ^7.0.1 numberpicker: ^2.1.1 package_info_plus: ^8.3.0 path: ^1.8.2 @@ -66,7 +64,9 @@ dependencies: smooth_page_indicator: ^1.0.0+2 syncfusion_flutter_calendar: ^29.1.38 timeago: ^3.7.0 + timeago_flutter: ^3.7.0 timezone: ^0.10.0 + toastification: ^3.0.3 tuple: ^2.0.0 universal_html: ^2.0.8 url_launcher: ^6.2.5 @@ -76,7 +76,6 @@ dependencies: dev_dependencies: dependency_validator: ^5.0.2 - dotenv: ^4.0.1 flutter_launcher_icons: ^0.14.3 flutter_test: sdk: flutter @@ -85,14 +84,13 @@ dev_dependencies: flutter: assets: - - .env - assets/images/logo_prod.png - assets/images/logo_alpha.png - assets/images/logo_dev.png - assets/images/gift.png - assets/images/soli.png - - assets/images/eclair.png - - assets/images/login.svg + - assets/images/proximapp.png + - assets/images/login.webp - assets/web/back.webp - assets/web/AMAP.webp - assets/web/Calendrier.webp @@ -106,5 +104,7 @@ flutter: - assets/images/pipe.png - assets/images/logo_flappybird.svg - assets/images/helloasso.svg + - assets/images/vache.png uses-material-design: true + generate: true diff --git a/test/admin/admin_test.dart b/test/admin/admin_test.dart index 3bc68a9b20..8566a26536 100644 --- a/test/admin/admin_test.dart +++ b/test/admin/admin_test.dart @@ -1,7 +1,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:titan/admin/class/account_type.dart'; +import 'package:titan/super_admin/class/account_type.dart'; import 'package:titan/admin/class/group.dart'; import 'package:titan/admin/class/simple_group.dart'; import 'package:titan/admin/repositories/group_repository.dart'; diff --git a/test/admin/group_list_provider_test.dart b/test/admin/group_list_provider_test.dart index b352ecc733..974a4cf558 100644 --- a/test/admin/group_list_provider_test.dart +++ b/test/admin/group_list_provider_test.dart @@ -1,7 +1,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:titan/admin/class/account_type.dart'; +import 'package:titan/super_admin/class/account_type.dart'; import 'package:titan/admin/class/simple_group.dart'; import 'package:titan/admin/providers/group_list_provider.dart'; import 'package:titan/admin/repositories/group_repository.dart'; @@ -61,6 +61,7 @@ void main() { floor: '', phone: '', promo: null, + isSuperAdmin: false, ); final GroupListNotifier groupNotifier = GroupListNotifier( groupRepository: mockGroup, diff --git a/test/admin/members_provider_test.dart b/test/admin/members_provider_test.dart index a3215c0ef8..8283376e3b 100644 --- a/test/admin/members_provider_test.dart +++ b/test/admin/members_provider_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:titan/admin/providers/members_provider.dart'; +import 'package:titan/super_admin/providers/members_provider.dart'; import 'package:titan/user/class/simple_users.dart'; void main() { diff --git a/test/amap/amap_test.dart b/test/amap/amap_test.dart index ea5678bf13..ea484d8dd8 100644 --- a/test/amap/amap_test.dart +++ b/test/amap/amap_test.dart @@ -12,7 +12,6 @@ import 'package:titan/amap/repositories/delivery_product_list_repository.dart'; import 'package:titan/amap/repositories/information_repository.dart'; import 'package:titan/amap/repositories/order_list_repository.dart'; import 'package:titan/amap/repositories/product_repository.dart'; -import 'package:titan/amap/tools/constants.dart'; import 'package:titan/amap/tools/functions.dart'; import 'package:titan/user/class/simple_users.dart'; @@ -428,16 +427,6 @@ void main() { }); group('Testing functions', () { - test('Should return the correct string', () async { - expect( - uiCollectionSlotToString(CollectionSlot.midDay), - AMAPTextConstants.midDay, - ); - expect( - uiCollectionSlotToString(CollectionSlot.evening), - AMAPTextConstants.evening, - ); - }); test('Should return a string', () async { expect(apiCollectionSlotToString(CollectionSlot.midDay), "midi"); expect(apiCollectionSlotToString(CollectionSlot.evening), "soir"); diff --git a/test/auth/is_connected_provider_test.dart b/test/auth/is_connected_provider_test.dart index e6b930dbb3..feb94b2ab5 100644 --- a/test/auth/is_connected_provider_test.dart +++ b/test/auth/is_connected_provider_test.dart @@ -1,14 +1,9 @@ -import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:titan/auth/providers/is_connected_provider.dart'; void main() { group('IsConnectedProvider', () { - setUp(() async { - await dotenv.load(); - }); - test('IsConnectedProvider initial state is false', () { final provider = IsConnectedProvider(); expect(provider.state, false); diff --git a/test/booking/booking_test.dart b/test/booking/booking_test.dart index 124637b34f..439bb39e2a 100644 --- a/test/booking/booking_test.dart +++ b/test/booking/booking_test.dart @@ -1,7 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:titan/booking/class/booking.dart'; import 'package:titan/service/class/room.dart'; -import 'package:titan/booking/tools/functions.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/user/class/applicant.dart'; import 'package:titan/user/class/simple_users.dart'; @@ -243,12 +242,6 @@ void main() { expect(Decision.pending, stringToDecision("random")); }); - test('Decision to string', () { - expect("Validée", decisionToString(Decision.approved)); - expect("Refusée", decisionToString(Decision.declined)); - expect("En attente", decisionToString(Decision.pending)); - }); - test('formatDates returns correct string for same day event', () { final dateStart = DateTime(2022, 1, 1, 10, 0); final dateEnd = DateTime(2022, 1, 1, 14, 0); @@ -273,45 +266,45 @@ void main() { expect(result, "Du 01/01/2022 à 10:00 au 03/01/2022 à 14:00"); }); - test( - 'formatRecurrenceRule returns correct string for empty recurrenceRule', - () { - DateTime dateStart = DateTime(2022, 1, 1, 10, 0); - DateTime dateEnd = DateTime(2022, 1, 1, 12, 0); - String recurrenceRule = ""; - bool allDay = false; - expect( - formatRecurrenceRule(dateStart, dateEnd, recurrenceRule, allDay), - "Le 01/01/2022 de 10:00 à 12:00", - ); - }, - ); + // test( + // 'formatRecurrenceRule returns correct string for empty recurrenceRule', + // () { + // DateTime dateStart = DateTime(2022, 1, 1, 10, 0); + // DateTime dateEnd = DateTime(2022, 1, 1, 12, 0); + // String recurrenceRule = ""; + // bool allDay = false; + // expect( + // formatRecurrenceRule(dateStart, dateEnd, recurrenceRule, allDay), + // "Le 01/01/2022 de 10:00 à 12:00", + // ); + // }, + // ); - test( - 'formatRecurrenceRule returns correct string for non-empty recurrenceRule', - () { - DateTime dateStart = DateTime(2022, 1, 1, 10, 0); - DateTime dateEnd = DateTime(2022, 1, 1, 12, 0); - String recurrenceRule = - "FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20220131T000000Z"; - bool allDay = false; - expect( - formatRecurrenceRule(dateStart, dateEnd, recurrenceRule, allDay), - "Tous les Lundi, Mercredi et Vendredi de 10:00 à 12:00 jusqu'au 31/01/2022", - ); - }, - ); + // test( + // 'formatRecurrenceRule returns correct string for non-empty recurrenceRule', + // () { + // DateTime dateStart = DateTime(2022, 1, 1, 10, 0); + // DateTime dateEnd = DateTime(2022, 1, 1, 12, 0); + // String recurrenceRule = + // "FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20220131T000000Z"; + // bool allDay = false; + // expect( + // formatRecurrenceRule(dateStart, dateEnd, recurrenceRule, allDay), + // "Tous les Lundi, Mercredi et Vendredi de 10:00 à 12:00 jusqu'au 31/01/2022", + // ); + // }, + // ); - test('formatRecurrenceRule returns correct string for allDay event', () { - DateTime dateStart = DateTime(2022, 1, 1); - DateTime dateEnd = DateTime(2022, 1, 3); - String recurrenceRule = ""; - bool allDay = true; - expect( - formatRecurrenceRule(dateStart, dateEnd, recurrenceRule, allDay), - "Du 01/01/2022 à 00:00 au 03/01/2022 à 00:00", - ); - }); + // test('formatRecurrenceRule returns correct string for allDay event', () { + // DateTime dateStart = DateTime(2022, 1, 1); + // DateTime dateEnd = DateTime(2022, 1, 3); + // String recurrenceRule = ""; + // bool allDay = true; + // expect( + // formatRecurrenceRule(dateStart, dateEnd, recurrenceRule, allDay), + // "Du 01/01/2022 à 00:00 au 03/01/2022 à 00:00", + // ); + // }); test('combineDate returns correct date', () { final date = DateTime(2022, 1, 1); diff --git a/test/drawer/drawer_test.dart b/test/drawer/drawer_test.dart deleted file mode 100644 index 86e532db25..0000000000 --- a/test/drawer/drawer_test.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:either_dart/either.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:titan/drawer/class/module.dart'; - -void main() { - group('Module', () { - test( - 'copy method should return a new Module object with updated properties', - () { - final module = Module( - name: 'Calendar', - icon: const Left(HeroIcons.calendar), - selected: true, - root: '', - ); - - final copiedModule = module.copy( - name: 'Settings', - icon: const Left(HeroIcons.cog), - selected: false, - root: '/test', - ); - - expect(copiedModule.name, 'Settings'); - expect(copiedModule.icon, const Left(HeroIcons.cog)); - expect(copiedModule.root, '/test'); - expect(copiedModule.selected, false); - }, - ); - }); -} diff --git a/test/drawer/swipe_provider_test.dart b/test/drawer/swipe_provider_test.dart deleted file mode 100644 index febd2fa0d9..0000000000 --- a/test/drawer/swipe_provider_test.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:titan/drawer/providers/swipe_provider.dart'; - -class MockTicker extends TickerProvider { - @override - Ticker createTicker(TickerCallback onTick) { - return Ticker(onTick); - } -} - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - group('SwipeControllerNotifier', () { - test('SwipeControllerNotifier can close animation controller', () { - final controller = AnimationController( - vsync: MockTicker(), - duration: const Duration(microseconds: 1), - ); - final swipeController = SwipeControllerNotifier(controller); - swipeController.close(); - Future.delayed(const Duration(milliseconds: 1), () { - expect(controller.value, equals(0)); - }); - }); - - test('SwipeControllerNotifier can detect drag start from left', () { - final controller = AnimationController( - vsync: MockTicker(), - duration: const Duration(seconds: 1), - ); - final swipeController = SwipeControllerNotifier(controller); - final startDetails = DragStartDetails( - globalPosition: const Offset(50, 0), - ); - swipeController.onDragStart(startDetails); - expect(SwipeControllerNotifier.shouldDrag, equals(true)); - }); - }); -} diff --git a/test/event/event_test.dart b/test/event/event_test.dart index ce878cd27d..ab81c05439 100644 --- a/test/event/event_test.dart +++ b/test/event/event_test.dart @@ -224,66 +224,66 @@ void main() { expect(getMonth(12), "Décembre"); }); - test('Testing formatRecurrenceRule', () { - final start = DateTime.parse("2021-01-01T00:00:00.000Z"); - final end = DateTime.parse("2021-01-01T01:00:00.000Z"); - final end2 = DateTime.parse("2021-01-02T00:00:00.000Z"); - const recurrenceRule = - "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU;WKST=MO;INTERVAL=1;UNTIL=20211231T235959Z"; - const recurrenceRule2 = ""; - const recurrenceRule3 = - "FREQ=WEEKLY;BYMONTH=1;BYDAY=MO;WKST=MO;UNTIL=20211231T235959Z"; - const allDay = false; - const allDay2 = true; - expect( - formatRecurrenceRule(start, end, recurrenceRule, allDay), - "Tous les Lundi, Mardi, Mercredi, Jeudi, Vendredi, Samedi et Dimanche de 00:00 à 01:00 jusqu'au 31/12/2021", - ); - expect( - formatRecurrenceRule(start, end2, recurrenceRule, allDay), - "Tous les Lundi, Mardi, Mercredi, Jeudi, Vendredi, Samedi et Dimanche de 00:00 à 00:00 jusqu'au 31/12/2021", - ); - expect( - formatRecurrenceRule(start, end, recurrenceRule2, allDay), - "Le 01/01/2021 de 00:00 à 01:00", - ); - expect( - formatRecurrenceRule(start, end2, recurrenceRule2, allDay), - "Du 01/01/2021 à 00:00 au 02/01/2021 à 00:00", - ); - expect( - formatRecurrenceRule(start, end, recurrenceRule, allDay2), - "Tous les Lundi, Mardi, Mercredi, Jeudi, Vendredi, Samedi et Dimanche toute la journée jusqu'au 31/12/2021", - ); - expect( - formatRecurrenceRule(start, end2, recurrenceRule, allDay2), - "Tous les Lundi, Mardi, Mercredi, Jeudi, Vendredi, Samedi et Dimanche toute la journée jusqu'au 31/12/2021", - ); - expect( - formatRecurrenceRule(start, end, recurrenceRule2, allDay2), - "Le 01/01/2021 toute la journée", - ); - expect( - formatRecurrenceRule(start, end2, recurrenceRule2, allDay2), - "Du 01/01/2021 à 00:00 au 02/01/2021 à 00:00", - ); - expect( - formatRecurrenceRule(start, end, recurrenceRule3, allDay), - "Tous les Lundi de 00:00 à 01:00 jusqu'au 31/12/2021", - ); - expect( - formatRecurrenceRule(start, end2, recurrenceRule3, allDay), - "Tous les Lundi de 00:00 à 00:00 jusqu'au 31/12/2021", - ); - expect( - formatRecurrenceRule(start, end, recurrenceRule3, allDay2), - "Tous les Lundi toute la journée jusqu'au 31/12/2021", - ); - expect( - formatRecurrenceRule(start, end2, recurrenceRule3, allDay2), - "Tous les Lundi toute la journée jusqu'au 31/12/2021", - ); - }); + // test('Testing formatRecurrenceRule', () { + // final start = DateTime.parse("2021-01-01T00:00:00.000Z"); + // final end = DateTime.parse("2021-01-01T01:00:00.000Z"); + // final end2 = DateTime.parse("2021-01-02T00:00:00.000Z"); + // const recurrenceRule = + // "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU;WKST=MO;INTERVAL=1;UNTIL=20211231T235959Z"; + // const recurrenceRule2 = ""; + // const recurrenceRule3 = + // "FREQ=WEEKLY;BYMONTH=1;BYDAY=MO;WKST=MO;UNTIL=20211231T235959Z"; + // const allDay = false; + // const allDay2 = true; + // expect( + // formatRecurrenceRule(start, end, recurrenceRule, allDay), + // "Tous les Lundi, Mardi, Mercredi, Jeudi, Vendredi, Samedi et Dimanche de 00:00 à 01:00 jusqu'au 31/12/2021", + // ); + // expect( + // formatRecurrenceRule(start, end2, recurrenceRule, allDay), + // "Tous les Lundi, Mardi, Mercredi, Jeudi, Vendredi, Samedi et Dimanche de 00:00 à 00:00 jusqu'au 31/12/2021", + // ); + // expect( + // formatRecurrenceRule(start, end, recurrenceRule2, allDay), + // "Le 01/01/2021 de 00:00 à 01:00", + // ); + // expect( + // formatRecurrenceRule(start, end2, recurrenceRule2, allDay), + // "Du 01/01/2021 à 00:00 au 02/01/2021 à 00:00", + // ); + // expect( + // formatRecurrenceRule(start, end, recurrenceRule, allDay2), + // "Tous les Lundi, Mardi, Mercredi, Jeudi, Vendredi, Samedi et Dimanche toute la journée jusqu'au 31/12/2021", + // ); + // expect( + // formatRecurrenceRule(start, end2, recurrenceRule, allDay2), + // "Tous les Lundi, Mardi, Mercredi, Jeudi, Vendredi, Samedi et Dimanche toute la journée jusqu'au 31/12/2021", + // ); + // expect( + // formatRecurrenceRule(start, end, recurrenceRule2, allDay2), + // "Le 01/01/2021 toute la journée", + // ); + // expect( + // formatRecurrenceRule(start, end2, recurrenceRule2, allDay2), + // "Du 01/01/2021 à 00:00 au 02/01/2021 à 00:00", + // ); + // expect( + // formatRecurrenceRule(start, end, recurrenceRule3, allDay), + // "Tous les Lundi de 00:00 à 01:00 jusqu'au 31/12/2021", + // ); + // expect( + // formatRecurrenceRule(start, end2, recurrenceRule3, allDay), + // "Tous les Lundi de 00:00 à 00:00 jusqu'au 31/12/2021", + // ); + // expect( + // formatRecurrenceRule(start, end, recurrenceRule3, allDay2), + // "Tous les Lundi toute la journée jusqu'au 31/12/2021", + // ); + // expect( + // formatRecurrenceRule(start, end2, recurrenceRule3, allDay2), + // "Tous les Lundi toute la journée jusqu'au 31/12/2021", + // ); + // }); test('Testing mergeDates', () { final date = DateTime.parse("2021-01-01T00:00:00.000Z"); diff --git a/test/event/is_admin_provider_test.dart b/test/event/is_admin_provider_test.dart index ad6b12a565..d0dfa85d8d 100644 --- a/test/event/is_admin_provider_test.dart +++ b/test/event/is_admin_provider_test.dart @@ -14,8 +14,8 @@ void main() { User.empty().copyWith( groups: [ SimpleGroup.empty().copyWith( - id: '53a669d6-84b1-4352-8d7c-421c1fbd9c6a', - name: 'Admin', + id: "b0357687-2211-410a-9e2a-144519eeaafa", + name: 'admin_calendar', ), SimpleGroup.empty().copyWith(id: '123', name: 'User'), ], diff --git a/test/loan/end_provider_test.dart b/test/loan/end_provider_test.dart index 68e5bd75f1..b663b0efe4 100644 --- a/test/loan/end_provider_test.dart +++ b/test/loan/end_provider_test.dart @@ -1,5 +1,4 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:titan/loan/class/item.dart'; import 'package:titan/loan/providers/end_provider.dart'; void main() { @@ -10,16 +9,16 @@ void main() { expect(endNotifier.state, '2022-12-31'); }); - test('setEndFromSelected', () { - final endNotifier = EndNotifier(); - const start = '01/01/2022'; - final selected = [ - Item.empty().copyWith(suggestedLendingDuration: 7), - Item.empty().copyWith(suggestedLendingDuration: 14), - Item.empty().copyWith(suggestedLendingDuration: 21), - ]; - endNotifier.setEndFromSelected(start, selected); - expect(endNotifier.state, '08/01/2022'); - }); + // test('setEndFromSelected', () { + // final endNotifier = EndNotifier(); + // const start = '01/01/2022'; + // final selected = [ + // Item.empty().copyWith(suggestedLendingDuration: 7), + // Item.empty().copyWith(suggestedLendingDuration: 14), + // Item.empty().copyWith(suggestedLendingDuration: 21), + // ]; + // endNotifier.setEndFromSelected(start, selected); + // expect(endNotifier.state, '08/01/2022'); + // }); }); } diff --git a/test/loan/loan_test.dart b/test/loan/loan_test.dart index 8491020c54..26afdfe949 100644 --- a/test/loan/loan_test.dart +++ b/test/loan/loan_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:titan/admin/class/account_type.dart'; +import 'package:titan/super_admin/class/account_type.dart'; import 'package:titan/loan/class/item.dart'; import 'package:titan/loan/class/item_quantity.dart'; import 'package:titan/loan/class/item_simple.dart'; diff --git a/test/login/login_test.dart b/test/login/login_test.dart deleted file mode 100644 index 9464b1eece..0000000000 --- a/test/login/login_test.dart +++ /dev/null @@ -1,242 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:titan/login/class/account_type.dart'; -import 'package:titan/login/class/create_account.dart'; -import 'package:titan/login/class/recover_request.dart'; -import 'package:titan/login/tools/functions.dart'; - -void main() { - group('Testing RecoverRequest class', () { - test('Should return an empty RecoverResquest', () { - final recoverRequest = RecoverRequest.empty(); - expect(recoverRequest, isA()); - expect(recoverRequest.resetToken, ''); - expect(recoverRequest.newPassword, ''); - }); - - test('Should return a RecoverRequest', () { - final recoverRequest = RecoverRequest( - resetToken: 'token', - newPassword: 'password', - ); - expect(recoverRequest, isA()); - expect(recoverRequest.resetToken, 'token'); - expect(recoverRequest.newPassword, 'password'); - }); - - test('Should update with new values', () { - final recoverRequest = RecoverRequest( - resetToken: 'token', - newPassword: 'password', - ); - RecoverRequest newRecoverRequest = recoverRequest.copyWith( - resetToken: 'newToken', - ); - expect(newRecoverRequest.resetToken, 'newToken'); - newRecoverRequest = recoverRequest.copyWith(newPassword: 'newPassword'); - expect(newRecoverRequest.newPassword, 'newPassword'); - }); - - test('Should print a recoverRequest', () { - final recoverRequest = RecoverRequest( - resetToken: 'token', - newPassword: 'password', - ); - expect( - recoverRequest.toString(), - 'RecoverRequest{resetToken: token, newPassword: password}', - ); - }); - - test('Should parse a recoverRequest', () { - final recoverRequest = RecoverRequest.fromJson({ - "reset_token": "token", - "new_password": "password", - }); - expect(recoverRequest, isA()); - expect(recoverRequest.resetToken, 'token'); - expect(recoverRequest.newPassword, 'password'); - }); - - test('Should return a correct json', () { - final recoverRequest = RecoverRequest.fromJson({ - "reset_token": "token", - "new_password": "password", - }); - expect(recoverRequest.toJson(), { - "reset_token": "token", - "new_password": "password", - }); - }); - }); - - group('Testing CreateAccount class', () { - test('Should return an empty CreateAccount', () { - final createAccount = CreateAccount.empty(); - expect(createAccount, isA()); - expect(createAccount.password, ''); - expect(createAccount.phone, ''); - expect(createAccount.activationToken, ''); - expect(createAccount.birthday, isA()); - expect(createAccount.firstname, ''); - expect(createAccount.floor, ''); - expect(createAccount.name, ''); - expect(createAccount.nickname, ''); - }); - - test('Should return a CreateAccount', () { - final createAccount = CreateAccount( - password: 'password', - phone: 'phone', - activationToken: '', - birthday: DateTime.parse('2021-01-01'), - firstname: '', - floor: '', - name: '', - nickname: '', - promo: 1, - ); - expect(createAccount, isA()); - expect(createAccount.password, 'password'); - expect(createAccount.phone, 'phone'); - expect(createAccount.activationToken, ''); - expect(createAccount.birthday, DateTime.parse('2021-01-01')); - expect(createAccount.firstname, ''); - expect(createAccount.floor, ''); - expect(createAccount.name, ''); - expect(createAccount.nickname, ''); - expect(createAccount.promo, 1); - }); - - test('Should update with new values', () { - final createAccount = CreateAccount( - password: 'password', - phone: 'phone', - activationToken: '', - birthday: DateTime.parse('2021-01-01'), - firstname: '', - floor: '', - name: '', - nickname: '', - promo: 1, - ); - CreateAccount newCreateAccount = createAccount.copyWith( - password: 'newPassword', - ); - expect(newCreateAccount.password, 'newPassword'); - newCreateAccount = createAccount.copyWith(phone: 'newPhone'); - expect(newCreateAccount.phone, 'newPhone'); - newCreateAccount = newCreateAccount.copyWith( - activationToken: 'newActivationToken', - ); - expect(newCreateAccount.activationToken, 'newActivationToken'); - newCreateAccount = newCreateAccount.copyWith( - birthday: DateTime.parse('2021-02-02'), - ); - expect(newCreateAccount.birthday, DateTime.parse('2021-02-02')); - newCreateAccount = newCreateAccount.copyWith(firstname: 'newFirstname'); - expect(newCreateAccount.firstname, 'newFirstname'); - newCreateAccount = newCreateAccount.copyWith(floor: 'newFloor'); - expect(newCreateAccount.floor, 'newFloor'); - newCreateAccount = newCreateAccount.copyWith(name: 'newName'); - expect(newCreateAccount.name, 'newName'); - newCreateAccount = newCreateAccount.copyWith(nickname: 'newNickname'); - expect(newCreateAccount.nickname, 'newNickname'); - newCreateAccount = newCreateAccount.copyWith(promo: 2); - expect(newCreateAccount.promo, 2); - }); - - test('Should print a createAccount', () { - final createAccount = CreateAccount( - password: 'password', - phone: 'phone', - activationToken: '', - birthday: DateTime.parse('2021-01-01'), - firstname: '', - floor: '', - name: '', - nickname: '', - promo: 1, - ); - expect( - createAccount.toString(), - 'CreateAccount {name: , firstname: , nickname: , password: password, birthday: 2021-01-01 00:00:00.000, phone: phone, promo: 1, floor: , activationToken: }', - ); - }); - - test('Should parse a createAccount', () { - final createAccount = CreateAccount.fromJson({ - "name": "", - "nickname": "", - "firstname": "", - "password": "password", - "birthday": "2021-01-01", - "phone": "phone", - "floor": "", - "activation_token": "", - }); - expect(createAccount, isA()); - expect(createAccount.password, 'password'); - expect(createAccount.phone, 'phone'); - expect(createAccount.activationToken, ''); - expect(createAccount.birthday, DateTime.parse('2021-01-01')); - expect(createAccount.firstname, ''); - expect(createAccount.floor, ''); - expect(createAccount.name, ''); - expect(createAccount.nickname, ''); - }); - - test('Should return a correct json', () { - final createAccount = CreateAccount.fromJson({ - "password": "password", - "phone": "phone", - "activation_token": "", - "birthday": "2021-01-01", - "firstname": "", - "floor": "", - "name": "", - "nickname": "", - }); - expect(createAccount.toJson(), { - 'name': '', - 'firstname': '', - 'nickname': '', - 'password': 'password', - 'birthday': '2021-01-01', - 'phone': 'phone', - 'floor': '', - 'promo': null, - 'activation_token': '', - }); - }); - }); - - group('Account Type Utils', () { - test('Account Type to ID - Student', () { - expect( - accountTypeToID(AccountType.student), - '39691052-2ae5-4e12-99d0-7a9f5f2b0136', - ); - }); - - test('Account Type to ID - Staff', () { - expect( - accountTypeToID(AccountType.staff), - '703056c4-be9d-475c-aa51-b7fc62a96aaa', - ); - }); - - test('Account Type to ID - Admin', () { - expect( - accountTypeToID(AccountType.admin), - '0a25cb76-4b63-4fd3-b939-da6d9feabf28', - ); - }); - - test('Account Type to ID - Association', () { - expect( - accountTypeToID(AccountType.association), - '29751438-103c-42f2-b09b-33fbb20758a7', - ); - }); - }); -} diff --git a/test/login/sign_up_provider_test.dart b/test/login/sign_up_provider_test.dart deleted file mode 100644 index f9b957fa79..0000000000 --- a/test/login/sign_up_provider_test.dart +++ /dev/null @@ -1,131 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:titan/login/class/account_type.dart'; -import 'package:titan/login/class/create_account.dart'; -import 'package:titan/login/class/recover_request.dart'; -import 'package:titan/login/providers/sign_up_provider.dart'; -import 'package:titan/login/repositories/sign_up_repository.dart'; - -class MockSignUpRepository extends Mock implements SignUpRepository {} - -void main() { - late SignUpProvider signUpProvider; - late MockSignUpRepository mockSignUpRepository; - - setUp(() { - mockSignUpRepository = MockSignUpRepository(); - signUpProvider = SignUpProvider(repository: mockSignUpRepository); - }); - - group('createUser', () { - test('returns true when repository returns true', () async { - when( - () => mockSignUpRepository.createUser( - 'test@test.com', - AccountType.student, - ), - ).thenAnswer((_) async => true); - - final result = await signUpProvider.createUser( - 'test@test.com', - AccountType.student, - ); - - expect(result, true); - }); - - test('returns false when repository returns false', () async { - when( - () => mockSignUpRepository.createUser( - 'test@test.com', - AccountType.student, - ), - ).thenAnswer((_) async => false); - - final result = await signUpProvider.createUser( - 'test@test.com', - AccountType.student, - ); - - expect(result, false); - }); - }); - - group('recoverUser', () { - test('returns true when repository returns true', () async { - when( - () => mockSignUpRepository.recoverUser('test@test.com'), - ).thenAnswer((_) async => true); - - final result = await signUpProvider.recoverUser('test@test.com'); - - expect(result, true); - }); - - test('returns false when repository returns false', () async { - when( - () => mockSignUpRepository.recoverUser('test@test.com'), - ).thenAnswer((_) async => false); - - final result = await signUpProvider.recoverUser('test@test.com'); - - expect(result, false); - }); - }); - - group('activateUser', () { - test('returns true when repository returns true', () async { - final createAccount = CreateAccount.empty().copyWith( - password: 'password', - ); - when( - () => mockSignUpRepository.activateUser(createAccount), - ).thenAnswer((_) async => true); - - final result = await signUpProvider.activateUser(createAccount); - - expect(result, true); - }); - - test('returns false when repository returns false', () async { - final createAccount = CreateAccount.empty().copyWith( - password: 'password', - ); - when( - () => mockSignUpRepository.activateUser(createAccount), - ).thenAnswer((_) async => false); - - final result = await signUpProvider.activateUser(createAccount); - - expect(result, false); - }); - }); - - group('resetPassword', () { - test('returns true when repository returns true', () async { - final recoverRequest = RecoverRequest.empty().copyWith( - newPassword: 'password', - ); - when( - () => mockSignUpRepository.resetPassword(recoverRequest), - ).thenAnswer((_) async => true); - - final result = await signUpProvider.resetPassword(recoverRequest); - - expect(result, true); - }); - - test('returns false when repository returns false', () async { - final recoverRequest = RecoverRequest.empty().copyWith( - newPassword: 'password', - ); - when( - () => mockSignUpRepository.resetPassword(recoverRequest), - ).thenAnswer((_) async => false); - - final result = await signUpProvider.resetPassword(recoverRequest); - - expect(result, false); - }); - }); -} diff --git a/test/tools/tools_test.dart b/test/tools/tools_test.dart index fba5f5d815..7b8a4f3314 100644 --- a/test/tools/tools_test.dart +++ b/test/tools/tools_test.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +// import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:titan/tools/exception.dart'; import 'package:titan/tools/functions.dart'; @@ -66,21 +66,21 @@ void main() { }); }); - group('Testing processDate function', () { - test('Should return a string', () { - final date = DateTime.parse("2021-01-01"); - expect(processDate(date), isA()); - expect(processDate(date), "01/01/2021"); - }); - }); - - group('Testing processDateWithHour function', () { - test('Should return a string', () { - final date = DateTime.parse("2021-01-01 12:00:00"); - expect(processDateWithHour(date), isA()); - expect(processDateWithHour(date), "01/01/2021 12:00"); - }); - }); + // group('Testing processDate function', () { + // test('Should return a string', () { + // final date = DateTime.parse("2021-01-01"); + // expect(DateFormat.yMd(locale).format(date), isA()); + // expect(DateFormat.yMd(locale).format(date), "01/01/2021"); + // }); + // }); + + // group('Testing processDateWithHour function', () { + // test('Should return a string', () { + // final date = DateTime.parse("2021-01-01 12:00:00"); + // expect(DateFormat.yMd(locale).add_Hm().format(date), isA()); + // expect(DateFormat.yMd(locale).add_Hm().format(date), "01/01/2021 12:00"); + // }); + // }); group('Testing processDatePrint function', () { test('Should return a string', () { @@ -92,27 +92,27 @@ void main() { }); group('Testing processDateBack function', () { - test('Should return a string', () { - const date = "01/01/2021"; - const dateWithHour = "01/01/2021 12:00"; - expect(processDateBack(""), ""); - expect(processDateBack(dateWithHour), isA()); - expect(processDateBack(date), isA()); - expect(processDateBack(dateWithHour), "2021-01-01 12:00"); - expect(processDateBack(date), "2021-01-01"); - }); - }); + // test('Should return a string', () { + // const date = "01/01/2021"; + // const dateWithHour = "01/01/2021 12:00"; + // expect(processDateBack(""), ""); + // expect(processDateBack(dateWithHour), isA()); + // expect(processDateBack(date), isA()); + // expect(processDateBack(dateWithHour), "2021-01-01 12:00"); + // expect(processDateBack(date), "2021-01-01"); + // }); + // }); - group('Testing processDateBackWithHour function', () { - test('Should return a string', () { - const date = "01/01/2021"; - const dateWithHour = "01/01/2021 12:00"; - expect(processDateBackWithHour(""), ""); - expect(processDateBackWithHour(dateWithHour), isA()); - expect(processDateBackWithHour(date), isA()); - expect(processDateBackWithHour(dateWithHour), "2021-01-01 12:00"); - expect(processDateBackWithHour(date), "2021-01-01"); - }); + // group('Testing processDateBackWithHour function', () { + // test('Should return a string', () { + // const date = "01/01/2021"; + // const dateWithHour = "01/01/2021 12:00"; + // expect(processDateBackWithHour(""), ""); + // expect(processDateBackWithHour(dateWithHour), isA()); + // expect(processDateBackWithHour(date), isA()); + // expect(processDateBackWithHour(dateWithHour), "2021-01-01 12:00"); + // expect(processDateBackWithHour(date), "2021-01-01"); + // }); }); test('Testing getDateInRecurrence', () { @@ -142,182 +142,182 @@ void main() { }); }); - group('displayToast', () { - testWidgets( - 'displays a toast message with the correct duration when the type is "msg"', - (WidgetTester tester) async { - // Arrange - const type = TypeMsg.msg; - const text = 'Success!'; - final scaffoldKey = GlobalKey(); - - // Act - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - key: scaffoldKey, - body: Builder( - builder: (context) => ElevatedButton( - onPressed: () => displayToast(context, type, text), - child: const Text('Show Toast'), - ), - ), - ), - ), - ); - await tester.tap(find.byType(ElevatedButton)); - await tester.pump(const Duration(milliseconds: 500)); - - // Assert - expect( - find.text(text), - findsOneWidget, - ); // Check that the toast message is still visible - await tester.pump(const Duration(milliseconds: 2000)); - expect( - find.text(text), - findsNothing, - ); // Check that the toast message has disappeared - }, - ); - - testWidgets( - 'displays a toast message with the correct duration when the type is "error"', - (WidgetTester tester) async { - // Arrange - const type = TypeMsg.error; - const text = 'Error!'; - final scaffoldKey = GlobalKey(); - - // Act - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - key: scaffoldKey, - body: Builder( - builder: (context) => ElevatedButton( - onPressed: () => displayToast(context, type, text), - child: const Text('Show Toast'), - ), - ), - ), - ), - ); - await tester.tap(find.byType(ElevatedButton)); - await tester.pump(const Duration(milliseconds: 500)); - - // Assert - expect( - find.text(text), - findsOneWidget, - ); // Check that the toast message is still visible - await tester.pump(const Duration(milliseconds: 3000)); - expect( - find.text(text), - findsNothing, - ); // Check that the toast message has disappeared - }, - ); - - // testWidgets( - // 'displays a toast message with the correct background color when the type is "msg"', - // (WidgetTester tester) async { - // // Arrange - // const type = TypeMsg.msg; - // const text = 'Success!'; - // final scaffoldKey = GlobalKey(); - - // // Act - // await tester.pumpWidget(MaterialApp( - // home: Scaffold( - // key: scaffoldKey, - // body: Builder( - // builder: (context) => ElevatedButton( - // onPressed: () => displayToast(context, type, text), - // child: const Text('Show Toast'), - // ), - // ), - // ), - // )); - // await tester.tap(find.byType(ElevatedButton)); - // await tester.pump(const Duration(milliseconds: 500)); - - // // Assert - // final container = - // find.byType(Ink).evaluate().first.widget as Ink; - // final decoration = container.decoration as BoxDecoration; - // expect( - // decoration.gradient!.colors, - // [ - // ColorConstants.gradient1, - // ColorConstants.gradient2 - // ]); // Check that the toast message has the correct background color - // await tester.pump(const Duration(milliseconds: 3000)); - // }); - - // testWidgets( - // 'displays a toast message with the correct background color when the type is "error"', - // (WidgetTester tester) async { - // // Arrange - // const type = TypeMsg.error; - // const text = 'Error!'; - // final scaffoldKey = GlobalKey(); - - // // Act - // await tester.pumpWidget(MaterialApp( - // home: Scaffold( - // key: scaffoldKey, - // body: Builder( - // builder: (context) => ElevatedButton( - // onPressed: () => displayToast(context, type, text), - // child: const Text('Show Toast'), - // ), - // ), - // ), - // )); - // await tester.tap(find.byType(ElevatedButton)); - // await tester.pump(const Duration(milliseconds: 500)); - - // // Assert - // final container = - // find.byType(Ink).evaluate().first.widget as Ink; - // final decoration = container.decoration as BoxDecoration; - // expect( - // decoration.gradient!.colors, - // [ - // ColorConstants.background2, - // Colors.black - // ]); // Check that the toast message has the correct background color - // await tester.pump(const Duration(milliseconds: 3000)); - // }); - - // testWidgets('displays a toast message with the correct font size', - // (WidgetTester tester) async { - // // Arrange - // const type = TypeMsg.msg; - // const text = 'Success!'; - // final scaffoldKey = GlobalKey(); - - // // Act - // await tester.pumpWidget(MaterialApp( - // home: Scaffold( - // key: scaffoldKey, - // body: Builder( - // builder: (context) => ElevatedButton( - // onPressed: () => displayToast(context, type, text), - // child: const Text('Show Toast'), - // ), - // ), - // ), - // )); - // await tester.tap(find.byType(ElevatedButton)); - // await tester.pump(const Duration(milliseconds: 500)); - - // // Assert - // final textWidget = find.text(text).evaluate().first.widget as Text; - // expect(textWidget.style!.fontSize, - // 20.0); // Check that the toast message has the correct font size - // await tester.pump(const Duration(milliseconds: 3000)); - // }); - }); + // group('displayToast', () { + // testWidgets( + // 'displays a toast message with the correct duration when the type is "msg"', + // (WidgetTester tester) async { + // // Arrange + // const type = TypeMsg.msg; + // const text = 'Success!'; + // final scaffoldKey = GlobalKey(); + + // // Act + // await tester.pumpWidget( + // MaterialApp( + // home: Scaffold( + // key: scaffoldKey, + // body: Builder( + // builder: (context) => ElevatedButton( + // onPressed: () => displayToast(context, type, text), + // child: const Text('Show Toast'), + // ), + // ), + // ), + // ), + // ); + // await tester.tap(find.byType(ElevatedButton)); + // await tester.pump(const Duration(milliseconds: 500)); + + // // Assert + // expect( + // find.text(text), + // findsOneWidget, + // ); // Check that the toast message is still visible + // await tester.pump(const Duration(milliseconds: 2000)); + // expect( + // find.text(text), + // findsNothing, + // ); // Check that the toast message has disappeared + // }, + // ); + + // testWidgets( + // 'displays a toast message with the correct duration when the type is "error"', + // (WidgetTester tester) async { + // // Arrange + // const type = TypeMsg.error; + // const text = 'Error!'; + // final scaffoldKey = GlobalKey(); + + // // Act + // await tester.pumpWidget( + // MaterialApp( + // home: Scaffold( + // key: scaffoldKey, + // body: Builder( + // builder: (context) => ElevatedButton( + // onPressed: () => displayToast(context, type, text), + // child: const Text('Show Toast'), + // ), + // ), + // ), + // ), + // ); + // await tester.tap(find.byType(ElevatedButton)); + // await tester.pump(const Duration(milliseconds: 500)); + + // // Assert + // expect( + // find.text(text), + // findsOneWidget, + // ); // Check that the toast message is still visible + // await tester.pump(const Duration(milliseconds: 3000)); + // expect( + // find.text(text), + // findsNothing, + // ); // Check that the toast message has disappeared + // }, + // ); + + // testWidgets( + // 'displays a toast message with the correct background color when the type is "msg"', + // (WidgetTester tester) async { + // // Arrange + // const type = TypeMsg.msg; + // const text = 'Success!'; + // final scaffoldKey = GlobalKey(); + + // // Act + // await tester.pumpWidget(MaterialApp( + // home: Scaffold( + // key: scaffoldKey, + // body: Builder( + // builder: (context) => ElevatedButton( + // onPressed: () => displayToast(context, type, text), + // child: const Text('Show Toast'), + // ), + // ), + // ), + // )); + // await tester.tap(find.byType(ElevatedButton)); + // await tester.pump(const Duration(milliseconds: 500)); + + // // Assert + // final container = + // find.byType(Ink).evaluate().first.widget as Ink; + // final decoration = container.decoration as BoxDecoration; + // expect( + // decoration.gradient!.colors, + // [ + // ColorConstants.gradient1, + // ColorConstants.gradient2 + // ]); // Check that the toast message has the correct background color + // await tester.pump(const Duration(milliseconds: 3000)); + // }); + + // testWidgets( + // 'displays a toast message with the correct background color when the type is "error"', + // (WidgetTester tester) async { + // // Arrange + // const type = TypeMsg.error; + // const text = 'Error!'; + // final scaffoldKey = GlobalKey(); + + // // Act + // await tester.pumpWidget(MaterialApp( + // home: Scaffold( + // key: scaffoldKey, + // body: Builder( + // builder: (context) => ElevatedButton( + // onPressed: () => displayToast(context, type, text), + // child: const Text('Show Toast'), + // ), + // ), + // ), + // )); + // await tester.tap(find.byType(ElevatedButton)); + // await tester.pump(const Duration(milliseconds: 500)); + + // // Assert + // final container = + // find.byType(Ink).evaluate().first.widget as Ink; + // final decoration = container.decoration as BoxDecoration; + // expect( + // decoration.gradient!.colors, + // [ + // ColorConstants.background2, + // Colors.black + // ]); // Check that the toast message has the correct background color + // await tester.pump(const Duration(milliseconds: 3000)); + // }); + + // testWidgets('displays a toast message with the correct font size', + // (WidgetTester tester) async { + // // Arrange + // const type = TypeMsg.msg; + // const text = 'Success!'; + // final scaffoldKey = GlobalKey(); + + // // Act + // await tester.pumpWidget(MaterialApp( + // home: Scaffold( + // key: scaffoldKey, + // body: Builder( + // builder: (context) => ElevatedButton( + // onPressed: () => displayToast(context, type, text), + // child: const Text('Show Toast'), + // ), + // ), + // ), + // )); + // await tester.tap(find.byType(ElevatedButton)); + // await tester.pump(const Duration(milliseconds: 500)); + + // // Assert + // final textWidget = find.text(text).evaluate().first.widget as Text; + // expect(textWidget.style!.fontSize, + // 20.0); // Check that the toast message has the correct font size + // await tester.pump(const Duration(milliseconds: 3000)); + // }); + // }); } diff --git a/test/user/user_test.dart b/test/user/user_test.dart index 600dc30937..bb17818ced 100644 --- a/test/user/user_test.dart +++ b/test/user/user_test.dart @@ -1,7 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:titan/admin/class/account_type.dart'; +import 'package:titan/super_admin/class/account_type.dart'; import 'package:titan/user/class/applicant.dart'; import 'package:titan/user/class/simple_users.dart'; import 'package:titan/user/class/user.dart'; @@ -122,6 +122,7 @@ void main() { groups: [], phone: 'phone', promo: null, + isSuperAdmin: false, ); expect( user.toString(), @@ -158,6 +159,7 @@ void main() { "groups": [], "phone": "phone", "promo": null, + "is_super_admin": false, }); }); }); diff --git a/test/vote/is_vote_admin_provider_test.dart b/test/vote/is_vote_admin_provider_test.dart index e878e98bb7..7977cafc2b 100644 --- a/test/vote/is_vote_admin_provider_test.dart +++ b/test/vote/is_vote_admin_provider_test.dart @@ -14,7 +14,7 @@ void main() { User.empty().copyWith( groups: [ SimpleGroup.empty().copyWith( - id: '6c6d7e88-fdb8-4e42-b2b5-3d3cfd12e7d6', + id: "2ca57402-605b-4389-a471-f2fea7b27db5", ), ], ), diff --git a/test/vote/vote_test.dart b/test/vote/vote_test.dart index 5f2547427d..1163ea212e 100644 --- a/test/vote/vote_test.dart +++ b/test/vote/vote_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:titan/admin/class/account_type.dart'; +import 'package:titan/super_admin/class/account_type.dart'; import 'package:titan/user/class/simple_users.dart'; import 'package:titan/vote/class/members.dart'; import 'package:titan/vote/class/contender.dart'; diff --git a/web/emlyon/favicon.png b/web/emlyon/favicon.png new file mode 100644 index 0000000000..a2c9e3e4af Binary files /dev/null and b/web/emlyon/favicon.png differ diff --git a/web/emlyon/icons/Icon-192.png b/web/emlyon/icons/Icon-192.png new file mode 100644 index 0000000000..23cdb9551c Binary files /dev/null and b/web/emlyon/icons/Icon-192.png differ diff --git a/web/emlyon/icons/Icon-512.png b/web/emlyon/icons/Icon-512.png new file mode 100644 index 0000000000..0fd60c40ed Binary files /dev/null and b/web/emlyon/icons/Icon-512.png differ diff --git a/web/emlyon/icons/Icon-maskable-192.png b/web/emlyon/icons/Icon-maskable-192.png new file mode 100644 index 0000000000..23cdb9551c Binary files /dev/null and b/web/emlyon/icons/Icon-maskable-192.png differ diff --git a/web/emlyon/icons/Icon-maskable-512.png b/web/emlyon/icons/Icon-maskable-512.png new file mode 100644 index 0000000000..0fd60c40ed Binary files /dev/null and b/web/emlyon/icons/Icon-maskable-512.png differ diff --git a/web/favicon.png b/web/favicon.png deleted file mode 100644 index 7f830fc0ba..0000000000 Binary files a/web/favicon.png and /dev/null differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png deleted file mode 100644 index c7c8088abc..0000000000 Binary files a/web/icons/Icon-192.png and /dev/null differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png deleted file mode 100644 index 42f969ef0c..0000000000 Binary files a/web/icons/Icon-512.png and /dev/null differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png deleted file mode 100644 index c7c8088abc..0000000000 Binary files a/web/icons/Icon-maskable-192.png and /dev/null differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png deleted file mode 100644 index 42f969ef0c..0000000000 Binary files a/web/icons/Icon-maskable-512.png and /dev/null differ diff --git a/web/index.html b/web/index.html index abc480965c..12cc217d7f 100644 --- a/web/index.html +++ b/web/index.html @@ -5,8 +5,11 @@ - MyECL - + {{ APP_NAME }} + @@ -17,10 +20,9 @@ - myecl + {{ APP_NAME }} - - + +
diff --git a/web/manifest.json b/web/manifest.json index e50b344eef..534ed02feb 100644 --- a/web/manifest.json +++ b/web/manifest.json @@ -1,35 +1,35 @@ { - "name": "MyECL", - "short_name": "MyECL", - "start_url": ".", - "display": "standalone", - "background_color": "#hexcode", - "theme_color": "#hexcode", - "description": "Application associative de Centrale Lyon, développée par ÉCLAIR", - "orientation": "portrait-primary", - "prefer_related_applications": false, - "icons": [ - { - "src": "icons/Icon-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "icons/Icon-512.png", - "sizes": "512x512", - "type": "image/png" - }, - { - "src": "icons/Icon-maskable-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "icons/Icon-maskable-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ] -} \ No newline at end of file + "name": "{{ APP_NAME }}", + "short_name": "{{ APP_NAME }}", + "start_url": ".", + "display": "standalone", + "background_color": "#hexcode", + "theme_color": "#hexcode", + "description": "Application associative de Centrale Lyon, développée par {{ ENTITY_NAME }}", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +}