diff --git a/.github/workflows/flutter-web.yml b/.github/workflows/flutter-web.yml new file mode 100644 index 00000000..169c111f --- /dev/null +++ b/.github/workflows/flutter-web.yml @@ -0,0 +1,36 @@ +name: Flutter Web Build and Deploy + +on: + push: + branches: [ main, dev-v1 ] + workflow_dispatch: + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: subosito/flutter-action@v2 + with: + flutter-version: '3.24.0' + channel: 'stable' + + - name: Install dependencies + run: flutter pub get + + - name: Enable web + run: flutter config --enable-web + + - name: Add web platform + run: flutter create . --platforms web + + - name: Build web + run: flutter build web --release --web-renderer html + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./build/web \ No newline at end of file diff --git a/.gitignore b/.gitignore index 78a6f473..367c9b5f 100644 --- a/.gitignore +++ b/.gitignore @@ -129,3 +129,13 @@ config/ firebase_config/ node_modules/ +# Git configuration files +.mailmap + +# Flutter DevTools configuration +devtools_options.yaml + +# Exam and test files +EXAM_PREPARATION.md +tests/test_compilation.dart + diff --git a/.mailmap b/.mailmap deleted file mode 100644 index ff985995..00000000 --- a/.mailmap +++ /dev/null @@ -1 +0,0 @@ -Sabrina Papeau diff --git a/assets/jobs.json b/assets/jobs.json index 975290a8..318765b9 100644 --- a/assets/jobs.json +++ b/assets/jobs.json @@ -9,7 +9,7 @@ "jobType": "CDI", "category": "UX/UI", "applicationsCount": 0, - "logoUrl": "https://i.imgur.com/czQq7xP.png", + "logoUrl": "https://zupimages.net/up/25/51/fu5s.png", "createdAt": { ".sv": "timestamp" } }, "ux_ui_2": { @@ -21,7 +21,7 @@ "jobType": "CDI", "category": "UX/UI", "applicationsCount": 0, - "logoUrl": "https://i.imgur.com/F4K2hQJ.png", + "logoUrl": "https://zupimages.net/up/25/51/k5ue.png", "createdAt": { ".sv": "timestamp" } }, "ux_ui_3": { @@ -33,7 +33,7 @@ "jobType": "Full Remote", "category": "UX/UI", "applicationsCount": 0, - "logoUrl": "https://i.imgur.com/7kUxVWq.png", + "logoUrl": "https://zupimages.net/up/25/51/7aau.png", "createdAt": { ".sv": "timestamp" } }, @@ -46,7 +46,7 @@ "jobType": "CDI", "category": "Data", "applicationsCount": 0, - "logoUrl": "https://i.imgur.com/UzPSjhc.png", + "logoUrl": "https://zupimages.net/up/25/51/kjw3.png", "createdAt": { ".sv": "timestamp" } }, "data_2": { @@ -58,7 +58,7 @@ "jobType": "CDI", "category": "Data", "applicationsCount": 0, - "logoUrl": "https://i.imgur.com/VdQDc5T.png", + "logoUrl": "https://zupimages.net/up/25/51/6zlp.png", "createdAt": { ".sv": "timestamp" } }, "data_3": { @@ -70,7 +70,7 @@ "jobType": "CDI", "category": "Data", "applicationsCount": 0, - "logoUrl": "https://i.imgur.com/iMMm9cL.png", + "logoUrl": "https://zupimages.net/up/25/51/kjw3.png", "createdAt": { ".sv": "timestamp" } }, @@ -83,7 +83,7 @@ "jobType": "CDI", "category": "Security", "applicationsCount": 0, - "logoUrl": "https://i.imgur.com/GgB1PNp.png", + "logoUrl": "https://zupimages.net/up/25/51/fwxw.png", "createdAt": { ".sv": "timestamp" } }, "security_2": { @@ -107,7 +107,7 @@ "jobType": "CDI", "category": "Security", "applicationsCount": 0, - "logoUrl": "https://i.imgur.com/bCcGpKU.png", + "logoUrl": "https://zupimages.net/up/25/51/pw34.png", "createdAt": { ".sv": "timestamp" } }, diff --git a/assets/translations/en.json b/assets/translations/en.json index e05b1887..883e0622 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -48,7 +48,6 @@ "salary_expectations": "Salary Expectations", "salary_range_min": "Minimum Salary", "salary_range_max": "Maximum Salary", - "Tour as Guest": "Tour as Guest", "Annuler": "Cancel", "Postuler": "Apply", "Postuler pour": "Apply for", diff --git a/assets/translations/fr.json b/assets/translations/fr.json index 8169f57d..75af933d 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -48,7 +48,6 @@ "salary_expectations": "Attentes salariales", "salary_range_min": "Salaire minimum", "salary_range_max": "Salaire maximum", - "Tour as Guest": "Tour as Guest", "Annuler": "Annuler", "Postuler": "Postuler", "Postuler pour": "Postuler pour", @@ -76,7 +75,7 @@ "employer_signin_title": "Se connecter en tant que PRO", "employer_signin_subtitle": "Choisissez votre méthode de connexion", - "signin_with_siret": "Se connecter avec votre code SIRET", + "signin_with_siret": "Se connecter avec votre code SIRET + Mot de passe", "signin_with_siret_description": "Utilisez votre numéro SIRET pour accéder rapidement à votre compte employeur", "fast_access": "Accès rapide", "signin_with_ape_email_password": "Se connecter avec APE + Email + Mot de passe", @@ -105,7 +104,7 @@ "professional_email": "Email professionnel", "professional_email_placeholder": "contact@votre-entreprise.com", "your_password": "Votre mot de passe", - "ape_verification_info": "Nous vérifions que votre code APE et votre email correspondent à un compte employeur enregistré.", + "ape_verification_info": "We verify that your APE code and email address match a registered employer account.", "company_name": "Nom de la société", "company_name_placeholder": "Ex: ACME Corp", diff --git a/backend/.env.example b/backend/.env.example index 9c63e1af..659bf06b 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,36 +1,36 @@ -# MongoDB Configuration +# Database connection (MongoDB) MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/timeless?retryWrites=true&w=majority -# JWT Configuration +# JWT settings (authentication) JWT_SECRET=your_super_secret_jwt_key_here JWT_REFRESH_SECRET=your_refresh_token_secret_here JWT_EXPIRE=7d JWT_REFRESH_EXPIRE=30d -# Server Configuration +# Server settings PORT=5000 NODE_ENV=development -# Google OAuth Configuration +# Google OAuth settings GOOGLE_CLIENT_ID=your_google_client_id GOOGLE_CLIENT_SECRET=your_google_client_secret GOOGLE_CALLBACK_URL=http://localhost:5000/auth/google/callback -# Cloudinary Configuration (for file uploads) +# Cloudinary settings (file uploads) CLOUDINARY_CLOUD_NAME=your_cloud_name CLOUDINARY_API_KEY=your_api_key CLOUDINARY_API_SECRET=your_api_secret -# Email Configuration +# Email (SMTP) settings EMAIL_HOST=smtp.gmail.com EMAIL_PORT=587 EMAIL_USER=your_email@gmail.com EMAIL_PASS=your_app_password -# External APIs +# External job APIs RAPIDAPI_KEY=your_rapidapi_key_for_jobs ADZUNA_APP_ID=your_adzuna_app_id ADZUNA_API_KEY=your_adzuna_api_key -# Frontend URL -FRONTEND_URL=http://localhost:3000 \ No newline at end of file +# Frontend app URL +FRONTEND_URL=http://localhost:3000 diff --git a/backend/firestore.rules b/backend/firestore.rules deleted file mode 100644 index f8cabdc0..00000000 --- a/backend/firestore.rules +++ /dev/null @@ -1,123 +0,0 @@ -// Règles de sécurité Firestore pour Timeless -rules_version = '2'; - -service cloud.firestore { - match /databases/{database}/documents { - - // === FONCTIONS DE VALIDATION === - - // Vérifie si l'utilisateur est authentifié - function isAuthenticated() { - return request.auth != null; - } - - // Vérifie si l'utilisateur est le propriétaire du document - function isOwner(userId) { - return request.auth.uid == userId; - } - - // Vérifie si l'utilisateur a un rôle spécifique - function hasRole(role) { - return isAuthenticated() && - get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == role; - } - - // Validation des données de profil candidat - function isValidCandidateProfile() { - return request.resource.data.keys().hasAll(['email', 'fullName', 'role']) && - request.resource.data.role == 'candidate' && - request.resource.data.email is string && - request.resource.data.fullName is string && - request.resource.data.email.matches('.*@.*\\..*'); - } - - // Validation des données de CV - function isValidCVData() { - return request.resource.data.keys().hasAll(['fileName', 'fileSize', 'contentType', 'uploadedAt']) && - request.resource.data.fileSize <= 10485760 && // 10MB max - request.resource.data.contentType in ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']; - } - - // Validation des candidatures - function isValidApplication() { - return request.resource.data.keys().hasAll(['jobId', 'candidateId', 'appliedAt', 'status']) && - request.resource.data.status in ['pending', 'reviewed', 'accepted', 'rejected'] && - exists(/databases/$(database)/documents/jobs/$(request.resource.data.jobId)); - } - - // === COLLECTIONS PRINCIPALES === - - // Collection des utilisateurs - match /users/{userId} { - allow read, write: if isAuthenticated() && isOwner(userId); - allow create: if isAuthenticated() && isOwner(userId) && isValidCandidateProfile(); - } - - // Collection des profils candidats détaillés - match /candidate_profiles/{candidateId} { - allow read: if isAuthenticated() && (isOwner(candidateId) || hasRole('recruiter')); - allow write: if isAuthenticated() && isOwner(candidateId); - allow create: if isAuthenticated() && isOwner(candidateId); - } - - // Collection des CVs - match /cvs/{cvId} { - allow read: if isAuthenticated() && - (isOwner(resource.data.candidateId) || hasRole('recruiter')); - allow create: if isAuthenticated() && - isOwner(request.resource.data.candidateId) && - isValidCVData(); - allow update: if isAuthenticated() && isOwner(resource.data.candidateId); - allow delete: if isAuthenticated() && isOwner(resource.data.candidateId); - } - - // Collection des candidatures - match /applications/{applicationId} { - allow read: if isAuthenticated() && - (isOwner(resource.data.candidateId) || hasRole('recruiter')); - allow create: if isAuthenticated() && - isOwner(request.resource.data.candidateId) && - isValidApplication(); - allow update: if isAuthenticated() && - (isOwner(resource.data.candidateId) || hasRole('recruiter')); - } - - // === COLLECTIONS PUBLIQUES (LECTURE SEULE) === - - // Annonces d'emploi - lecture publique pour les candidats authentifiés - match /jobs/{jobId} { - allow read: if isAuthenticated(); - allow write: if hasRole('recruiter') || hasRole('admin'); - } - - // Collections legacy (à migrer progressivement) - match /allPost/{postId} { - allow read: if isAuthenticated(); - allow write: if hasRole('recruiter') || hasRole('admin'); - } - - match /category/{categoryName}/{jobId} { - allow read: if isAuthenticated(); - allow write: if hasRole('recruiter') || hasRole('admin'); - } - - // === COLLECTIONS D'AUTHENTIFICATION === - - match /Auth/User/register/{userId} { - allow read, write: if isAuthenticated() && isOwner(userId); - } - - // === COLLECTIONS ANALYTICS (ADMIN SEULEMENT) === - - match /analytics/{document=**} { - allow read, write: if hasRole('admin'); - } - - // === RÈGLES PAR DÉFAUT === - - // Interdire tout accès par défaut - match /{document=**} { - allow read, write: if false; - } - } -} \ No newline at end of file diff --git a/backend/models/User.js b/backend/models/User.js index 2950197b..35298e02 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -2,7 +2,8 @@ const mongoose = require('mongoose'); const bcrypt = require('bcryptjs'); const userSchema = new mongoose.Schema({ - // Basic Information + + // Basic user info email: { type: String, required: [true, 'Email is required'], @@ -11,52 +12,58 @@ const userSchema = new mongoose.Schema({ trim: true, match: [/^\S+@\S+\.\S+$/, 'Please enter a valid email'] }, + password: { type: String, minlength: [6, 'Password must be at least 6 characters'], - select: false // Don't include password in queries by default + select: false // Hide password by default }, - - // Profile Information + + // Profile info firstName: { type: String, required: [true, 'First name is required'], trim: true, maxlength: [50, 'First name cannot exceed 50 characters'] }, + lastName: { type: String, required: [true, 'Last name is required'], trim: true, maxlength: [50, 'Last name cannot exceed 50 characters'] }, + phoneNumber: { type: String, trim: true, match: [/^[\+]?[1-9][\d]{0,15}$/, 'Please enter a valid phone number'] }, - - // Professional Information + + // Professional details title: { type: String, trim: true, maxlength: [100, 'Title cannot exceed 100 characters'] }, + bio: { type: String, maxlength: [500, 'Bio cannot exceed 500 characters'] }, + skills: [{ type: String, trim: true }], + experience: { type: String, enum: ['internship', 'junior', 'mid', 'senior', 'expert'], default: 'junior' }, - - // Location + + // Location data location: { city: String, country: String, @@ -65,12 +72,13 @@ const userSchema = new mongoose.Schema({ default: false } }, - - // Files + + // Uploaded files profilePicture: { url: String, - publicId: String // Cloudinary public ID for deletion + publicId: String // Used to delete the file }, + cv: { url: String, publicId: String, @@ -79,21 +87,22 @@ const userSchema = new mongoose.Schema({ default: Date.now } }, - - // Social Links + + // Social profiles socialLinks: { linkedin: String, github: String, portfolio: String, behance: String }, - - // Job Preferences + + // Job preferences jobPreferences: { categories: [{ type: String, enum: ['development', 'design', 'marketing', 'data', 'product', 'management', 'sales', 'other'] }], + salaryRange: { min: Number, max: Number, @@ -102,17 +111,19 @@ const userSchema = new mongoose.Schema({ default: 'EUR' } }, + workType: [{ type: String, enum: ['remote', 'hybrid', 'onsite'] }], + contractType: [{ type: String, enum: ['fulltime', 'parttime', 'contract', 'internship'] }] }, - - // Job Activity + + // Job activity savedJobs: [{ jobId: String, savedAt: { @@ -120,6 +131,7 @@ const userSchema = new mongoose.Schema({ default: Date.now } }], + appliedJobs: [{ jobId: String, appliedAt: { @@ -132,74 +144,73 @@ const userSchema = new mongoose.Schema({ default: 'applied' } }], - - // Authentication + + // Auth data googleId: String, emailVerified: { type: Boolean, default: false }, + emailVerificationToken: String, passwordResetToken: String, passwordResetExpires: Date, refreshTokens: [String], - - // Account Status + + // Account state isActive: { type: Boolean, default: true }, + role: { type: String, enum: ['user', 'employer', 'admin'], default: 'user' }, - - // Analytics + + // Login stats lastLogin: Date, loginCount: { type: Number, default: 0 }, - + }, { timestamps: true, toJSON: { virtuals: true }, toObject: { virtuals: true } }); -// Virtual for full name +// Full name virtual userSchema.virtual('fullName').get(function() { return `${this.firstName} ${this.lastName}`; }); -// Index for better query performance +// Indexes for faster queries userSchema.index({ email: 1 }); userSchema.index({ 'location.city': 1 }); userSchema.index({ skills: 1 }); userSchema.index({ 'jobPreferences.categories': 1 }); -// Pre-save middleware to hash password +// Hash password before save userSchema.pre('save', async function(next) { - // Only hash the password if it has been modified (or is new) if (!this.isModified('password')) return next(); - + try { - // Hash password with cost of 12 - const hashedPassword = await bcrypt.hash(this.password, 12); - this.password = hashedPassword; + this.password = await bcrypt.hash(this.password, 12); next(); } catch (error) { next(error); } }); -// Instance method to check password +// Check password userSchema.methods.comparePassword = async function(candidatePassword) { return await bcrypt.compare(candidatePassword, this.password); }; -// Instance method to generate JWT payload +// Build JWT payload userSchema.methods.toJWT = function() { return { id: this._id, @@ -210,9 +221,9 @@ userSchema.methods.toJWT = function() { }; }; -// Static method to find by email +// Find user by email userSchema.statics.findByEmail = function(email) { return this.findOne({ email: email.toLowerCase() }); }; -module.exports = mongoose.model('User', userSchema); \ No newline at end of file +module.exports = mongoose.model('User', userSchema); diff --git a/backend/server.js b/backend/server.js index 76e93e38..39e3b80e 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,3 +1,4 @@ +// backend/server.js const express = require('express'); const mongoose = require('mongoose'); const cors = require('cors'); diff --git a/devtools_options.yaml b/devtools_options.yaml deleted file mode 100644 index fa0b357c..00000000 --- a/devtools_options.yaml +++ /dev/null @@ -1,3 +0,0 @@ -description: This file stores settings for Dart & Flutter DevTools. -documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states -extensions: diff --git a/ios/.gitignore b/ios/.gitignore index 7a7f9873..f7b1a0e9 100644 --- a/ios/.gitignore +++ b/ios/.gitignore @@ -32,3 +32,7 @@ Runner/GeneratedPluginRegistrant.* !default.mode2v3 !default.pbxuser !default.perspectivev3 +Runner.xcworkspace/xcshareddata/ +# Legacy Flutter artifacts +Flutter/app.flx +Flutter/app.zip diff --git a/lib/api/api_country.dart b/lib/api/api_country.dart index 1c683cd4..7e6d30e1 100644 --- a/lib/api/api_country.dart +++ b/lib/api/api_country.dart @@ -4,21 +4,23 @@ import 'package:http/http.dart' as http; import 'package:timeless/api/model/api_country_model.dart'; import 'package:timeless/services/http_service.dart'; +// API service to fetch country/state/city data class CountrySearch { + // Gets all countries with their states and cities from GitHub repo + // This is used for location pickers throughout the app static Future?> countNotification() async { var url = "https://raw.githubusercontent.com/prof22/country_state_city_picker/main/lib/assets/country.json"; - http.Response? response = await HttpService.getApi( - url: url, - ); + + http.Response? response = await HttpService.getApi(url: url); + if (response!.statusCode == 200) { return searchCountryFromJson(response.body); } else { - - if (kDebugMode) { - print(jsonDecode(response.body)); - } - + // Log error in debug mode only + if (kDebugMode) { + print(jsonDecode(response.body)); + } } return null; } diff --git a/lib/api/model/api_country_model.dart b/lib/api/model/api_country_model.dart index 2a9df872..66669c38 100644 --- a/lib/api/model/api_country_model.dart +++ b/lib/api/model/api_country_model.dart @@ -1,9 +1,9 @@ -// To parse this JSON data, do -// -// final searchCountry = searchCountryFromJson(jsonString); +// Country search model for location picker +// Handles the hierarchical structure: Country > State > City import 'dart:convert'; +// Helper functions to parse JSON data from API List searchCountryFromJson(String str) => List.from( json.decode(str).map((x) => SearchCountry.fromJson(x))); @@ -21,10 +21,10 @@ class SearchCountry { }); int? id; - String? name; - String? emoji; - String? emojiU; - List? state; + String? name; // "France", "Canada", etc. + String? emoji; // 🇫🇷, 🇨🇦 + String? emojiU; // Unicode version of flag + List? state; // All states/regions in this country factory SearchCountry.fromJson(Map json) => SearchCountry( id: json["id"], @@ -52,9 +52,9 @@ class State { }); int? id; - String? name; - int? countryId; - List? city; + String? name; // "Île-de-France", "California", "Ontario" + int? countryId; // Points back to parent country + List? city; // All cities in this state factory State.fromJson(Map json) => State( id: json["id"], @@ -79,8 +79,8 @@ class City { }); int? id; - String? name; - int? stateId; + String? name; // "Paris", "Los Angeles", "Toronto" + int? stateId; // Points back to parent state factory City.fromJson(Map json) => City( id: json["id"], diff --git a/lib/common/widgets/accessibility_fab.dart b/lib/common/widgets/accessibility_fab.dart index cdd83bce..56ec76bc 100644 --- a/lib/common/widgets/accessibility_fab.dart +++ b/lib/common/widgets/accessibility_fab.dart @@ -13,7 +13,7 @@ class AccessibilityFAB extends StatelessWidget { return Obx(() => Positioned( right: 16, - bottom: 80, // Au-dessus de la barre de navigation + bottom: 80, // Positioning it just above the navigation bar child: accessibilityService.buildAccessibleWidget( semanticLabel: 'Open accessibility settings', onTap: () { diff --git a/lib/common/widgets/back_button.dart b/lib/common/widgets/back_button.dart index 830e94d2..7dee44e3 100644 --- a/lib/common/widgets/back_button.dart +++ b/lib/common/widgets/back_button.dart @@ -3,44 +3,12 @@ import 'package:get/get.dart'; import 'package:timeless/utils/asset_res.dart'; import 'package:timeless/utils/color_res.dart'; +// Default back button used in the app Widget backButton({VoidCallback? onTap}) { - return Builder( - builder: (context) => InkWell( - onTap: onTap ?? () { - if (Navigator.canPop(context)) { - Navigator.pop(context); - } else { - Get.offAllNamed('/dashboard'); - } - }, - child: Container( - height: 36, - width: 36, - decoration: BoxDecoration( - color: ColorRes.white, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: const Color(0xFF000647), - width: 2, - ), - boxShadow: [ - BoxShadow( - color: const Color(0xFF000647).withOpacity(0.2), - blurRadius: 6, - offset: const Offset(0, 2), - ), - ], - ), - child: const Icon( - Icons.arrow_back, - color: ColorRes.black, - size: 18, - ), - ), - ), - ); + return universalBackButton(onTap: onTap); } +// Reusable universal back button with optional text Widget universalBackButton({ VoidCallback? onTap, String? text, @@ -48,6 +16,7 @@ Widget universalBackButton({ }) { return Builder( builder: (context) => InkWell( + // Go back if possible, else redirect to dashboard onTap: onTap ?? () { if (Navigator.canPop(context)) { Navigator.pop(context); @@ -55,6 +24,7 @@ Widget universalBackButton({ Get.offAllNamed('/dashboard'); } }, + borderRadius: BorderRadius.circular(8), child: Container( padding: showText ? const EdgeInsets.symmetric(horizontal: 12, vertical: 8) : null, height: showText ? null : 36, @@ -63,47 +33,50 @@ Widget universalBackButton({ color: ColorRes.white, borderRadius: BorderRadius.circular(8), border: Border.all( - color: const Color(0xFF000647), + color: ColorRes.primaryBlue, width: 2, ), boxShadow: [ BoxShadow( - color: const Color(0xFF000647).withOpacity(0.2), + color: ColorRes.primaryBlue.withOpacity(0.2), blurRadius: 6, offset: const Offset(0, 2), ), ], ), - child: showText ? Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.arrow_back, - color: ColorRes.black, - size: 18, - ), - if (text != null) ...[ - const SizedBox(width: 8), - Text( - text, - style: const TextStyle( - color: ColorRes.black, - fontSize: 14, - fontWeight: FontWeight.w600, - ), + child: showText + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.arrow_back, + color: ColorRes.black, + size: 18, + ), + if (text != null) ...[ + const SizedBox(width: 8), + Text( + text, + style: const TextStyle( + color: ColorRes.black, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ], + ], + ) + : const Icon( + Icons.arrow_back, + color: ColorRes.black, + size: 18, ), - ], - ], - ) : const Icon( - Icons.arrow_back, - color: ColorRes.black, - size: 18, - ), ), ), ); } +// widget to display the app logo Widget logo() { return Container( alignment: Alignment.center, @@ -116,4 +89,4 @@ Widget logo() { image: AssetImage(AssetRes.logo), ), ); -} \ No newline at end of file +} diff --git a/lib/common/widgets/common_error_box.dart b/lib/common/widgets/common_error_box.dart index 4e47a1cb..ce4c6f66 100644 --- a/lib/common/widgets/common_error_box.dart +++ b/lib/common/widgets/common_error_box.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:timeless/utils/app_style.dart'; import 'package:timeless/utils/color_res.dart'; +// widget to show common error box Widget commonErrorBox(String text) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2), diff --git a/lib/common/widgets/common_loader.dart b/lib/common/widgets/common_loader.dart index b7d0867a..9d94b7fc 100644 --- a/lib/common/widgets/common_loader.dart +++ b/lib/common/widgets/common_loader.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:timeless/utils/color_res.dart'; +// Loading indicator widget used across the app class CommonLoader extends StatelessWidget { const CommonLoader({super.key}); diff --git a/lib/common/widgets/common_text_field.dart b/lib/common/widgets/common_text_field.dart index e5c62ddc..bd692250 100644 --- a/lib/common/widgets/common_text_field.dart +++ b/lib/common/widgets/common_text_field.dart @@ -1,15 +1,17 @@ import 'package:flutter/material.dart'; import 'package:timeless/utils/color_res.dart'; -Widget commonTextFormField( - {InputDecoration? textDecoration, - TextEditingController? controller, - VoidCallback? onTap, - Function(String)? onChanged, - TextInputType? type, - bool? readOnly, - Color? color, - bool? obscureText}) { +// Reusable text field used across the app +Widget commonTextFormField({ + InputDecoration? textDecoration, + TextEditingController? controller, + VoidCallback? onTap, + Function(String)? onChanged, + TextInputType? type, + bool? readOnly, + Color? color, + bool? obscureText, +}) { return Container( height: 50, decoration: BoxDecoration( @@ -25,14 +27,8 @@ Widget commonTextFormField( onChanged: onChanged ?? (value) {}, obscureText: obscureText ?? false, readOnly: readOnly ?? false, - // Ensure copy/paste always works + // Enable copy / paste on all devices enableInteractiveSelection: true, - toolbarOptions: const ToolbarOptions( - copy: true, - cut: true, - paste: true, - selectAll: true, - ), ), ); } diff --git a/lib/common/widgets/copy_paste_text_field.dart b/lib/common/widgets/copy_paste_text_field.dart index 5ae0607f..975eb7fd 100644 --- a/lib/common/widgets/copy_paste_text_field.dart +++ b/lib/common/widgets/copy_paste_text_field.dart @@ -1,8 +1,10 @@ -// lib/common/widgets/copy_paste_text_field.dart +// Copy / paste text field helpers import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; +import '../../utils/color_res.dart'; +// TextFormField with copy / paste always enabled class CopyPasteTextField extends StatelessWidget { final TextEditingController controller; final InputDecoration? decoration; @@ -66,19 +68,13 @@ class CopyPasteTextField extends StatelessWidget { autofocus: autofocus, autocorrect: autocorrect, enableSuggestions: enableSuggestions, - // ALWAYS enable copy/paste functionality + // Always allow copy / paste enableInteractiveSelection: true, - toolbarOptions: const ToolbarOptions( - copy: true, - cut: true, - paste: true, - selectAll: true, - ), ); } } -// Enhanced TextField with copy/paste guarantee +// Simple TextField with copy / paste support class CopyPasteField extends StatelessWidget { final TextEditingController controller; final InputDecoration? decoration; @@ -118,33 +114,27 @@ class CopyPasteField extends StatelessWidget { enabled: enabled, readOnly: readOnly, onTap: onTap, - // ALWAYS enable copy/paste functionality + // Force copy / paste support enableInteractiveSelection: true, - toolbarOptions: const ToolbarOptions( - copy: true, - cut: true, - paste: true, - selectAll: true, - ), ); } } -// Utility to show copy success message +// Show a message when text is copied void showCopySuccessMessage({String? label}) { Get.snackbar( '✅ Copied!', '${label ?? 'Text'} copied to clipboard', snackPosition: SnackPosition.BOTTOM, duration: const Duration(seconds: 2), - backgroundColor: Colors.green, - colorText: Colors.white, + backgroundColor: ColorRes.primaryBlue, + colorText: ColorRes.white, margin: const EdgeInsets.all(8), borderRadius: 8, ); } -// Copy text to clipboard with visual feedback +// Copy text to clipboard and show feedback Future copyTextToClipboard(String text, {String? label}) async { await Clipboard.setData(ClipboardData(text: text)); showCopySuccessMessage(label: label); diff --git a/lib/common/widgets/helper.dart b/lib/common/widgets/helper.dart index 5b021402..252865d9 100644 --- a/lib/common/widgets/helper.dart +++ b/lib/common/widgets/helper.dart @@ -1,74 +1,69 @@ import 'package:intl/intl.dart'; +// Formatting a date based on its recency to the current date String getFormattedTime(DateTime time, {DateFormat? format}) { if (isToday(time)) { + // Today: show time if (format != null) { return format.format(time); } else { return DateFormat("h:mm a").format(time); } } else if (isYesterday(time)) { + // Yesterday return "Yesterday"; } else if (isInWeek(time)) { + // Same week: show day name return DateFormat("EEEE").format(time); } else if (isInMonth(time)) { + // Same month: show day/month return DateFormat("dd/MM").format(time); } else if (isInYear(time)) { + // Same year: show month return DateFormat("MMMM").format(time); } else { + // Older: show year return time.year.toString(); } } +// Check if date is today bool isToday(DateTime time) { DateTime now = DateTime.now(); - - if (now.year == time.year && now.month == time.month && now.day == time.day) { - return true; - } - return false; + return now.year == time.year && + now.month == time.month && + now.day == time.day; } +// Check if date is yesterday bool isYesterday(DateTime time) { DateTime now = DateTime.now(); - - if (now.year == time.year && + return now.year == time.year && now.month == time.month && - ((now.day - 1) == time.day)) { - return true; - } - return false; + (now.day - 1) == time.day; } +// Check if date is within the last week bool isInWeek(DateTime time) { DateTime now = DateTime.now(); - - if (now.year == time.year && + return now.year == time.year && now.month == time.month && - ((now.day - 6) > time.day)) { - return false; - } - return true; + (now.day - 6) <= time.day; } +// Check if date is in the current month bool isInMonth(DateTime time) { DateTime now = DateTime.now(); - - if (now.year == time.year && now.month == time.month) { - return true; - } - return false; + return now.year == time.year && now.month == time.month; } +// Check if date is in the current year bool isInYear(DateTime time) { DateTime now = DateTime.now(); - - if (now.year == time.year) { - return true; - } - return false; + return now.year == time.year; } +// Format date for alerts String getAlertString(DateTime time) { if (isToday(time)) { return "Today"; @@ -76,4 +71,4 @@ String getAlertString(DateTime time) { return "Yesterday"; } return DateFormat('dd MMMM yyyy').format(time); -} \ No newline at end of file +} diff --git a/lib/common/widgets/language_switcher.dart b/lib/common/widgets/language_switcher.dart index 158b7083..2203fa77 100644 --- a/lib/common/widgets/language_switcher.dart +++ b/lib/common/widgets/language_switcher.dart @@ -207,7 +207,7 @@ class LanguageSwitcher extends StatelessWidget { accessibilityService.triggerHapticFeedback(); translationService.setLanguage(languageCode); - // Show feedback + // Provide immediate visual feedback to the user that the language has changed. AppTheme.showStandardSnackBar( title: 'Language Changed', message: languageCode == 'en' @@ -271,7 +271,7 @@ class LanguageSwitcher extends StatelessWidget { } } -// Widget helper for floating language switcher +// A floating widget that allows users to switch languages from anywhere on the screen. class FloatingLanguageSwitcher extends StatelessWidget { const FloatingLanguageSwitcher({super.key}); diff --git a/lib/common/widgets/language_toggle.dart b/lib/common/widgets/language_toggle.dart index b25049cf..4663cbf4 100644 --- a/lib/common/widgets/language_toggle.dart +++ b/lib/common/widgets/language_toggle.dart @@ -1,10 +1,11 @@ -// lib/common/widgets/language_toggle.dart +// Language switch button (EN / FR) import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:timeless/services/unified_translation_service.dart'; import 'package:timeless/utils/app_theme.dart'; +// Toggle button to switch app language class LanguageToggle extends StatelessWidget { const LanguageToggle({super.key}); @@ -13,15 +14,15 @@ class LanguageToggle extends StatelessWidget { return Obx(() { final translationService = UnifiedTranslationService.instance; final isEnglish = translationService.currentLanguage.value == 'en'; - + return InkWell( onTap: () { translationService.toggleLanguage(); AppTheme.showStandardSnackBar( title: translationService.getText('language_switched'), - message: isEnglish - ? translationService.getText('switched_to_french') - : translationService.getText('switched_to_english'), + message: isEnglish + ? translationService.getText('switched_to_french') + : translationService.getText('switched_to_english'), ); }, borderRadius: BorderRadius.circular(20), @@ -30,15 +31,14 @@ class LanguageToggle extends StatelessWidget { decoration: BoxDecoration( color: const Color(0xFF000647), borderRadius: BorderRadius.circular(20), - border: Border.all(color: const Color(0xFF000647), width: 2.0), + border: Border.all( + color: const Color(0xFF000647), + width: 2.0, + ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - Text( - isEnglish ? '' : '', - style: const TextStyle(fontSize: 16), - ), const SizedBox(width: 6), Text( isEnglish ? 'EN' : 'FR', @@ -49,7 +49,7 @@ class LanguageToggle extends StatelessWidget { ), ), const SizedBox(width: 4), - Icon( + const Icon( Icons.swap_horiz, size: 14, color: Colors.white, @@ -60,4 +60,4 @@ class LanguageToggle extends StatelessWidget { ); }); } -} \ No newline at end of file +} diff --git a/lib/common/widgets/logout_menu.dart b/lib/common/widgets/logout_menu.dart index a0068753..d0557c3a 100644 --- a/lib/common/widgets/logout_menu.dart +++ b/lib/common/widgets/logout_menu.dart @@ -1,4 +1,4 @@ -// lib/common/widgets/logout_menu.dart +// Logout menu and actions import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:firebase_auth/firebase_auth.dart'; @@ -10,9 +10,10 @@ import 'package:timeless/services/preferences_service.dart'; class LogoutMenu extends StatelessWidget { const LogoutMenu({super.key}); + // Handle user logout with confirmation static Future handleLogout() async { try { - // Show confirmation dialog + // Ask user to confirm logout final shouldLogout = await Get.dialog( AlertDialog( backgroundColor: Colors.white, @@ -69,26 +70,24 @@ class LogoutMenu extends StatelessWidget { ); if (shouldLogout == true) { - // Show loading + // Show loading while logging out Get.dialog( - const Center( - child: CircularProgressIndicator(), - ), + const Center(child: CircularProgressIndicator()), barrierDismissible: false, ); // Clear local data await PreferencesService.clear(); - + // Sign out from Firebase await FirebaseAuth.instance.signOut(); - - // Close loading + + // Close loader Get.back(); - - // Navigate to login screen + + // Go back to login screen Get.offAllNamed(AppRes.firstScreen); - + // Show success message Get.snackbar( 'Logged Out', @@ -100,14 +99,15 @@ class LogoutMenu extends StatelessWidget { ); } } catch (e) { - // Close loading if open + // Close loader if still open if (Get.isDialogOpen == true) { Get.back(); } - + + // Show error message Get.snackbar( 'Error', - 'Unable to logout: ${e.toString()}', + 'Unable to logout', snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red, colorText: Colors.white, @@ -116,6 +116,7 @@ class LogoutMenu extends StatelessWidget { } } + // Build popup menu button static Widget buildMenuButton() { return PopupMenuButton( icon: Container( @@ -148,9 +149,9 @@ class LogoutMenu extends StatelessWidget { Get.offAllNamed(AppRes.dashBoardScreen); break; case 'profile': - // Navigate to profile if route exists + // Profile navigation (if needed) if (Get.routing.route?.settings.name != '/profile') { - // Add profile navigation here if needed + // TODO: Add profile navigation } break; case 'logout': @@ -219,4 +220,4 @@ class LogoutMenu extends StatelessWidget { Widget build(BuildContext context) { return buildMenuButton(); } -} \ No newline at end of file +} diff --git a/lib/common/widgets/modern_language_selector.dart b/lib/common/widgets/modern_language_selector.dart index 6ecbd203..9581f694 100644 --- a/lib/common/widgets/modern_language_selector.dart +++ b/lib/common/widgets/modern_language_selector.dart @@ -1,3 +1,4 @@ +// Modern language selector with accessibility support // lib/common/widgets/modern_language_selector.dart // ignore_for_file: deprecated_member_use, duplicate_ignore @@ -21,20 +22,30 @@ class ModernLanguageSelector extends StatelessWidget { @override Widget build(BuildContext context) { - final UnifiedTranslationService translationService = Get.find(); - final AccessibilityService accessibilityService = Get.find(); + final UnifiedTranslationService translationService = + Get.find(); + final AccessibilityService accessibilityService = + Get.find(); - return Obx(() => GestureDetector( - onTap: () { - accessibilityService.triggerHapticFeedback(); - _showLanguageSelection(context); - }, - child: isCompact ? _buildCompactSelector(translationService, accessibilityService) - : _buildFullSelector(translationService, accessibilityService), - )); + return Obx( + () => GestureDetector( + // Open language selection sheet + onTap: () { + accessibilityService.triggerHapticFeedback(); + _showLanguageSelection(context); + }, + child: isCompact + ? _buildCompactSelector(translationService, accessibilityService) + : _buildFullSelector(translationService, accessibilityService), + ), + ); } - Widget _buildCompactSelector(UnifiedTranslationService translationService, AccessibilityService accessibilityService) { + // Compact version used in tight layouts + Widget _buildCompactSelector( + UnifiedTranslationService translationService, + AccessibilityService accessibilityService, + ) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( @@ -46,7 +57,6 @@ class ModernLanguageSelector extends StatelessWidget { ), boxShadow: [ BoxShadow( - // ignore: deprecated_member_use color: Colors.black.withOpacity(0.1), blurRadius: 4, offset: const Offset(0, 2), @@ -56,11 +66,13 @@ class ModernLanguageSelector extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ + // Current language flag Text( translationService.currentFlag, style: const TextStyle(fontSize: 18), ), const SizedBox(width: 6), + // Language code (EN / FR) Text( translationService.currentLanguage.value.toUpperCase(), style: accessibilityService.getAccessibleTextStyle( @@ -80,7 +92,11 @@ class ModernLanguageSelector extends StatelessWidget { ); } - Widget _buildFullSelector(UnifiedTranslationService translationService, AccessibilityService accessibilityService) { + // Full version with label and gradient + Widget _buildFullSelector( + UnifiedTranslationService translationService, + AccessibilityService accessibilityService, + ) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( @@ -91,11 +107,13 @@ class ModernLanguageSelector extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ + // Current language flag Text( translationService.currentFlag, style: const TextStyle(fontSize: 20), ), const SizedBox(width: 8), + // Optional language label if (showLabel) ...[ Text( translationService.currentLanguageName, @@ -117,9 +135,12 @@ class ModernLanguageSelector extends StatelessWidget { ); } + // Bottom sheet to choose language and options void _showLanguageSelection(BuildContext context) { - final UnifiedTranslationService translationService = Get.find(); - final AccessibilityService accessibilityService = Get.find(); + final UnifiedTranslationService translationService = + Get.find(); + final AccessibilityService accessibilityService = + Get.find(); showModalBottomSheet( context: context, @@ -145,7 +166,7 @@ class ModernLanguageSelector extends StatelessWidget { ), child: Column( children: [ - // Handle + // Drag handle Container( width: 40, height: 4, @@ -155,7 +176,7 @@ class ModernLanguageSelector extends StatelessWidget { borderRadius: BorderRadius.circular(2), ), ), - + // Header Padding( padding: const EdgeInsets.all(20), @@ -185,26 +206,35 @@ class ModernLanguageSelector extends StatelessWidget { ], ), ), - - // Auto-translate toggle + + // Auto-translation toggle if (showAutoTranslateToggle) ...[ Padding( padding: const EdgeInsets.symmetric(horizontal: 20), - child: Obx(() => _buildAutoTranslateToggle(translationService, accessibilityService)), + child: Obx( + () => _buildAutoTranslateToggle( + translationService, + accessibilityService, + ), + ), ), const Divider(height: 1), ], - - // Languages list + + // Language list Expanded( child: ListView.builder( controller: scrollController, padding: const EdgeInsets.all(20), - itemCount: translationService.availableLanguageCodes.length, + itemCount: + translationService.availableLanguageCodes.length, itemBuilder: (context, index) { - final langCode = translationService.availableLanguageCodes[index]; - final isSelected = translationService.currentLanguage.value == langCode; - + final langCode = + translationService.availableLanguageCodes[index]; + final isSelected = + translationService.currentLanguage.value == + langCode; + return Padding( padding: const EdgeInsets.only(bottom: 8), child: _buildLanguageOption( @@ -229,7 +259,11 @@ class ModernLanguageSelector extends StatelessWidget { ); } - Widget _buildAutoTranslateToggle(UnifiedTranslationService translationService, AccessibilityService accessibilityService) { + // Toggle for automatic translation + Widget _buildAutoTranslateToggle( + UnifiedTranslationService translationService, + AccessibilityService accessibilityService, + ) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( @@ -281,6 +315,7 @@ class ModernLanguageSelector extends StatelessWidget { ); } + // Single language option item Widget _buildLanguageOption( BuildContext context, String langCode, @@ -301,26 +336,27 @@ class ModernLanguageSelector extends StatelessWidget { child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: isSelected + color: isSelected ? accessibilityService.primaryColor.withOpacity(0.1) : accessibilityService.cardBackgroundColor, borderRadius: BorderRadius.circular(12), border: Border.all( - color: isSelected + color: isSelected ? accessibilityService.primaryColor : accessibilityService.borderColor.withOpacity(0.3), - width: isSelected - ? (accessibilityService.isHighContrastMode.value ? 3 : 2) + width: isSelected + ? (accessibilityService.isHighContrastMode.value ? 3 : 2) : 1, ), ), child: Row( children: [ + // Flag icon Container( width: 40, height: 40, decoration: BoxDecoration( - color: isSelected + color: isSelected ? accessibilityService.primaryColor.withOpacity(0.1) : Colors.grey[100], borderRadius: BorderRadius.circular(20), @@ -333,6 +369,7 @@ class ModernLanguageSelector extends StatelessWidget { ), ), const SizedBox(width: 16), + // Language name and code Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -341,8 +378,9 @@ class ModernLanguageSelector extends StatelessWidget { languageName, style: accessibilityService.getAccessibleTextStyle( fontSize: AppTheme.fontSizeMedium, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, - color: isSelected + fontWeight: + isSelected ? FontWeight.w600 : FontWeight.w500, + color: isSelected ? accessibilityService.primaryColor : accessibilityService.textColor, ), @@ -357,6 +395,7 @@ class ModernLanguageSelector extends StatelessWidget { ], ), ), + // Selected check icon if (isSelected) Container( width: 24, @@ -378,35 +417,40 @@ class ModernLanguageSelector extends StatelessWidget { } } -// Floating action button version +// Floating button version for quick language switch class FloatingLanguageSelector extends StatelessWidget { const FloatingLanguageSelector({super.key}); @override Widget build(BuildContext context) { - final UnifiedTranslationService translationService = Get.find(); + final UnifiedTranslationService translationService = + Get.find(); return Positioned( top: MediaQuery.of(context).padding.top + 16, right: 16, - child: Obx(() => FloatingActionButton.small( - heroTag: "language_selector", - onPressed: () { - _showQuickLanguageSwitch(context); - }, - backgroundColor: Colors.white, - foregroundColor: Colors.black87, - elevation: 4, - child: Text( - translationService.currentFlag, - style: const TextStyle(fontSize: 18), + child: Obx( + () => FloatingActionButton.small( + heroTag: "language_selector", + onPressed: () { + _showQuickLanguageSwitch(context); + }, + backgroundColor: Colors.white, + foregroundColor: Colors.black87, + elevation: 4, + child: Text( + translationService.currentFlag, + style: const TextStyle(fontSize: 18), + ), ), - )), + ), ); } + // Quick toggle between languages void _showQuickLanguageSwitch(BuildContext context) { - final UnifiedTranslationService translationService = Get.find(); + final UnifiedTranslationService translationService = + Get.find(); translationService.toggleLanguage(); } -} \ No newline at end of file +} diff --git a/lib/common/widgets/timeless_components.dart b/lib/common/widgets/timeless_components.dart index 9b95c980..f886f50b 100644 --- a/lib/common/widgets/timeless_components.dart +++ b/lib/common/widgets/timeless_components.dart @@ -1,14 +1,16 @@ +// Timeless UI components +// Reusable buttons, inputs, layouts and helpers used across the app + import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:timeless/utils/color_res.dart'; import 'package:timeless/utils/app_dimensions.dart'; import 'package:timeless/utils/app_style.dart'; -// ========================================== -// COMPOSANTS UI STANDARDISÉS TIMELESS -// ========================================== +// ------------------------------------------ +// BUTTONS +// ------------------------------------------ -// Bouton principal standard +// Main button used everywhere in the app class TimelessButton extends StatelessWidget { final String text; final VoidCallback? onPressed; @@ -118,7 +120,11 @@ class TimelessButton extends StatelessWidget { enum ButtonSize { small, medium, large } -// Champ de texte standard optimisé +// ------------------------------------------ +// TEXT FIELDS +// ------------------------------------------ + +// Standard text field following the Timeless design system class TimelessTextField extends StatelessWidget { final String? label; final String? hintText; @@ -156,11 +162,9 @@ class TimelessTextField extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Optional field label if (label != null) ...[ - Text( - label!, - style: AppTextStyles.labelMedium, - ), + Text(label!, style: AppTextStyles.labelMedium), SizedBox(height: AppDimensions.xs), ], Container( @@ -214,7 +218,11 @@ class TimelessTextField extends StatelessWidget { } } -// Carte standard avec padding optimisé +// ------------------------------------------ +// CARDS & LAYOUT +// ------------------------------------------ + +// Basic card container with consistent style class TimelessCard extends StatelessWidget { final Widget child; final EdgeInsets? padding; @@ -251,7 +259,7 @@ class TimelessCard extends StatelessWidget { } } -// Container de page optimisé pour éviter le scroll +// Page wrapper handling app bar, safe area and keyboard class TimelessPageContainer extends StatelessWidget { final Widget child; final String? title; @@ -285,13 +293,16 @@ class TimelessPageContainer extends StatelessWidget { leading: showBackButton ? IconButton( icon: Icon(Icons.arrow_back, color: ColorRes.textPrimary), - onPressed: onBackPressed ?? () => Navigator.of(context).pop(), + onPressed: + onBackPressed ?? () => Navigator.of(context).pop(), ) : null, title: title != null ? Text( title!, - style: AppTextStyles.h4.copyWith(color: ColorRes.textPrimary), + style: AppTextStyles.h4.copyWith( + color: ColorRes.textPrimary, + ), ) : null, actions: actions, @@ -302,7 +313,9 @@ class TimelessPageContainer extends StatelessWidget { constraints: BoxConstraints( maxWidth: AppDimensions.maxContentWidth, ), - margin: EdgeInsets.symmetric(horizontal: AppDimensions.pageHorizontalPadding), + margin: EdgeInsets.symmetric( + horizontal: AppDimensions.pageHorizontalPadding, + ), child: child, ), ), @@ -310,7 +323,11 @@ class TimelessPageContainer extends StatelessWidget { } } -// Formulaire compact optimisé +// ------------------------------------------ +// FORMS & SECTIONS +// ------------------------------------------ + +// Helper to build clean and compact forms class TimelessForm extends StatelessWidget { final List children; final String? title; @@ -342,15 +359,20 @@ class TimelessForm extends StatelessWidget { SizedBox(height: AppDimensions.sm), ], if (subtitle != null) ...[ - Text(subtitle!, style: AppTextStyles.bodyMedium.copyWith( - color: ColorRes.textSecondary, - )), + Text( + subtitle!, + style: AppTextStyles.bodyMedium.copyWith( + color: ColorRes.textSecondary, + ), + ), SizedBox(height: AppDimensions.lg), ], - ...children.map((child) => Padding( - padding: AppDimensions.formFieldMargin, - child: child, - )).toList(), + ...children.map( + (child) => Padding( + padding: AppDimensions.formFieldMargin, + child: child, + ), + ), if (submitButton != null) ...[ SizedBox(height: AppDimensions.lg), submitButton!, @@ -361,7 +383,7 @@ class TimelessForm extends StatelessWidget { } } -// Section avec espacement standardisé +// Section wrapper with optional title class TimelessSection extends StatelessWidget { final String? title; final Widget child; @@ -392,7 +414,11 @@ class TimelessSection extends StatelessWidget { } } -// Badge et chips standardisés +// ------------------------------------------ +// CHIPS +// ------------------------------------------ + +// Small selectable chip used for tags and filters class TimelessChip extends StatelessWidget { final String text; final Color? backgroundColor; @@ -411,8 +437,10 @@ class TimelessChip extends StatelessWidget { @override Widget build(BuildContext context) { - final bgColor = backgroundColor ?? (isSelected ? ColorRes.primaryBlue : ColorRes.grey100); - final txtColor = textColor ?? (isSelected ? ColorRes.white : ColorRes.textSecondary); + final bgColor = backgroundColor ?? + (isSelected ? ColorRes.primaryBlue : ColorRes.grey100); + final txtColor = + textColor ?? (isSelected ? ColorRes.white : ColorRes.textSecondary); return InkWell( onTap: onTap, @@ -434,4 +462,4 @@ class TimelessChip extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/common/widgets/universal_app_bar.dart b/lib/common/widgets/universal_app_bar.dart index 662e5985..ade142c0 100644 --- a/lib/common/widgets/universal_app_bar.dart +++ b/lib/common/widgets/universal_app_bar.dart @@ -1,7 +1,11 @@ +// Universal app bar and scaffold +// Shared layout widgets to keep navigation and headers consistent + import 'package:flutter/material.dart'; import 'package:timeless/utils/color_res.dart'; import 'package:timeless/common/widgets/back_button.dart'; +// Custom AppBar used across the app class UniversalAppBar extends StatelessWidget implements PreferredSizeWidget { final String? title; final bool showBackButton; @@ -30,19 +34,26 @@ class UniversalAppBar extends StatelessWidget implements PreferredSizeWidget { backgroundColor: backgroundColor ?? ColorRes.backgroundColor, elevation: elevation, centerTitle: centerTitle, - leading: leading ?? (showBackButton ? Padding( - padding: const EdgeInsets.all(8.0), - child: universalBackButton(onTap: onBackPressed), - ) : null), - title: title != null ? Text( - title!, - style: const TextStyle( - color: ColorRes.black, - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ) : null, + // Custom back button or custom leading widget + leading: leading ?? + (showBackButton + ? Padding( + padding: const EdgeInsets.all(8.0), + child: universalBackButton(onTap: onBackPressed), + ) + : null), + title: title != null + ? Text( + title!, + style: const TextStyle( + color: ColorRes.black, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ) + : null, actions: actions, + // Disable default back button behavior automaticallyImplyLeading: false, ); } @@ -51,6 +62,7 @@ class UniversalAppBar extends StatelessWidget implements PreferredSizeWidget { Size get preferredSize => const Size.fromHeight(kToolbarHeight); } +// Scaffold wrapper that includes the UniversalAppBar by default class TimelessScaffold extends StatelessWidget { final Widget body; final String? title; @@ -101,4 +113,4 @@ class TimelessScaffold extends StatelessWidget { bottomNavigationBar: bottomNavigationBar, ); } -} \ No newline at end of file +} diff --git a/lib/common/widgets/universal_back_fab.dart b/lib/common/widgets/universal_back_fab.dart index 9bbcb159..3d11a5f3 100644 --- a/lib/common/widgets/universal_back_fab.dart +++ b/lib/common/widgets/universal_back_fab.dart @@ -1,7 +1,11 @@ +// Back navigation floating buttons +// Reusable back buttons for different UI situations + import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:timeless/utils/color_res.dart'; +// Main floating back button used for navigation class UniversalBackFab extends StatelessWidget { final VoidCallback? onTap; final String? tooltip; @@ -17,18 +21,20 @@ class UniversalBackFab extends StatelessWidget { @override Widget build(BuildContext context) { return FloatingActionButton( - onPressed: onTap ?? () { - if (Navigator.canPop(context)) { - Navigator.pop(context); - } else { - Get.offAllNamed('/dashboard'); - } - }, + // Go back if possible, otherwise return to dashboard + onPressed: onTap ?? + () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + } else { + Get.offAllNamed('/dashboard'); + } + }, backgroundColor: ColorRes.white, foregroundColor: ColorRes.black, elevation: 4, mini: mini, - tooltip: tooltip ?? 'Retour', + tooltip: tooltip ?? 'Back', shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(mini ? 28 : 32), side: const BorderSide( @@ -44,6 +50,7 @@ class UniversalBackFab extends StatelessWidget { } } +// Lightweight back button for small or inline usage class SimpleBackFab extends StatelessWidget { final VoidCallback? onTap; final bool mini; @@ -79,13 +86,15 @@ class SimpleBackFab extends StatelessWidget { ], ), child: InkWell( - onTap: onTap ?? () { - if (Navigator.canPop(context)) { - Navigator.pop(context); - } else { - Get.offAllNamed('/dashboard'); - } - }, + // Same navigation logic as the main FAB + onTap: onTap ?? + () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + } else { + Get.offAllNamed('/dashboard'); + } + }, borderRadius: BorderRadius.circular(mini ? 28 : 32), child: SizedBox( width: mini ? 40 : 56, @@ -103,6 +112,7 @@ class SimpleBackFab extends StatelessWidget { } } +// Back button overlay for full screen pages (images, maps, etc.) class BackButtonOverlay extends StatelessWidget { final VoidCallback? onTap; final Alignment alignment; @@ -126,13 +136,15 @@ class BackButtonOverlay extends StatelessWidget { color: Colors.transparent, borderRadius: BorderRadius.circular(8), child: InkWell( - onTap: onTap ?? () { - if (Navigator.canPop(context)) { - Navigator.pop(context); - } else { - Get.offAllNamed('/dashboard'); - } - }, + // Back navigation with dashboard fallback + onTap: onTap ?? + () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + } else { + Get.offAllNamed('/dashboard'); + } + }, borderRadius: BorderRadius.circular(8), child: Container( height: 40, @@ -164,4 +176,4 @@ class BackButtonOverlay extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/config/api_config.dart b/lib/config/api_config.dart index d7401c29..6a5676f4 100644 --- a/lib/config/api_config.dart +++ b/lib/config/api_config.dart @@ -1,6 +1,6 @@ // lib/config/api_config.dart -// Configuration des clés API - FICHIER SENSIBLE -// ⚠️ NE PAS COMMITTER CE FICHIER SUR GIT ! +// API KEYS CONFIGURATION FILE +// ⚠️ NEVER COMMIT REAL API KEYS TO PUBLIC REPOS ⚠️ class ApiConfig { // Clé API Google Cloud Translation diff --git a/lib/config/api_config.example.dart b/lib/config/api_config.example.dart index 151767a0..43308e14 100644 --- a/lib/config/api_config.example.dart +++ b/lib/config/api_config.example.dart @@ -1,20 +1,21 @@ -// lib/config/api_config.example.dart -// Fichier exemple pour la configuration des clés API -// COPIEZ CE FICHIER VERS api_config.dart ET AJOUTEZ VOS VRAIES CLÉS +// API configuration example file +// Copy this file to api_config.dart and add your real keys class ApiConfig { - // Clé API Google Cloud Translation - // Obtenez votre clé sur: https://console.cloud.google.com/ - static const String googleTranslationApiKey = 'YOUR_GOOGLE_TRANSLATION_API_KEY_HERE'; - - // Autres clés API (pour usage futur) + // Google Cloud Translation API key + // Get your key from: https://console.cloud.google.com/ + static const String googleTranslationApiKey = + 'YOUR_GOOGLE_TRANSLATION_API_KEY_HERE'; + + // Other API keys (for future use) // static const String firebaseApiKey = 'YOUR_FIREBASE_KEY_HERE'; // static const String mapApiKey = 'YOUR_MAP_KEY_HERE'; - - // URLs de base - static const String googleTranslationBaseUrl = 'https://translation.googleapis.com/language/translate/v2'; - - // Configuration de debug + + // Base URL for Google Translation API + static const String googleTranslationBaseUrl = + 'https://translation.googleapis.com/language/translate/v2'; + + // Debug options static const bool enableApiLogging = true; static const bool enableErrorLogging = true; -} \ No newline at end of file +} diff --git a/lib/controllers/candidate_controller.dart b/lib/controllers/candidate_controller.dart index 79a7ed3c..20137029 100644 --- a/lib/controllers/candidate_controller.dart +++ b/lib/controllers/candidate_controller.dart @@ -1,4 +1,4 @@ -// Contrôleur pour la gestion des candidats avec GetX +// Candidate controller (GetX) import 'package:get/get.dart'; import 'package:flutter/foundation.dart'; import 'package:file_picker/file_picker.dart'; @@ -10,20 +10,21 @@ import '../services/candidate_api_service.dart'; import '../utils/app_theme.dart'; class CandidateController extends GetxController { - // État du profil candidat + + // Candidate profile final Rx _candidateProfile = Rx(null); CandidateProfileModel? get candidateProfile => _candidateProfile.value; - // Liste des CVs + // CV list final RxList _cvs = [].obs; List get cvs => _cvs; - // Liste des candidatures + // Applications list final RxList _applications = [].obs; List get applications => _applications; - // États de chargement + // Loading flags final RxBool _isLoading = false.obs; bool get isLoading => _isLoading.value; @@ -33,11 +34,11 @@ class CandidateController extends GetxController { final RxBool _isApplying = false.obs; bool get isApplying => _isApplying.value; - // Statistiques + // Candidate stats final RxMap _stats = {}.obs; Map get stats => _stats; - // Messages d'erreur + // Error message final RxString _errorMessage = ''.obs; String get errorMessage => _errorMessage.value; @@ -47,16 +48,16 @@ class CandidateController extends GetxController { _initializeData(); } - // Initialiser les données du candidat + // Load all candidate data Future _initializeData() async { try { _isLoading.value = true; _errorMessage.value = ''; - // Charger le profil candidat + // Load profile await loadCandidateProfile(); - // Charger les données associées si le profil existe + // Load related data if (_candidateProfile.value != null) { await Future.wait([ loadCVs(), @@ -65,27 +66,27 @@ class CandidateController extends GetxController { ]); } } catch (e) { - _errorMessage.value = 'Erreur lors du chargement des données: $e'; + _errorMessage.value = 'Failed to load data: $e'; if (kDebugMode) print('CandidateController init error: $e'); } finally { _isLoading.value = false; } } - // === GESTION DU PROFIL === + // --- Profile --- - // Charger le profil candidat + // Load candidate profile Future loadCandidateProfile() async { try { final profile = await CandidateApiService.getCurrentCandidateProfile(); _candidateProfile.value = profile; } catch (e) { - _errorMessage.value = 'Erreur lors du chargement du profil: $e'; + _errorMessage.value = 'Failed to load profile: $e'; if (kDebugMode) print('CandidateController loadProfile error: $e'); } } - // Créer un profil candidat + // Create profile Future createProfile({ required String email, required String fullName, @@ -108,16 +109,16 @@ class CandidateController extends GetxController { _candidateProfile.value = profile; AppTheme.showStandardSnackBar( - title: 'Succès', - message: 'Profil candidat créé avec succès', + title: 'Success', + message: 'Profile created', isSuccess: true, ); return true; } catch (e) { - _errorMessage.value = 'Erreur lors de la création du profil: $e'; + _errorMessage.value = 'Profile creation failed: $e'; AppTheme.showStandardSnackBar( - title: 'Erreur', + title: 'Error', message: _errorMessage.value, isError: true, ); @@ -128,7 +129,7 @@ class CandidateController extends GetxController { } } - // Mettre à jour le profil candidat + // Update profile Future updateProfile(CandidateProfileModel updatedProfile) async { try { _isLoading.value = true; @@ -138,20 +139,20 @@ class CandidateController extends GetxController { await CandidateApiService.updateCandidateProfile(updatedProfile); _candidateProfile.value = profile; - // Recharger les statistiques + // Refresh stats await loadStats(); AppTheme.showStandardSnackBar( - title: 'Succès', - message: 'Profil mis à jour avec succès', + title: 'Success', + message: 'Profile updated', isSuccess: true, ); return true; } catch (e) { - _errorMessage.value = 'Erreur lors de la mise à jour: $e'; + _errorMessage.value = 'Update failed: $e'; AppTheme.showStandardSnackBar( - title: 'Erreur', + title: 'Error', message: _errorMessage.value, isError: true, ); @@ -162,7 +163,7 @@ class CandidateController extends GetxController { } } - // Ajouter une compétence + // Add a skill void addSkill(String skill) { if (_candidateProfile.value == null || skill.isEmpty) return; @@ -175,7 +176,7 @@ class CandidateController extends GetxController { } } - // Supprimer une compétence + // Remove a skill void removeSkill(String skill) { if (_candidateProfile.value == null) return; @@ -186,7 +187,7 @@ class CandidateController extends GetxController { updateProfile(updatedProfile); } - // Ajouter une expérience + // Add work experience void addExperience(WorkExperience experience) { if (_candidateProfile.value == null) return; @@ -198,7 +199,7 @@ class CandidateController extends GetxController { updateProfile(updatedProfile); } - // Supprimer une expérience + // Remove work experience void removeExperience(String experienceId) { if (_candidateProfile.value == null) return; @@ -210,7 +211,7 @@ class CandidateController extends GetxController { updateProfile(updatedProfile); } - // Ajouter une formation + // Add education void addEducation(Education education) { if (_candidateProfile.value == null) return; @@ -222,7 +223,7 @@ class CandidateController extends GetxController { updateProfile(updatedProfile); } - // Supprimer une formation + // Remove education void removeEducation(String educationId) { if (_candidateProfile.value == null) return; @@ -234,62 +235,60 @@ class CandidateController extends GetxController { updateProfile(updatedProfile); } - // === GESTION DES CVS === + // --- CVs --- - // Charger la liste des CVs + // Load CV list Future loadCVs() async { try { final cvsList = await CandidateApiService.getCandidateCVs(); _cvs.value = cvsList; } catch (e) { - _errorMessage.value = 'Erreur lors du chargement des CVs: $e'; + _errorMessage.value = 'Failed to load CVs: $e'; if (kDebugMode) print('CandidateController loadCVs error: $e'); } } - // Upload d'un CV + // Upload a CV Future uploadCV() async { try { _isUploadingCV.value = true; _errorMessage.value = ''; - // Sélectionner un fichier + // Pick file FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: ['pdf', 'doc', 'docx'], allowMultiple: false, ); - if (result == null || result.files.isEmpty) { - return false; - } + if (result == null || result.files.isEmpty) return false; final file = File(result.files.single.path!); final fileName = result.files.single.name; - // Upload via le service + // Upload file final cvModel = await CandidateApiService.uploadCV( file: file, fileName: fileName, ); - // Mettre à jour la liste des CVs + // Update local list _cvs.add(cvModel); - // Recharger le profil pour avoir le CV actuel mis à jour + // Refresh profile await loadCandidateProfile(); AppTheme.showStandardSnackBar( - title: 'Succès', - message: 'CV uploadé avec succès', + title: 'Success', + message: 'CV uploaded', isSuccess: true, ); return true; } catch (e) { - _errorMessage.value = 'Erreur lors de l\'upload: $e'; + _errorMessage.value = 'Upload failed: $e'; AppTheme.showStandardSnackBar( - title: 'Erreur', + title: 'Error', message: _errorMessage.value, isError: true, ); @@ -300,7 +299,7 @@ class CandidateController extends GetxController { } } - // Supprimer un CV + // Delete a CV Future deleteCV(String cvId) async { try { _isLoading.value = true; @@ -308,23 +307,23 @@ class CandidateController extends GetxController { await CandidateApiService.deleteCV(cvId); - // Retirer de la liste locale + // Remove from list _cvs.removeWhere((cv) => cv.id == cvId); - // Recharger le profil si c'était le CV actuel + // Refresh profile await loadCandidateProfile(); AppTheme.showStandardSnackBar( - title: 'Succès', - message: 'CV supprimé avec succès', + title: 'Success', + message: 'CV deleted', isSuccess: true, ); return true; } catch (e) { - _errorMessage.value = 'Erreur lors de la suppression: $e'; + _errorMessage.value = 'Delete failed: $e'; AppTheme.showStandardSnackBar( - title: 'Erreur', + title: 'Error', message: _errorMessage.value, isError: true, ); @@ -335,7 +334,7 @@ class CandidateController extends GetxController { } } - // Définir un CV comme actuel + // Set current CV Future setCurrentCV(String cvId) async { if (_candidateProfile.value == null) return false; @@ -343,21 +342,21 @@ class CandidateController extends GetxController { return await updateProfile(updatedProfile); } - // === GESTION DES CANDIDATURES === + // --- Applications --- - // Charger la liste des candidatures + // Load applications Future loadApplications() async { try { final applicationsList = await CandidateApiService.getCandidateApplications(); _applications.value = applicationsList; } catch (e) { - _errorMessage.value = 'Erreur lors du chargement des candidatures: $e'; + _errorMessage.value = 'Failed to load applications: $e'; if (kDebugMode) print('CandidateController loadApplications error: $e'); } } - // Postuler à une annonce + // Apply to a job Future applyToJob({ required String jobId, String? coverLetter, @@ -375,23 +374,23 @@ class CandidateController extends GetxController { answers: answers, ); - // Ajouter à la liste locale + // Add locally _applications.add(application); - // Recharger les statistiques + // Refresh stats await loadStats(); AppTheme.showStandardSnackBar( - title: 'Succès', - message: 'Candidature envoyée avec succès', + title: 'Success', + message: 'Application sent', isSuccess: true, ); return true; } catch (e) { - _errorMessage.value = 'Erreur lors de la candidature: $e'; + _errorMessage.value = 'Application failed: $e'; AppTheme.showStandardSnackBar( - title: 'Erreur', + title: 'Error', message: _errorMessage.value, isError: true, ); @@ -402,7 +401,7 @@ class CandidateController extends GetxController { } } - // Retirer une candidature + // Withdraw application Future withdrawApplication(String applicationId) async { try { _isLoading.value = true; @@ -410,7 +409,7 @@ class CandidateController extends GetxController { await CandidateApiService.withdrawApplication(applicationId); - // Mettre à jour le statut dans la liste locale + // Update local status final index = _applications.indexWhere((app) => app.id == applicationId); if (index != -1) { _applications[index] = @@ -419,16 +418,16 @@ class CandidateController extends GetxController { } AppTheme.showStandardSnackBar( - title: 'Succès', - message: 'Candidature retirée', + title: 'Success', + message: 'Application withdrawn', isSuccess: true, ); return true; } catch (e) { - _errorMessage.value = 'Erreur lors du retrait: $e'; + _errorMessage.value = 'Withdraw failed: $e'; AppTheme.showStandardSnackBar( - title: 'Erreur', + title: 'Error', message: _errorMessage.value, isError: true, ); @@ -441,15 +440,15 @@ class CandidateController extends GetxController { } } - // Vérifier si l'utilisateur a déjà postulé à une annonce + // Check if job already applied bool hasAppliedToJob(String jobId) { return _applications.any((app) => app.jobId == jobId && app.status != ApplicationStatus.withdrawn); } - // === STATISTIQUES === + // --- Stats --- - // Charger les statistiques du candidat + // Load stats Future loadStats() async { try { final statistics = await CandidateApiService.getCandidateStats(); @@ -459,26 +458,26 @@ class CandidateController extends GetxController { } } - // === MÉTHODES UTILITAIRES === + // --- Utils --- - // Vérifier si le profil est complet + // Profile completion status bool get isProfileComplete => _candidateProfile.value?.isComplete ?? false; - // Obtenir le score de complétion du profil + // Profile completion score int get profileCompletionScore => _candidateProfile.value?.profileCompletionScore ?? 0; - // Obtenir le nombre de candidatures par statut + // Count applications by status int getApplicationCountByStatus(ApplicationStatus status) { return _applications.where((app) => app.status == status).length; } - // Rafraîchir toutes les données + // Reload all data Future refreshAll() async { await _initializeData(); } - // Nettoyer les données + // Clear local data void clearData() { _candidateProfile.value = null; _cvs.clear(); @@ -494,8 +493,10 @@ class CandidateController extends GetxController { } } -// Extensions pour faciliter l'utilisation +// Application helpers extension ApplicationModelExtension on ApplicationModel { + + // Copy with updates ApplicationModel copyWith({ String? id, String? jobId, diff --git a/lib/controllers/employer_applications_controller.dart b/lib/controllers/employer_applications_controller.dart index 45af5d32..839a31c9 100644 --- a/lib/controllers/employer_applications_controller.dart +++ b/lib/controllers/employer_applications_controller.dart @@ -1,3 +1,4 @@ +// Employer applications controller import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:timeless/models/application_model.dart'; @@ -5,15 +6,31 @@ import 'package:timeless/models/job_offer_model.dart'; import 'package:timeless/services/job_service.dart'; import 'package:timeless/services/preferences_service.dart'; +// Employer applications controller class EmployerApplicationsController extends GetxController { + + // All applications final applications = [].obs; + + // Filtered applications final filteredApplications = [].obs; + + // Employer job offers final employerJobs = [].obs; + + // Loading state final isLoading = false.obs; + + // Selected job filter final selectedJob = Rx(null); + + // Selected status filters final selectedStatuses = [].obs; + + // Sort option final sortBy = 'date_desc'.obs; + // Employer ID from preferences String get employerId => PreferencesService.getString('userId'); @override @@ -22,21 +39,22 @@ class EmployerApplicationsController extends GetxController { loadEmployerData(); } + // Load jobs and applications Future loadEmployerData() async { try { isLoading.value = true; - + if (employerId.isEmpty) { - throw Exception('Employeur non connecté'); + throw Exception('Employer not logged in'); } await loadEmployerJobs(); await loadAllApplications(); - + } catch (e) { Get.snackbar( - 'Erreur', - 'Impossible de charger les données: $e', + 'Error', + 'Failed to load data: $e', backgroundColor: Colors.red, colorText: Colors.white, ); @@ -45,28 +63,33 @@ class EmployerApplicationsController extends GetxController { } } + // Load employer jobs Future loadEmployerJobs() async { List jobs = await JobService.getEmployerJobs(employerId); employerJobs.value = jobs; } + // Load all applications for all jobs Future loadAllApplications() async { List allApps = []; - + for (JobOfferModel job in employerJobs) { - List jobApps = await JobService.getJobApplications(job.id); + List jobApps = + await JobService.getJobApplications(job.id); allApps.addAll(jobApps); } - + applications.value = allApps; _applyFilters(); } + // Select job filter void selectJob(JobOfferModel? job) { selectedJob.value = job; _applyFilters(); } + // Add status filter void addStatusFilter(ApplicationStatus status) { if (!selectedStatuses.contains(status)) { selectedStatuses.add(status); @@ -74,36 +97,44 @@ class EmployerApplicationsController extends GetxController { } } + // Remove status filter void removeStatusFilter(ApplicationStatus status) { selectedStatuses.remove(status); _applyFilters(); } + // Reset all filters void clearFilters() { selectedStatuses.clear(); selectedJob.value = null; _applyFilters(); } + // Change sort option void changeSortOrder(String newSortBy) { sortBy.value = newSortBy; _applyFilters(); } + // Apply filters and sorting void _applyFilters() { List filtered = List.from(applications); - + // Filter by job if (selectedJob.value != null) { - filtered = filtered.where((app) => app.jobId == selectedJob.value!.id).toList(); + filtered = filtered + .where((app) => app.jobId == selectedJob.value!.id) + .toList(); } - + // Filter by status if (selectedStatuses.isNotEmpty) { - filtered = filtered.where((app) => selectedStatuses.contains(app.status)).toList(); + filtered = filtered + .where((app) => selectedStatuses.contains(app.status)) + .toList(); } - - // Sort + + // Sort results switch (sortBy.value) { case 'date_desc': filtered.sort((a, b) => b.appliedAt.compareTo(a.appliedAt)); @@ -118,74 +149,105 @@ class EmployerApplicationsController extends GetxController { filtered.sort((a, b) => a.status.index.compareTo(b.status.index)); break; } - + filteredApplications.value = filtered; } - Future updateApplicationStatus(String applicationId, ApplicationStatus newStatus) async { + // Update application status + Future updateApplicationStatus( + String applicationId, + ApplicationStatus newStatus, + ) async { try { await JobService.updateApplicationStatus(applicationId, newStatus); - - // Update local data - int index = applications.indexWhere((app) => app.id == applicationId); + + // Update local list + int index = + applications.indexWhere((app) => app.id == applicationId); if (index != -1) { - applications[index] = applications[index].copyWith(status: newStatus); + applications[index] = + applications[index].copyWith(status: newStatus); _applyFilters(); } - + Get.snackbar( - 'Statut mis à jour', - 'Le statut de la candidature a été modifié', + 'Status updated', + 'Application status changed', backgroundColor: Colors.green, colorText: Colors.white, ); - + } catch (e) { Get.snackbar( - 'Erreur', - 'Impossible de modifier le statut', + 'Error', + 'Failed to update status', backgroundColor: Colors.red, colorText: Colors.white, ); } } - // Statistics + // --- Stats --- + + // Total applications int get totalApplications => applications.length; - int get pendingApplications => applications.where((app) => app.status == ApplicationStatus.pending).length; - int get viewedApplications => applications.where((app) => app.status == ApplicationStatus.viewed).length; - int get shortlistedApplications => applications.where((app) => app.status == ApplicationStatus.shortlisted).length; - int get interviewApplications => applications.where((app) => app.status == ApplicationStatus.interview).length; - int get hiredApplications => applications.where((app) => app.status == ApplicationStatus.hired).length; - int get rejectedApplications => applications.where((app) => app.status == ApplicationStatus.rejected).length; + // By status + int get pendingApplications => + applications.where((app) => app.status == ApplicationStatus.pending).length; + + int get viewedApplications => + applications.where((app) => app.status == ApplicationStatus.viewed).length; + + int get shortlistedApplications => + applications.where((app) => app.status == ApplicationStatus.shortlisted).length; + + int get interviewApplications => + applications.where((app) => app.status == ApplicationStatus.interview).length; + + int get hiredApplications => + applications.where((app) => app.status == ApplicationStatus.hired).length; + + int get rejectedApplications => + applications.where((app) => app.status == ApplicationStatus.rejected).length; + + // Applications from today List get todayApplications { final today = DateTime.now(); final startOfDay = DateTime(today.year, today.month, today.day); final endOfDay = startOfDay.add(const Duration(days: 1)); - - return applications.where((app) => - app.appliedAt.isAfter(startOfDay) && - app.appliedAt.isBefore(endOfDay) + + return applications.where((app) => + app.appliedAt.isAfter(startOfDay) && + app.appliedAt.isBefore(endOfDay) ).toList(); } + // Applications from this week List get thisWeekApplications { final now = DateTime.now(); - final startOfWeek = now.subtract(Duration(days: now.weekday - 1)); - final startOfWeekDay = DateTime(startOfWeek.year, startOfWeek.month, startOfWeek.day); - - return applications.where((app) => app.appliedAt.isAfter(startOfWeekDay)).toList(); + final startOfWeek = + now.subtract(Duration(days: now.weekday - 1)); + final startOfWeekDay = + DateTime(startOfWeek.year, startOfWeek.month, startOfWeek.day); + + return applications + .where((app) => app.appliedAt.isAfter(startOfWeekDay)) + .toList(); } + // Get job linked to application JobOfferModel? getJobForApplication(ApplicationModel application) { - return employerJobs.firstWhereOrNull((job) => job.id == application.jobId); + return employerJobs + .firstWhereOrNull((job) => job.id == application.jobId); } + // Reload all data Future refreshData() async { await loadEmployerData(); } + // Status label String getStatusLabel(ApplicationStatus status) { switch (status) { case ApplicationStatus.pending: @@ -207,6 +269,7 @@ class EmployerApplicationsController extends GetxController { } } + // Status color Color getStatusColor(ApplicationStatus status) { switch (status) { case ApplicationStatus.pending: @@ -227,4 +290,4 @@ class EmployerApplicationsController extends GetxController { return Colors.green; } } -} \ No newline at end of file +} diff --git a/lib/controllers/employer_profile_controller.dart b/lib/controllers/employer_profile_controller.dart index 7ca2b015..898fb589 100644 --- a/lib/controllers/employer_profile_controller.dart +++ b/lib/controllers/employer_profile_controller.dart @@ -1,3 +1,4 @@ +// Employer profile controller import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -11,14 +12,14 @@ import 'package:timeless/utils/pref_keys.dart'; import 'package:timeless/services/employer_validation_service.dart'; class EmployerProfileController extends GetxController { - // Firebase instances + // Firebase services final FirebaseAuth _auth = FirebaseAuth.instance; final FirebaseFirestore _firestore = FirebaseFirestore.instance; - // Loading state + // Loading flag final RxBool isLoading = false.obs; - // Profile data - Observable pour reactivity + // Profile fields (reactive) final RxString companyName = ''.obs; final RxString email = ''.obs; final RxString phoneNumber = ''.obs; @@ -36,7 +37,7 @@ class EmployerProfileController extends GetxController { final RxString profileImageUrl = ''.obs; final RxBool isVerified = false.obs; - // Controllers pour l'édition + // Text controllers final companyNameController = TextEditingController(); final emailController = TextEditingController(); final phoneController = TextEditingController(); @@ -52,7 +53,7 @@ class EmployerProfileController extends GetxController { final sectorController = TextEditingController(); final employeeCountController = TextEditingController(); - // Image management + // Selected image File? image; @override @@ -61,85 +62,70 @@ class EmployerProfileController extends GetxController { loadEmployerProfileFromFirebase(); _setupRealTimeListeners(); } - - // Configurer les listeners pour la mise à jour en temps réel + + // Sync text fields with reactive values void _setupRealTimeListeners() { companyNameController.addListener(() { companyName.value = companyNameController.text.trim(); }); - emailController.addListener(() { email.value = emailController.text.trim(); }); - phoneController.addListener(() { phoneNumber.value = phoneController.text.trim(); }); - websiteController.addListener(() { website.value = websiteController.text.trim(); }); - locationController.addListener(() { location.value = locationController.text.trim(); }); - addressController.addListener(() { address.value = addressController.text.trim(); }); - postalCodeController.addListener(() { postalCode.value = postalCodeController.text.trim(); }); - countryController.addListener(() { country.value = countryController.text.trim(); }); - contactPersonController.addListener(() { contactPerson.value = contactPersonController.text.trim(); }); - siretController.addListener(() { siretCode.value = siretController.text.trim(); }); - apeController.addListener(() { apeCode.value = apeController.text.trim(); }); - descriptionController.addListener(() { description.value = descriptionController.text.trim(); }); - sectorController.addListener(() { sector.value = sectorController.text.trim(); }); - employeeCountController.addListener(() { employeeCount.value = employeeCountController.text.trim(); }); } - // Charger le profil employeur depuis Firebase + // Load employer profile from Firebase Future loadEmployerProfileFromFirebase() async { try { isLoading.value = true; final user = _auth.currentUser; - + if (user != null) { - // Charger depuis la collection Auth/Manager/register final doc = await _firestore .collection('Auth') .doc('Manager') .collection('register') .doc(user.uid) .get(); - + if (doc.exists) { final data = doc.data()!; - - // Charger toutes les données du profil employeur + companyName.value = data['companyName'] ?? ''; email.value = data['Email'] ?? user.email ?? ''; phoneNumber.value = data['Phone'] ?? ''; @@ -156,8 +142,8 @@ class EmployerProfileController extends GetxController { employeeCount.value = data['employeeCount'] ?? ''; profileImageUrl.value = data['photoURL'] ?? user.photoURL ?? ''; isVerified.value = data['isVerified'] ?? false; - - // Remplir aussi les contrôleurs pour l'édition + + // Fill form fields companyNameController.text = companyName.value; emailController.text = email.value; phoneController.text = phoneNumber.value; @@ -172,28 +158,21 @@ class EmployerProfileController extends GetxController { descriptionController.text = description.value; sectorController.text = sector.value; employeeCountController.text = employeeCount.value; - - // Charger aussi les données de l'entreprise si elles existent + await _loadCompanyData(user.uid); - - print('✅ Profil employeur chargé depuis Firebase'); } else { - // Si pas de document, utiliser les données de Firebase Auth companyName.value = user.displayName ?? ''; email.value = user.email ?? ''; profileImageUrl.value = user.photoURL ?? ''; - + companyNameController.text = companyName.value; emailController.text = email.value; - - print('❓ Aucun document profil employeur trouvé, utilisation des données Firebase Auth'); } } } catch (e) { - print('❌ Erreur lors du chargement du profil employeur: $e'); AppTheme.showStandardSnackBar( - title: "Erreur", - message: "Impossible de charger le profil employeur", + title: "Error", + message: "Failed to load employer profile", isError: true, ); } finally { @@ -201,7 +180,7 @@ class EmployerProfileController extends GetxController { } } - // Charger les données spécifiques de l'entreprise + // Load extra company data Future _loadCompanyData(String userId) async { try { final companyDocs = await _firestore @@ -214,104 +193,80 @@ class EmployerProfileController extends GetxController { if (companyDocs.docs.isNotEmpty) { final companyData = companyDocs.docs.first.data(); - - // Mettre à jour les champs spécifiques de l'entreprise + if (companyData['name'] != null && companyName.value.isEmpty) { companyName.value = companyData['name']; companyNameController.text = companyName.value; } - if (companyData['website'] != null && website.value.isEmpty) { website.value = companyData['website']; websiteController.text = website.value; } - if (companyData['location'] != null && location.value.isEmpty) { location.value = companyData['location']; locationController.text = location.value; } - if (companyData['description'] != null && description.value.isEmpty) { description.value = companyData['description']; descriptionController.text = description.value; } - - print('✅ Données entreprise chargées depuis Firebase'); } - } catch (e) { - print('❌ Erreur lors du chargement des données entreprise: $e'); - } + } catch (_) {} } - // Rafraîchir le profil + // Refresh profile Future refreshProfile() async { await loadEmployerProfileFromFirebase(); } - // Getters pour compatibilité avec l'écran - String get displayCompanyName => companyName.value.isNotEmpty ? companyName.value : 'Votre Entreprise'; - String get displayEmail => email.value; - String get displayPhone => phoneNumber.value; - String get displayWebsite => website.value; - String get displayLocation => location.value; - String get displayAddress => address.value; - String get displayPostalCode => postalCode.value; - String get displayCountry => country.value; - String get displayContactPerson => contactPerson.value; - String get displaySiretCode => siretCode.value; - String get displayApeCode => apeCode.value; - String get displayDescription => description.value; - String get displaySector => sector.value; - String get displayEmployeeCount => employeeCount.value; - - // Méthode pour obtenir les initiales de l'entreprise + // Get company initials String getInitials() { if (companyName.value.isEmpty) return 'E'; - final words = companyName.value.split(' '); if (words.length == 1) return words[0][0].toUpperCase(); - return '${words[0][0]}${words[words.length - 1][0]}'.toUpperCase(); + return '${words.first[0]}${words.last[0]}'.toUpperCase(); } - // Vérifier si une image de profil existe + // Check profile image bool hasProfileImage() { return profileImageUrl.value.isNotEmpty; } - // Méthode pour valider le SIRET + // Validate SIRET and auto-fill company data Future validateSiretCode() async { if (siretCode.value.isEmpty) return false; - + try { - final companyInfo = await EmployerValidationService.validateSiret(siretCode.value); + final companyInfo = + await EmployerValidationService.validateSiret(siretCode.value); + if (companyInfo != null) { - // Mettre à jour automatiquement les informations de l'entreprise - companyName.value = companyInfo['denomination'] ?? companyName.value; - apeCode.value = companyInfo['activitePrincipaleUniteLegale'] ?? apeCode.value; + companyName.value = + companyInfo['denomination'] ?? companyName.value; + apeCode.value = + companyInfo['activitePrincipaleUniteLegale'] ?? apeCode.value; sector.value = companyInfo['secteur'] ?? sector.value; address.value = companyInfo['adresse'] ?? address.value; - - // Mettre à jour les contrôleurs + companyNameController.text = companyName.value; apeController.text = apeCode.value; sectorController.text = sector.value; addressController.text = address.value; - + isVerified.value = true; - + AppTheme.showStandardSnackBar( - title: "SIRET validé", - message: "Les informations de votre entreprise ont été mises à jour automatiquement", + title: "SIRET verified", + message: "Company data updated automatically", isSuccess: true, ); - return true; } return false; } catch (e) { AppTheme.showStandardSnackBar( - title: "SIRET invalide", - message: "Impossible de valider le SIRET: $e", + title: "Invalid SIRET", + message: "SIRET validation failed", isError: true, ); isVerified.value = false; @@ -319,303 +274,228 @@ class EmployerProfileController extends GetxController { } } - // Méthode pour forcer la mise à jour de l'UI - void forceUpdate() { - update(); - } - - // Méthodes pour l'édition d'image + // Pick image from camera Future onTapImage() async => await _pickFromCamera(); + + // Pick image from gallery Future onTapGallery1() async => await _pickFromGallery(); + // Save profile + Future onTapSubmit() async { + try { + isLoading.value = true; + update(['save_button']); + + String? imageUrl; + if (image != null) { + imageUrl = await _uploadImageToFirebase(); + if (imageUrl != null) { + profileImageUrl.value = imageUrl; + } + } + + await _saveToFirebase(imageUrl); + _updateLocalValues(); + + AppTheme.showStandardSnackBar( + title: "Profile updated", + message: "Employer profile saved successfully", + isSuccess: true, + ); + } catch (_) { + AppTheme.showStandardSnackBar( + title: "Error", + message: "Failed to save employer profile", + isError: true, + ); + } finally { + isLoading.value = false; + update(['save_button']); + } + } + + // Clear employer profile data + Future clearEmployerProfileData() async { + try { + // Clear reactive values + companyName.value = ''; + email.value = ''; + phoneNumber.value = ''; + website.value = ''; + location.value = ''; + address.value = ''; + postalCode.value = ''; + country.value = ''; + contactPerson.value = ''; + siretCode.value = ''; + apeCode.value = ''; + description.value = ''; + sector.value = ''; + employeeCount.value = ''; + profileImageUrl.value = ''; + isVerified.value = false; + + // Clear text controllers + companyNameController.clear(); + emailController.clear(); + phoneController.clear(); + websiteController.clear(); + locationController.clear(); + addressController.clear(); + postalCodeController.clear(); + countryController.clear(); + contactPersonController.clear(); + siretController.clear(); + apeController.clear(); + descriptionController.clear(); + sectorController.clear(); + employeeCountController.clear(); + + // Clear selected image + image = null; + } catch (e) { + AppTheme.showStandardSnackBar( + title: "Error", + message: "Failed to clear profile data", + isError: true, + ); + } + } + + // Pick image from camera Future _pickFromCamera() async { try { - final ImagePicker picker = ImagePicker(); - final XFile? pickedFile = await picker.pickImage( + final picker = ImagePicker(); + final pickedFile = await picker.pickImage( source: ImageSource.camera, - maxWidth: 1024, - maxHeight: 1024, + maxWidth: 800, + maxHeight: 800, imageQuality: 85, ); if (pickedFile != null) { image = File(pickedFile.path); - update(['image']); - AppTheme.showStandardSnackBar( - title: "Succès", - message: "Photo capturée avec succès", - isSuccess: true, - ); + update(['image_picker']); } } catch (e) { AppTheme.showStandardSnackBar( - title: "Erreur", - message: "Problème avec la caméra", + title: "Error", + message: "Failed to pick image from camera", isError: true, ); } } + // Pick image from gallery Future _pickFromGallery() async { try { - final ImagePicker picker = ImagePicker(); - final XFile? pickedFile = await picker.pickImage( + final picker = ImagePicker(); + final pickedFile = await picker.pickImage( source: ImageSource.gallery, - maxWidth: 1024, - maxHeight: 1024, + maxWidth: 800, + maxHeight: 800, imageQuality: 85, ); if (pickedFile != null) { image = File(pickedFile.path); - update(['image']); - AppTheme.showStandardSnackBar( - title: "Succès", - message: "Photo sélectionnée avec succès", - isSuccess: true, - ); + update(['image_picker']); } } catch (e) { AppTheme.showStandardSnackBar( - title: "Erreur", - message: "Problème avec la galerie", + title: "Error", + message: "Failed to pick image from gallery", isError: true, ); } } - // Méthode de sauvegarde - Future onTapSubmit() async { + // Upload image to Firebase Storage + Future _uploadImageToFirebase() async { try { - isLoading.value = true; - update(['save_button']); + if (image == null) return null; - // Upload image if needed - String? imageUrl; - if (image != null) { - imageUrl = await _uploadImageToFirebase(); - if (imageUrl != null) { - profileImageUrl.value = imageUrl; - } - } - - // Save to Firebase - await _saveToFirebase(imageUrl); + final user = _auth.currentUser; + if (user == null) return null; - // Update local values - _updateLocalValues(); + final fileName = 'employer_profile_${user.uid}_${DateTime.now().millisecondsSinceEpoch}.jpg'; + final storageRef = FirebaseStorage.instance + .ref() + .child('employer_profiles') + .child(fileName); - AppTheme.showStandardSnackBar( - title: "Profil mis à jour", - message: "Votre profil employeur a été sauvegardé avec succès !", - isSuccess: true, - ); + final uploadTask = storageRef.putFile(image!); + final snapshot = await uploadTask; + return await snapshot.ref.getDownloadURL(); } catch (e) { AppTheme.showStandardSnackBar( - title: "Erreur", - message: "Impossible de sauvegarder le profil employeur", + title: "Error", + message: "Failed to upload image", isError: true, ); - } finally { - isLoading.value = false; - update(['save_button']); + return null; } } - Future _uploadImageToFirebase() async { - if (image == null) return null; - + // Save profile data to Firebase + Future _saveToFirebase(String? imageUrl) async { try { final user = _auth.currentUser; - if (user == null) return null; + if (user == null) throw Exception('User not authenticated'); - final String fileName = '${user.uid}_employer_profile_${DateTime.now().millisecondsSinceEpoch}.jpg'; - final Reference ref = FirebaseStorage.instance - .ref() - .child('employer_profile_images') - .child(fileName); + final profileData = { + 'companyName': companyName.value, + 'Email': email.value, + 'Phone': phoneNumber.value, + 'website': website.value, + 'location': location.value, + 'address': address.value, + 'postalCode': postalCode.value, + 'country': country.value, + 'contactPerson': contactPerson.value, + 'siretCode': siretCode.value, + 'apeCode': apeCode.value, + 'description': description.value, + 'sector': sector.value, + 'employeeCount': employeeCount.value, + 'isVerified': isVerified.value, + 'updatedAt': FieldValue.serverTimestamp(), + }; - final UploadTask uploadTask = ref.putFile(image!); - final TaskSnapshot snapshot = await uploadTask; - return await snapshot.ref.getDownloadURL(); + if (imageUrl != null) { + profileData['photoURL'] = imageUrl; + } + + await _firestore + .collection('Auth') + .doc('Manager') + .collection('register') + .doc(user.uid) + .update(profileData); } catch (e) { - throw Exception('Erreur upload image: $e'); - } - } - - Future _saveToFirebase(String? imageUrl) async { - final user = _auth.currentUser; - if (user == null) return; - - final Map employerData = { - 'uid': user.uid, - 'Email': emailController.text.trim(), - 'companyName': companyNameController.text.trim(), - 'Phone': phoneController.text.trim(), - 'website': websiteController.text.trim(), - 'location': locationController.text.trim(), - 'address': addressController.text.trim(), - 'postalCode': postalCodeController.text.trim(), - 'country': countryController.text.trim(), - 'contactPerson': contactPersonController.text.trim(), - 'siretCode': siretController.text.trim(), - 'apeCode': apeController.text.trim(), - 'description': descriptionController.text.trim(), - 'sector': sectorController.text.trim(), - 'employeeCount': employeeCountController.text.trim(), - 'isVerified': isVerified.value, - 'updatedAt': FieldValue.serverTimestamp(), - 'userType': 'employer', - }; - - if (imageUrl != null) { - employerData['photoURL'] = imageUrl; - } - - // Sauvegarder dans Auth/Manager/register - await _firestore - .collection('Auth') - .doc('Manager') - .collection('register') - .doc(user.uid) - .set(employerData, SetOptions(merge: true)); - - // Sauvegarder aussi dans la sous-collection company pour compatibilité - await _saveCompanyData(user.uid); - - // Mettre à jour Firebase Auth displayName et photoURL pour synchronisation - await user.updateDisplayName(companyNameController.text.trim()); - if (imageUrl != null) { - await user.updatePhotoURL(imageUrl); - } - await user.reload(); - - // Mettre à jour les préférences locales - await _updateLocalPreferences(); - } - - // Sauvegarder les données de l'entreprise dans la sous-collection - Future _saveCompanyData(String userId) async { - final companyData = { - 'name': companyNameController.text.trim(), - 'website': websiteController.text.trim(), - 'location': locationController.text.trim(), - 'description': descriptionController.text.trim(), - 'UpdatedAt': FieldValue.serverTimestamp(), - }; - - final companyRef = _firestore - .collection("Auth") - .doc("Manager") - .collection("register") - .doc(userId) - .collection("company"); - - final existingDocs = await companyRef.get(); - - if (existingDocs.docs.isNotEmpty) { - await companyRef.doc(existingDocs.docs.first.id).update(companyData); - } else { - await companyRef.add({ - ...companyData, - 'CreatedAt': FieldValue.serverTimestamp(), - }); + rethrow; } } + // Update local values after save void _updateLocalValues() { - companyName.value = companyNameController.text.trim(); - email.value = emailController.text.trim(); - phoneNumber.value = phoneController.text.trim(); - website.value = websiteController.text.trim(); - location.value = locationController.text.trim(); - address.value = addressController.text.trim(); - postalCode.value = postalCodeController.text.trim(); - country.value = countryController.text.trim(); - contactPerson.value = contactPersonController.text.trim(); - siretCode.value = siretController.text.trim(); - apeCode.value = apeController.text.trim(); - description.value = descriptionController.text.trim(); - sector.value = sectorController.text.trim(); - employeeCount.value = employeeCountController.text.trim(); - } - - // Mettre à jour les préférences locales - Future _updateLocalPreferences() async { - await PreferencesService.setValue(PrefKeys.companyName, companyNameController.text.trim()); - await PreferencesService.setValue(PrefKeys.email, emailController.text.trim()); - await PreferencesService.setValue(PrefKeys.phoneNumber, phoneController.text.trim()); - await PreferencesService.setValue('website', websiteController.text.trim()); - await PreferencesService.setValue('location', locationController.text.trim()); - await PreferencesService.setValue('address', addressController.text.trim()); - await PreferencesService.setValue('postalCode', postalCodeController.text.trim()); - await PreferencesService.setValue('country', countryController.text.trim()); - await PreferencesService.setValue('contactPerson', contactPersonController.text.trim()); - await PreferencesService.setValue('siretCode', siretController.text.trim()); - await PreferencesService.setValue('apeCode', apeController.text.trim()); - await PreferencesService.setValue('description', descriptionController.text.trim()); - await PreferencesService.setValue('sector', sectorController.text.trim()); - await PreferencesService.setValue('employeeCount', employeeCountController.text.trim()); + // Save to local preferences + PreferencesService.setString( + PrefKeys.companyName, + companyName.value, + ); + PreferencesService.setString( + PrefKeys.email, + email.value, + ); + PreferencesService.setString( + PrefKeys.phone, + phoneNumber.value, + ); - if (profileImageUrl.value.isNotEmpty) { - await PreferencesService.setValue('employerProfileImageUrl', profileImageUrl.value); - } - } - - // Méthode pour vider le profil Firebase - Future clearEmployerProfileData() async { - try { - final user = _auth.currentUser; - if (user != null) { - // Supprimer de la collection Auth/Manager/register - await _firestore - .collection('Auth') - .doc('Manager') - .collection('register') - .doc(user.uid) - .delete(); - - // Clear all values - companyName.value = ''; - email.value = ''; - phoneNumber.value = ''; - website.value = ''; - location.value = ''; - address.value = ''; - postalCode.value = ''; - country.value = ''; - contactPerson.value = ''; - siretCode.value = ''; - apeCode.value = ''; - description.value = ''; - sector.value = ''; - employeeCount.value = ''; - profileImageUrl.value = ''; - isVerified.value = false; - - // Clear controllers - companyNameController.clear(); - emailController.clear(); - phoneController.clear(); - websiteController.clear(); - locationController.clear(); - addressController.clear(); - postalCodeController.clear(); - countryController.clear(); - contactPersonController.clear(); - siretController.clear(); - apeController.clear(); - descriptionController.clear(); - sectorController.clear(); - employeeCountController.clear(); - - print('✅ Profil employeur supprimé de Firebase'); - } - } catch (e) { - print('❌ Erreur lors de la suppression du profil employeur: $e'); - throw e; - } + // Update reactive values to ensure UI consistency + update(); } @override @@ -636,4 +516,4 @@ class EmployerProfileController extends GetxController { employeeCountController.dispose(); super.onClose(); } -} \ No newline at end of file +} diff --git a/lib/main.dart b/lib/main.dart index 27cf1d17..c49d89e3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,7 +11,7 @@ import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'firebase_options.dart'; +import 'package:timeless/utils/firebase_options.dart'; // Candidate screens import 'package:timeless/screen/splashScreen/splash_screen.dart'; @@ -88,8 +88,8 @@ Future main() async { // Register global services with GetX Get.put(UnifiedTranslationService()); // Centralized translation service - Get.put(ThemeService()); // App theme management - Get.put(AccessibilityService()); // Accessibility options + Get.put(ThemeService()); // App theme management + Get.put(AccessibilityService()); // Accessibility options // Start the app with localization support runApp( diff --git a/lib/models/application_model.dart b/lib/models/application_model.dart index 8565f12d..ae685b51 100644 --- a/lib/models/application_model.dart +++ b/lib/models/application_model.dart @@ -1,5 +1,6 @@ import 'package:cloud_firestore/cloud_firestore.dart'; +// All possible states of a job application in the app enum ApplicationStatus { pending, viewed, @@ -11,21 +12,51 @@ enum ApplicationStatus { accepted, } +// Main model used to represent a job application class ApplicationModel { + // Firestore document ID final String id; + + // Related job offer ID final String jobId; + + // ID of the candidate who applied final String candidateId; + + // ID of the employer who owns the job offer final String employerId; + + // Candidate full name final String candidateName; + + // Candidate email address final String candidateEmail; + + // Candidate phone number (optional) final String? candidatePhone; + + // URL of the uploaded CV file final String cvUrl; + + // Original CV file name final String cvFileName; + + // Optional cover letter text final String? coverLetter; + + // Current status of the application final ApplicationStatus status; + + // Date when the application was submitted final DateTime appliedAt; + + // Date when the application was reviewed by the employer final DateTime? reviewedAt; + + // Optional notes written by the employer final String? reviewNotes; + + // Optional extra data about the candidate profile final Map? candidateProfile; ApplicationModel({ @@ -46,6 +77,7 @@ class ApplicationModel { this.candidateProfile, }); + // Build an ApplicationModel from a Firestore document factory ApplicationModel.fromFirestore(DocumentSnapshot doc) { Map data = doc.data() as Map; return ApplicationModel( @@ -61,12 +93,15 @@ class ApplicationModel { coverLetter: data['coverLetter'], status: _parseApplicationStatus(data['status']), appliedAt: (data['appliedAt'] as Timestamp).toDate(), - reviewedAt: data['reviewedAt'] != null ? (data['reviewedAt'] as Timestamp).toDate() : null, + reviewedAt: data['reviewedAt'] != null + ? (data['reviewedAt'] as Timestamp).toDate() + : null, reviewNotes: data['reviewNotes'], candidateProfile: data['candidateProfile'], ); } + // Build an ApplicationModel from a JSON object (API or local usage) factory ApplicationModel.fromJson(Map json) { return ApplicationModel( id: json['id'] ?? '', @@ -80,19 +115,22 @@ class ApplicationModel { cvFileName: json['cvFileName'] ?? '', coverLetter: json['coverLetter'], status: _parseApplicationStatus(json['status']), - appliedAt: json['appliedAt'] is Timestamp + appliedAt: json['appliedAt'] is Timestamp ? (json['appliedAt'] as Timestamp).toDate() : DateTime.tryParse(json['appliedAt'] ?? '') ?? DateTime.now(), - reviewedAt: json['reviewedAt'] is Timestamp + reviewedAt: json['reviewedAt'] is Timestamp ? (json['reviewedAt'] as Timestamp).toDate() - : json['reviewedAt'] != null ? DateTime.tryParse(json['reviewedAt']) : null, + : json['reviewedAt'] != null + ? DateTime.tryParse(json['reviewedAt']) + : null, reviewNotes: json['reviewNotes'], - candidateProfile: json['candidateProfile'] is Map + candidateProfile: json['candidateProfile'] is Map ? json['candidateProfile'] as Map : null, ); } + // Convert the model into a Firestore-friendly map Map toFirestore() { return { 'jobId': jobId, @@ -112,6 +150,7 @@ class ApplicationModel { }; } + // Convert the model into JSON format Map toJson() { return { 'id': id, @@ -132,6 +171,7 @@ class ApplicationModel { }; } + // Convert a string value into an ApplicationStatus enum static ApplicationStatus _parseApplicationStatus(String? status) { switch (status) { case 'pending': @@ -155,31 +195,34 @@ class ApplicationModel { } } + // Convert an enum status to a string static String _statusToString(ApplicationStatus status) { return status.toString().split('.').last; } + // Human-readable status label (used in the UI) String get statusDisplay { switch (status) { case ApplicationStatus.pending: - return 'En attente'; + return 'Pending'; case ApplicationStatus.viewed: - return 'Vue'; + return 'Viewed'; case ApplicationStatus.shortlisted: - return 'Présélectionnée'; + return 'Shortlisted'; case ApplicationStatus.interview: - return 'Entretien'; + return 'Interview'; case ApplicationStatus.rejected: - return 'Refusée'; + return 'Rejected'; case ApplicationStatus.hired: - return 'Embauchée'; + return 'Hired'; case ApplicationStatus.withdrawn: - return 'Retirée'; + return 'Withdrawn'; case ApplicationStatus.accepted: - return 'Acceptée'; + return 'Accepted'; } } + // Helper getters for cleaner UI conditions bool get isPending => status == ApplicationStatus.pending; bool get isViewed => status == ApplicationStatus.viewed; bool get isShortlisted => status == ApplicationStatus.shortlisted; @@ -187,6 +230,7 @@ class ApplicationModel { bool get isRejected => status == ApplicationStatus.rejected; bool get isHired => status == ApplicationStatus.hired; + // Create a copy of the application with updated values ApplicationModel copyWith({ String? id, String? jobId, @@ -222,4 +266,4 @@ class ApplicationModel { candidateProfile: candidateProfile ?? this.candidateProfile, ); } -} \ No newline at end of file +} diff --git a/lib/models/candidate_profile_model.dart b/lib/models/candidate_profile_model.dart index a9f7642e..de809612 100644 --- a/lib/models/candidate_profile_model.dart +++ b/lib/models/candidate_profile_model.dart @@ -1,24 +1,35 @@ -// Modèle de profil candidat complet +// This file contains all data models related to a candidate profile in the app. import 'package:cloud_firestore/cloud_firestore.dart'; class CandidateProfileModel { + // Main identifiers final String id; final String email; final String fullName; + + // Contact and personal info final String? phone; final String? location; final String? photoURL; final String? bio; + + // Professional data final List skills; final List experience; final List education; final List languages; + + // External links final String? portfolioUrl; final String? linkedinUrl; final String? githubUrl; final String? websiteUrl; + + // CV and profile state final String? currentCVId; final ProfileStatus status; + + // Metadata final DateTime createdAt; final DateTime updatedAt; final Map preferences; @@ -46,7 +57,7 @@ class CandidateProfileModel { this.preferences = const {}, }); - // Conversion depuis Firestore + // Create a profile model from Firestore data factory CandidateProfileModel.fromJson(Map json) { return CandidateProfileModel( id: json['id'] ?? '', @@ -58,11 +69,13 @@ class CandidateProfileModel { bio: json['bio'], skills: List.from(json['skills'] ?? []), experience: (json['experience'] as List?) - ?.map((e) => WorkExperience.fromJson(e)) - .toList() ?? [], + ?.map((e) => WorkExperience.fromJson(e)) + .toList() ?? + [], education: (json['education'] as List?) - ?.map((e) => Education.fromJson(e)) - .toList() ?? [], + ?.map((e) => Education.fromJson(e)) + .toList() ?? + [], languages: List.from(json['languages'] ?? []), portfolioUrl: json['portfolioUrl'], linkedinUrl: json['linkedinUrl'], @@ -79,7 +92,7 @@ class CandidateProfileModel { ); } - // Conversion vers Firestore + // Convert the profile model to Firestore format Map toJson() { return { 'id': id, @@ -105,7 +118,7 @@ class CandidateProfileModel { }; } - // Copie avec modifications + // Create a copy of the profile with updated fields CandidateProfileModel copyWith({ String? id, String? email, @@ -152,44 +165,41 @@ class CandidateProfileModel { ); } - // Validation du profil + // Check if the profile is considered complete bool get isComplete { return fullName.isNotEmpty && - email.isNotEmpty && - bio != null && - bio!.isNotEmpty && - skills.isNotEmpty && - currentCVId != null; + email.isNotEmpty && + bio != null && + bio!.isNotEmpty && + skills.isNotEmpty && + currentCVId != null; } - // Calcul du score de profil (0-100) + // Compute a simple completion score (0 to 100) int get profileCompletionScore { int score = 0; - - // Informations de base (40 points) + if (fullName.isNotEmpty) score += 10; if (email.isNotEmpty) score += 10; if (phone?.isNotEmpty == true) score += 5; if (location?.isNotEmpty == true) score += 5; if (bio?.isNotEmpty == true) score += 10; - - // Compétences et expérience (40 points) + if (skills.isNotEmpty) score += 15; if (experience.isNotEmpty) score += 15; if (education.isNotEmpty) score += 10; - - // CV et liens (20 points) + if (currentCVId != null) score += 10; if (linkedinUrl?.isNotEmpty == true) score += 3; if (githubUrl?.isNotEmpty == true) score += 3; if (portfolioUrl?.isNotEmpty == true) score += 2; if (websiteUrl?.isNotEmpty == true) score += 2; - + return score.clamp(0, 100); } } -// Modèle d'expérience professionnelle +// Describes one professional experience class WorkExperience { final String id; final String company; @@ -238,7 +248,7 @@ class WorkExperience { } } -// Modèle d'éducation +// Describes an education record class Education { final String id; final String institution; @@ -287,15 +297,10 @@ class Education { } } -// Statut du profil -enum ProfileStatus { - incomplete, - complete, - verified, - suspended -} +// Current status of a candidate profile +enum ProfileStatus { incomplete, complete, verified, suspended } -// Modèle de CV +// Represents a CV uploaded by a candidate class CVModel { final String id; final String candidateId; @@ -327,7 +332,8 @@ class CVModel { downloadUrl: json['downloadUrl'] ?? '', fileSize: json['fileSize'] ?? 0, contentType: json['contentType'] ?? '', - uploadedAt: (json['uploadedAt'] as Timestamp?)?.toDate() ?? DateTime.now(), + uploadedAt: + (json['uploadedAt'] as Timestamp?)?.toDate() ?? DateTime.now(), extractedText: json['extractedText'], metadata: Map.from(json['metadata'] ?? {}), ); @@ -347,4 +353,3 @@ class CVModel { }; } } - diff --git a/lib/models/employer_model.dart b/lib/models/employer_model.dart index e9ff3ec5..226077fe 100644 --- a/lib/models/employer_model.dart +++ b/lib/models/employer_model.dart @@ -1,3 +1,4 @@ +// This file defines the employer profile model used to represent companies on the platform. import 'package:cloud_firestore/cloud_firestore.dart'; class EmployerModel { @@ -29,6 +30,7 @@ class EmployerModel { this.isVerified = false, }); + // Build an employer object from a Firestore document factory EmployerModel.fromFirestore(DocumentSnapshot doc) { Map data = doc.data() as Map; return EmployerModel( @@ -47,6 +49,7 @@ class EmployerModel { ); } + // Convert employer data to Firestore format Map toFirestore() { return { 'email': email, @@ -64,6 +67,7 @@ class EmployerModel { }; } + // Create a new employer instance with updated values EmployerModel copyWith({ String? id, String? email, @@ -93,4 +97,4 @@ class EmployerModel { isVerified: isVerified ?? this.isVerified, ); } -} \ No newline at end of file +} diff --git a/lib/models/job_offer_model.dart b/lib/models/job_offer_model.dart index f7accb80..8e6a5af6 100644 --- a/lib/models/job_offer_model.dart +++ b/lib/models/job_offer_model.dart @@ -1,3 +1,4 @@ +// This file defines the job offer model used to store and display job postings in the application. import 'package:cloud_firestore/cloud_firestore.dart'; enum JobType { @@ -56,10 +57,11 @@ class JobOfferModel { this.applicationsCount = 0, }); + // Create a job offer from a Firestore document factory JobOfferModel.fromFirestore(DocumentSnapshot doc) { Map data = doc.data() as Map; - - // Parse salary from string format "min-max" + + // Parse salary stored as a "min-max" string double? salaryMinParsed; double? salaryMaxParsed; String? salaryStr = data['salary']; @@ -70,28 +72,35 @@ class JobOfferModel { salaryMaxParsed = double.tryParse(salaryParts[1].trim()); } } - + return JobOfferModel( id: doc.id, employerId: data['EmployerId'] ?? '', companyName: data['CompanyName'] ?? '', title: data['Position'] ?? '', description: data['description'] ?? '', - requirements: [], // Not stored in current structure + requirements: [], // Not used in current Firestore structure location: data['location'] ?? '', jobType: _parseJobTypeFromString(data['jobType']), - experienceLevel: ExperienceLevel.mid, // Default for now - salaryMin: salaryMinParsed ?? double.tryParse(data['salaryMin']?.toString() ?? '0'), - salaryMax: salaryMaxParsed ?? double.tryParse(data['salaryMax']?.toString() ?? '0'), - skills: [], // Not stored in current structure + experienceLevel: ExperienceLevel.mid, // Temporary default value + salaryMin: salaryMinParsed ?? + double.tryParse(data['salaryMin']?.toString() ?? '0'), + salaryMax: salaryMaxParsed ?? + double.tryParse(data['salaryMax']?.toString() ?? '0'), + skills: [], // Not used in current Firestore structure industry: data['category'] ?? '', - createdAt: data['createdAt'] != null ? (data['createdAt'] as Timestamp).toDate() : DateTime.now(), - deadline: data['deadline'] != null ? (data['deadline'] as Timestamp).toDate() : null, + createdAt: data['createdAt'] != null + ? (data['createdAt'] as Timestamp).toDate() + : DateTime.now(), + deadline: data['deadline'] != null + ? (data['deadline'] as Timestamp).toDate() + : null, isActive: data['isActive'] ?? true, applicationsCount: data['applicationsCount'] ?? 0, ); } + // Convert the job offer to Firestore format Map toFirestore() { return { 'employerId': employerId, @@ -113,8 +122,7 @@ class JobOfferModel { }; } - // Helper methods for enum conversion - + // Helpers to convert enums static JobType _parseJobTypeFromString(String? type) { switch (type?.toLowerCase()) { case 'full-time': @@ -142,51 +150,54 @@ class JobOfferModel { return level.toString().split('.').last; } - // Getters for display + // Readable job type for the UI String get jobTypeDisplay { switch (jobType) { case JobType.fullTime: - return 'Temps plein'; + return 'Full-time'; case JobType.partTime: - return 'Temps partiel'; + return 'Part-time'; case JobType.contract: - return 'Contrat'; + return 'Contract'; case JobType.internship: - return 'Stage'; + return 'Internship'; case JobType.freelance: return 'Freelance'; } } + // Readable experience level for the UI String get experienceLevelDisplay { switch (experienceLevel) { case ExperienceLevel.entry: - return 'Débutant'; + return 'Entry level'; case ExperienceLevel.junior: return 'Junior'; case ExperienceLevel.mid: - return 'Confirmé'; + return 'Mid-level'; case ExperienceLevel.senior: return 'Senior'; case ExperienceLevel.lead: return 'Lead'; case ExperienceLevel.executive: - return 'Direction'; + return 'Executive'; } } + // Salary formatted for display String get salaryDisplay { if (salaryMin == null && salaryMax == null) { - return 'Salaire non spécifié'; + return 'Salary not specified'; } else if (salaryMin != null && salaryMax != null) { return '${salaryMin!.toStringAsFixed(0)}€ - ${salaryMax!.toStringAsFixed(0)}€'; } else if (salaryMin != null) { - return 'À partir de ${salaryMin!.toStringAsFixed(0)}€'; + return 'From ${salaryMin!.toStringAsFixed(0)}€'; } else { - return 'Jusqu\'à ${salaryMax!.toStringAsFixed(0)}€'; + return 'Up to ${salaryMax!.toStringAsFixed(0)}€'; } } + // Create a modified copy of the job offer JobOfferModel copyWith({ String? id, String? employerId, @@ -226,4 +237,4 @@ class JobOfferModel { applicationsCount: applicationsCount ?? this.applicationsCount, ); } -} \ No newline at end of file +} diff --git a/lib/models/user_model_unified.dart b/lib/models/user_model_unified.dart index 373ef2aa..36cc5bac 100644 --- a/lib/models/user_model_unified.dart +++ b/lib/models/user_model_unified.dart @@ -1,31 +1,29 @@ -// Modèle utilisateur unifié - Compatible avec ProfileCompletionController +// This file defines the unified user model used across the app for authentication, profile data, and activity tracking. import 'package:cloud_firestore/cloud_firestore.dart'; class UserModel { - // Identifiants de base + // Basic identifiers final String uid; final String email; - - // Informations personnelles + + // Personal information final String firstName; - final String lastName; + final String lastName; final String fullName; final String? phoneNumber; final String? photoURL; - - // Informations professionnelles + + // Professional information final String title; final String bio; final String experience; final String city; - - // Job preferences removed - - // Activité + + // User activity final List savedJobs; final List appliedJobs; - - // Métadonnées + + // Metadata and status final String provider; final String role; final bool profileCompleted; @@ -57,10 +55,10 @@ class UserModel { this.lastLogin, }); - // Conversion depuis Firestore + // Build a user model from a Firestore document factory UserModel.fromFirestore(DocumentSnapshot doc) { final data = doc.data() as Map; - + return UserModel( uid: data['uid'] ?? doc.id, email: data['email'] ?? '', @@ -73,7 +71,6 @@ class UserModel { bio: data['bio'] ?? '', experience: data['experience'] ?? 'junior', city: data['city'] ?? '', - // jobPreferences removed savedJobs: List.from(data['savedJobs'] ?? []), appliedJobs: List.from(data['appliedJobs'] ?? []), provider: data['provider'] ?? 'email', @@ -86,7 +83,7 @@ class UserModel { ); } - // Conversion vers Firestore + // Convert the user model to Firestore format Map toFirestore() { return { 'uid': uid, @@ -100,7 +97,6 @@ class UserModel { 'bio': bio, 'experience': experience, 'city': city, - // jobPreferences removed 'savedJobs': savedJobs, 'appliedJobs': appliedJobs, 'provider': provider, @@ -113,15 +109,16 @@ class UserModel { }; } - // Helper pour parser les timestamps + // Safely parse Firestore timestamps or strings static DateTime _parseTimestamp(dynamic timestamp) { if (timestamp == null) return DateTime.now(); if (timestamp is Timestamp) return timestamp.toDate(); - if (timestamp is String) return DateTime.tryParse(timestamp) ?? DateTime.now(); + if (timestamp is String) + return DateTime.tryParse(timestamp) ?? DateTime.now(); return DateTime.now(); } - // Copie avec modifications + // Create a copy with updated values UserModel copyWith({ String? uid, String? email, @@ -134,7 +131,6 @@ class UserModel { String? bio, String? experience, String? city, - // jobPreferences parameter removed List? savedJobs, List? appliedJobs, String? provider, @@ -157,7 +153,6 @@ class UserModel { bio: bio ?? this.bio, experience: experience ?? this.experience, city: city ?? this.city, - // jobPreferences removed savedJobs: savedJobs ?? this.savedJobs, appliedJobs: appliedJobs ?? this.appliedJobs, provider: provider ?? this.provider, @@ -170,18 +165,25 @@ class UserModel { ); } - // Nom d'affichage complet - String get displayName => fullName.isNotEmpty ? fullName : '$firstName $lastName'.trim(); + // Full name displayed in the UI + String get displayName => + fullName.isNotEmpty ? fullName : '$firstName $lastName'.trim(); - // Niveau d'expérience lisible + // Human-readable experience label String get experienceLabel { switch (experience) { - case 'internship': return 'Stage / Alternance'; - case 'junior': return 'Junior (0-2 ans)'; - case 'mid': return 'Confirmé (3-5 ans)'; - case 'senior': return 'Senior (5-10 ans)'; - case 'expert': return 'Expert (10+ ans)'; - default: return experience; + case 'internship': + return 'Internship / Apprenticeship'; + case 'junior': + return 'Junior (0-2 years)'; + case 'mid': + return 'Mid-level (3-5 years)'; + case 'senior': + return 'Senior (5-10 years)'; + case 'expert': + return 'Expert (10+ years)'; + default: + return experience; } } -} \ No newline at end of file +} diff --git a/lib/models/user_type.dart b/lib/models/user_type.dart index 6b093747..711a8ebb 100644 --- a/lib/models/user_type.dart +++ b/lib/models/user_type.dart @@ -1,9 +1,12 @@ +// This file defines user roles and provides helpers to convert between enum and stored string values. + enum UserType { candidate, employer, } class UserTypeHelper { + // Convert value to a string for storage or API usage static String getUserTypeString(UserType type) { switch (type) { case UserType.candidate: @@ -13,6 +16,7 @@ class UserTypeHelper { } } + // Convert a stored string into its enum equivalent static UserType getUserTypeFromString(String type) { switch (type) { case 'candidate': @@ -23,4 +27,4 @@ class UserTypeHelper { return UserType.candidate; } } -} \ No newline at end of file +} diff --git a/lib/screen/accessibility/accessibility_panel.dart b/lib/screen/accessibility/accessibility_panel.dart index 9a473593..3090f640 100644 --- a/lib/screen/accessibility/accessibility_panel.dart +++ b/lib/screen/accessibility/accessibility_panel.dart @@ -1,4 +1,3 @@ -// lib/screen/accessibility/accessibility_panel.dart import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -27,12 +26,13 @@ class AccessibilityPanel extends StatelessWidget { foregroundColor: accessibilityService.textColor, elevation: 0, ), + body: Obx(() => SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header + Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( @@ -81,7 +81,6 @@ class AccessibilityPanel extends StatelessWidget { const SizedBox(height: 24), - // Visual Accessibility _buildSection( translationService.getText('visual_accessibility'), [ @@ -112,7 +111,6 @@ class AccessibilityPanel extends StatelessWidget { const SizedBox(height: 20), - // Hearing Accessibility _buildSection( translationService.getText('hearing_accessibility'), [ @@ -142,12 +140,10 @@ class AccessibilityPanel extends StatelessWidget { const SizedBox(height: 24), - // Test Section _buildTestSection(), const SizedBox(height: 24), - // Reset Button SizedBox( width: double.infinity, child: ElevatedButton.icon( @@ -217,7 +213,9 @@ class AccessibilityPanel extends StatelessWidget { color: value ? accessibilityService.primaryColor : accessibilityService.borderColor, - width: value ? (accessibilityService.isHighContrastMode.value ? 3 : 2) : 1, + width: value + ? (accessibilityService.isHighContrastMode.value ? 3 : 2) + : 1, ), boxShadow: accessibilityService.isHighContrastMode.value ? [] : [ BoxShadow( @@ -231,7 +229,7 @@ class AccessibilityPanel extends StatelessWidget { children: [ Icon( icon, - color: value + color: value ? accessibilityService.primaryColor : accessibilityService.secondaryTextColor, size: 24 * accessibilityService.currentFontSize.value, @@ -440,9 +438,10 @@ class AccessibilityPanel extends StatelessWidget { ), ElevatedButton( onPressed: () { - // Reset logic here Navigator.pop(context); - accessibilityService.showAccessibilityFeedback('Settings reset to default'); + accessibilityService.showAccessibilityFeedback( + 'Settings reset to default', + ); }, style: accessibilityService.getAccessibleButtonStyle(), child: Obx(() => Text(translationService.getText('reset'))), @@ -451,4 +450,4 @@ class AccessibilityPanel extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/screen/auth/email_verification/email_verification_screen.dart b/lib/screen/auth/email_verification/email_verification_screen.dart index ef46a079..48c5995c 100644 --- a/lib/screen/auth/email_verification/email_verification_screen.dart +++ b/lib/screen/auth/email_verification/email_verification_screen.dart @@ -1,6 +1,3 @@ -// lib/screen/auth/email_verification/email_verification_screen.dart -// ignore_for_file: deprecated_member_use - import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:google_fonts/google_fonts.dart'; diff --git a/lib/screen/auth/email_verification/employer_email_verification_screen.dart b/lib/screen/auth/email_verification/employer_email_verification_screen.dart index adbd9247..1c47bc8f 100644 --- a/lib/screen/auth/email_verification/employer_email_verification_screen.dart +++ b/lib/screen/auth/email_verification/employer_email_verification_screen.dart @@ -1,4 +1,3 @@ -// lib/screen/auth/email_verification/employer_email_verification_screen.dart import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:google_fonts/google_fonts.dart'; diff --git a/lib/screen/auth/employer_signin/employer_signin_controller.dart b/lib/screen/auth/employer_signin/employer_signin_controller.dart index 910219ce..9d2e75bc 100644 --- a/lib/screen/auth/employer_signin/employer_signin_controller.dart +++ b/lib/screen/auth/employer_signin/employer_signin_controller.dart @@ -1,3 +1,4 @@ +// import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:firebase_auth/firebase_auth.dart'; diff --git a/lib/screen/auth/employer_signin/employer_signin_screen.dart b/lib/screen/auth/employer_signin/employer_signin_screen.dart index 06c82849..81c259c9 100644 --- a/lib/screen/auth/employer_signin/employer_signin_screen.dart +++ b/lib/screen/auth/employer_signin/employer_signin_screen.dart @@ -1,4 +1,3 @@ -// lib/screen/auth/employer_signin/employer_signup_screen_new.dart import 'package:flutter/gestures.dart'; import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; diff --git a/lib/screen/auth/sign_in_screen/sign_in_controller.dart b/lib/screen/auth/sign_in_screen/sign_in_controller.dart index 43d12e90..047e85f4 100644 --- a/lib/screen/auth/sign_in_screen/sign_in_controller.dart +++ b/lib/screen/auth/sign_in_screen/sign_in_controller.dart @@ -1,4 +1,3 @@ -// lib/screen/auth/sign_in_screen/sign_in_controller.dart import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/foundation.dart' show kDebugMode, kIsWeb; @@ -39,14 +38,12 @@ class SignInScreenController extends GetxController { final email = PreferencesService.getString(PrefKeys.emailRememberUser); final pwd = PreferencesService.getString(PrefKeys.passwordRememberUser); - // Seulement pré-remplir si on a des données ET que Remember Me était activé if (email.isNotEmpty && pwd.isNotEmpty) { rememberMe = true; emailController.text = email; passwordController.text = pwd; update(["showEmail", "showPassword", "remember_me"]); } else { - // S'assurer que les champs sont vides si rien n'est mémorisé rememberMe = false; emailController.clear(); passwordController.clear(); @@ -209,12 +206,10 @@ class SignInScreenController extends GetxController { } } - // Sauvegarder les données seulement si Remember Me est activé ET la connexion réussie if (rememberMe) { await PreferencesService.setValue(PrefKeys.emailRememberUser, email); await PreferencesService.setValue(PrefKeys.passwordRememberUser, password); } else { - // Si Remember Me n'est pas activé, s'assurer de supprimer les données sauvegardées PreferencesService.remove(PrefKeys.emailRememberUser); PreferencesService.remove(PrefKeys.passwordRememberUser); } diff --git a/lib/screen/auth/sign_in_screen/sign_in_screen.dart b/lib/screen/auth/sign_in_screen/sign_in_screen.dart index 756d8251..634e733f 100644 --- a/lib/screen/auth/sign_in_screen/sign_in_screen.dart +++ b/lib/screen/auth/sign_in_screen/sign_in_screen.dart @@ -1,4 +1,3 @@ -// lib/screen/auth/sign_in_screen/sign_in_screen.dart import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show PlatformException; @@ -97,23 +96,19 @@ class _SigninScreenUState extends State { PreferencesService.setValue(PrefKeys.fullName, user.displayName ?? ""); PreferencesService.setValue(PrefKeys.rol, "User"); - // Sauvegarder les données utilisateur dans Firestore await GoogleAuthService.saveUserToFirestore(user); // ⭐ NAVIGATION INTELLIGENTE ⭐ - // Vérifier si c'est la première connexion ou un utilisateur existant final creationTime = user.metadata.creationTime; final isNewUser = creationTime != null && creationTime.difference(DateTime.now()).inMinutes.abs() < 5; if (isNewUser) { - // Nouvel utilisateur (créé il y a moins de 5 minutes) AppTheme.showStandardSnackBar( title: "Bienvenue !", message: "Compte créé avec succès. Complétez votre profil.", isSuccess: true, ); - // Aller vers l'écran de complétion de profil Get.offAll(() => const ProfileCompletionScreen()); } else { // Utilisateur existant @@ -405,10 +400,8 @@ class _SigninScreenUState extends State { onTap: () { controller.rememberMe = !controller.rememberMe; if (!controller.rememberMe) { - // Si on désactive Remember Me, supprimer les données sauvegardées PreferencesService.remove(PrefKeys.emailRememberUser); PreferencesService.remove(PrefKeys.passwordRememberUser); - // Et vider les champs pour permettre à l'utilisateur de saisir de nouvelles données controller.emailController.clear(); controller.passwordController.clear(); controller.update(["showEmail", "showPassword"]); diff --git a/lib/screen/auth/sign_up/sign_up_controller.dart b/lib/screen/auth/sign_up/sign_up_controller.dart index 144657c1..70db8d3e 100644 --- a/lib/screen/auth/sign_up/sign_up_controller.dart +++ b/lib/screen/auth/sign_up/sign_up_controller.dart @@ -1,4 +1,3 @@ -// lib/screen/auth/sign_up/sign_up_controller.dart import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/foundation.dart' show kDebugMode; diff --git a/lib/screen/auth/sign_up/sign_up_screen.dart b/lib/screen/auth/sign_up/sign_up_screen.dart index abd1bfa4..83cfc917 100644 --- a/lib/screen/auth/sign_up/sign_up_screen.dart +++ b/lib/screen/auth/sign_up/sign_up_screen.dart @@ -1,4 +1,3 @@ -// lib/screen/auth/sign_up/sign_up_screen.dart import 'package:flutter/gestures.dart'; import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; diff --git a/lib/screen/dashboard/dashboard_controller.dart b/lib/screen/dashboard/dashboard_controller.dart index 2c190b34..b8d2593f 100644 --- a/lib/screen/dashboard/dashboard_controller.dart +++ b/lib/screen/dashboard/dashboard_controller.dart @@ -8,17 +8,14 @@ class DashBoardController extends GetxController{ debugPrint("onBottomBarChange called with index: $index"); if (index == 0) { - // Home tab - forcer le retour sur Home currentTab = 0; debugPrint("Home tab selected - currentTab set to 0"); - // Si on est sur une autre page, revenir à Home if (Get.currentRoute != '/dashboard') { Get.offAllNamed('/dashboard'); debugPrint("Navigating to dashboard"); } } else if (index == 1) { - // Jobs tab - aller vers les offres d'emploi currentTab = 1; debugPrint("Jobs tab selected"); Get.toNamed(AppRes.jobRecommendationScreen); diff --git a/lib/screen/dashboard/home/widgets/appbar.dart b/lib/screen/dashboard/home/widgets/appbar.dart index d255befe..c91516bf 100644 --- a/lib/screen/dashboard/home/widgets/appbar.dart +++ b/lib/screen/dashboard/home/widgets/appbar.dart @@ -19,12 +19,10 @@ Widget homeAppBar() { margin: const EdgeInsets.symmetric(horizontal: AppTheme.spacingRegular), child: Row( children: [ - // Bouton retour à gauche accessibilityService.buildAccessibleWidget( semanticLabel: 'Back to login', onTap: () { accessibilityService.triggerHapticFeedback(); - // Rediriger vers l'écran de connexion Get.offAllNamed('/'); }, child: Container( @@ -45,7 +43,6 @@ Widget homeAppBar() { const SizedBox(width: 12), - // Section de salutation centrée et moderne avec photo Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.center, @@ -123,7 +120,6 @@ Widget homeAppBar() { // Actions modernes Row( children: [ - // Menu utilisateur élégant accessibilityService.buildAccessibleWidget( semanticLabel: 'User menu', onTap: () { @@ -156,7 +152,7 @@ void _showUserMenu() { Get.bottomSheet( Container( padding: const EdgeInsets.all(AppTheme.spacingMedium), - margin: const EdgeInsets.only(bottom: 80), // Marge pour éviter la barre de navigation + margin: const EdgeInsets.only(bottom: 80), decoration: const BoxDecoration( color: AppTheme.white, borderRadius: BorderRadius.only( @@ -181,11 +177,11 @@ void _showUserMenu() { // Menu items _buildMenuItem(Icons.settings_outlined, 'Settings', () { Get.back(); - Get.to(() => const SettingsScreenU()); // Navigation vers les paramètres + Get.to(() => const SettingsScreenU()); }), _buildMenuItem(Icons.accessibility, 'Accessibility', () { Get.back(); - Get.to(() => const AccessibilityPanel()); // Navigation vers l'accessibilité + Get.to(() => const AccessibilityPanel()); }), _buildMenuItem(Icons.logout, 'Logout', () => Get.offAllNamed('/')), ], diff --git a/lib/screen/dashboard/home/widgets/quick_apply_button.dart b/lib/screen/dashboard/home/widgets/quick_apply_button.dart index 6342deda..4ad86406 100644 --- a/lib/screen/dashboard/home/widgets/quick_apply_button.dart +++ b/lib/screen/dashboard/home/widgets/quick_apply_button.dart @@ -1,4 +1,3 @@ -// lib/screen/dashboard/home/widgets/quick_apply_button.dart import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:timeless/screen/job_detail_screen/job_detail_upload_cv_screen/upload_cv_controller.dart'; diff --git a/lib/screen/employer/employer_profile_screen.dart b/lib/screen/employer/employer_profile_screen.dart index 76a3970f..0a793c74 100644 --- a/lib/screen/employer/employer_profile_screen.dart +++ b/lib/screen/employer/employer_profile_screen.dart @@ -196,12 +196,6 @@ class _EmployerProfileScreenState extends State { validator: (v) => (v == null || v.trim().isEmpty) ? 'Required' : null, enableInteractiveSelection: true, - toolbarOptions: const ToolbarOptions( - copy: true, - cut: true, - paste: true, - selectAll: true, - ), ), const SizedBox(height: 12), TextFormField( @@ -210,12 +204,6 @@ class _EmployerProfileScreenState extends State { decoration: _dec('Website', hint: 'https://example.com'), keyboardType: TextInputType.url, enableInteractiveSelection: true, - toolbarOptions: const ToolbarOptions( - copy: true, - cut: true, - paste: true, - selectAll: true, - ), ), const SizedBox(height: 12), TextFormField( @@ -223,12 +211,6 @@ class _EmployerProfileScreenState extends State { style: const TextStyle(color: Colors.white), decoration: _dec('Location', hint: 'City, Country'), enableInteractiveSelection: true, - toolbarOptions: const ToolbarOptions( - copy: true, - cut: true, - paste: true, - selectAll: true, - ), ), const SizedBox(height: 12), TextFormField( @@ -238,12 +220,6 @@ class _EmployerProfileScreenState extends State { maxLines: 6, decoration: _dec('About the company'), enableInteractiveSelection: true, - toolbarOptions: const ToolbarOptions( - copy: true, - cut: true, - paste: true, - selectAll: true, - ), ), const SizedBox(height: 20), SizedBox( diff --git a/lib/screen/employer/simple_profile_screen.dart b/lib/screen/employer/simple_profile_screen.dart index 36d671ff..77dabb3b 100644 --- a/lib/screen/employer/simple_profile_screen.dart +++ b/lib/screen/employer/simple_profile_screen.dart @@ -77,7 +77,6 @@ class _SimpleProfileScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Profile Header Container( width: double.infinity, padding: const EdgeInsets.all(20), @@ -128,7 +127,6 @@ class _SimpleProfileScreenState extends State { const SizedBox(height: 24), - // Company Information Section Text( 'Company Information', style: GoogleFonts.poppins( @@ -148,7 +146,6 @@ class _SimpleProfileScreenState extends State { const SizedBox(height: 20), - // Legal Information Section Text( 'Legal Information', style: GoogleFonts.poppins( @@ -167,7 +164,6 @@ class _SimpleProfileScreenState extends State { const SizedBox(height: 24), - // Action Buttons Text( 'Account Actions', style: GoogleFonts.poppins( @@ -194,7 +190,6 @@ class _SimpleProfileScreenState extends State { onTap: () { if (employerData != null) { Get.to(() => EditProfileScreen(employerData: employerData!))?.then((_) { - // Refresh data when returning from edit screen _loadEmployerData(); }); } diff --git a/lib/screen/first_page/first_controller.dart b/lib/screen/first_page/first_controller.dart index b9b0acb1..889c2728 100644 --- a/lib/screen/first_page/first_controller.dart +++ b/lib/screen/first_page/first_controller.dart @@ -1,4 +1,3 @@ -// lib/screen/first_page/first_controller.dart import 'package:get/get.dart'; class FirstScreenController extends GetxController {} diff --git a/lib/screen/first_page/first_screen.dart b/lib/screen/first_page/first_screen.dart index db8dca7b..cb239231 100644 --- a/lib/screen/first_page/first_screen.dart +++ b/lib/screen/first_page/first_screen.dart @@ -1,4 +1,3 @@ -// lib/screen/first_page/first_screen.dart import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -32,7 +31,6 @@ class FirstScreen extends StatelessWidget { backgroundColor: ColorRes.backgroundColor, body: Stack( children: [ - // Arrière-plan avec gradient propre Container( width: Get.width, height: Get.height, @@ -47,7 +45,6 @@ class FirstScreen extends StatelessWidget { ), ), ), - // Contenu scrollable SingleChildScrollView( child: Column( children: [ @@ -57,7 +54,6 @@ class FirstScreen extends StatelessWidget { SizedBox(height: Get.height * 0.04), - // Logo principal TIMELESS SizedBox( width: Get.width * 0.95, height: Get.height * 0.32, @@ -83,7 +79,6 @@ class FirstScreen extends StatelessWidget { ), SizedBox(height: Get.height * 0.03), - // Bouton pour se connecter InkWell( onTap: () { Navigator.push( @@ -118,7 +113,6 @@ class FirstScreen extends StatelessWidget { SizedBox(height: Get.height * 0.025), - // Bouton Créer un compte InkWell( onTap: () { Navigator.push( @@ -217,7 +211,6 @@ class FirstScreen extends StatelessWidget { ), SizedBox(height: Get.height * 0.025), - // Bouton Create Account for PRO InkWell( onTap: () { Navigator.push( @@ -326,12 +319,10 @@ class FirstScreen extends StatelessWidget { ), )), ), - // Espace supplémentaire pour garantir l'accessibilité des CGU SizedBox(height: Get.height * 0.08), ], ), ), - // Boutons fixes en haut Positioned( top: 0, left: 0, @@ -342,10 +333,8 @@ class FirstScreen extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - // Boutons à droite Row( children: [ - // Bouton d'accessibilité InkWell( onTap: () { // Navigator vers AccessibilityPanel diff --git a/lib/screen/introducation_screen/introducation_screen.dart b/lib/screen/introducation_screen/introducation_screen.dart index 5e823efd..c35eee90 100644 --- a/lib/screen/introducation_screen/introducation_screen.dart +++ b/lib/screen/introducation_screen/introducation_screen.dart @@ -1,4 +1,3 @@ -// introduction_screen.dart import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; diff --git a/lib/screen/introducation_screen/introduction_controller.dart b/lib/screen/introducation_screen/introduction_controller.dart index 15d43a4f..89094402 100644 --- a/lib/screen/introducation_screen/introduction_controller.dart +++ b/lib/screen/introducation_screen/introduction_controller.dart @@ -1,4 +1,3 @@ -// introduction_controller.dart import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; diff --git a/lib/screen/jobs/job_application_screen.dart b/lib/screen/jobs/job_application_screen.dart index 162855df..0c372ed9 100644 --- a/lib/screen/jobs/job_application_screen.dart +++ b/lib/screen/jobs/job_application_screen.dart @@ -388,12 +388,6 @@ class _JobApplicationScreenState extends State { TextFormField( controller: _nameController, enableInteractiveSelection: true, - toolbarOptions: const ToolbarOptions( - copy: true, - cut: true, - paste: true, - selectAll: true, - ), decoration: InputDecoration( labelText: translationService.getText('full_name_label'), border: OutlineInputBorder( diff --git a/lib/screen/manager_section/auth_manager/sign_up_new/sign_up_new_controller.dart b/lib/screen/manager_section/auth_manager/sign_up_new/sign_up_new_controller.dart index 5fd3a9ec..1afef06e 100644 --- a/lib/screen/manager_section/auth_manager/sign_up_new/sign_up_new_controller.dart +++ b/lib/screen/manager_section/auth_manager/sign_up_new/sign_up_new_controller.dart @@ -1,4 +1,3 @@ -// lib/screen/manager_section/auth_manager/sign_up_new/sign_up_new_controller.dart import 'dart:async'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; diff --git a/lib/screen/manager_section/auth_manager/sign_up_new/sign_up_new_screen.dart b/lib/screen/manager_section/auth_manager/sign_up_new/sign_up_new_screen.dart index d7b49a03..d905af4e 100644 --- a/lib/screen/manager_section/auth_manager/sign_up_new/sign_up_new_screen.dart +++ b/lib/screen/manager_section/auth_manager/sign_up_new/sign_up_new_screen.dart @@ -1,4 +1,3 @@ -// lib/screen/manager_section/auth_manager/sign_up_new/sign_up_new_screen.dart import 'package:flutter/material.dart'; import 'package:get/get.dart'; diff --git a/lib/screen/new_home_page/new_home_page_controller.dart b/lib/screen/new_home_page/new_home_page_controller.dart index 29ba78b7..c5956ca5 100644 --- a/lib/screen/new_home_page/new_home_page_controller.dart +++ b/lib/screen/new_home_page/new_home_page_controller.dart @@ -1,5 +1,3 @@ -// \lib\screen\new_home_page\new_home_page_controller.dart - import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:timeless/screen/job_recomandation_search/job_recomadation_search.dart'; diff --git a/lib/screen/new_home_page/new_home_page_screen.dart b/lib/screen/new_home_page/new_home_page_screen.dart index d32a6711..27dc3816 100644 --- a/lib/screen/new_home_page/new_home_page_screen.dart +++ b/lib/screen/new_home_page/new_home_page_screen.dart @@ -1,5 +1,3 @@ -// \lib\screen\new_home_page\new_home_page_screen.dart - import 'package:flutter/material.dart'; class HomePageNewScreenU extends StatelessWidget { @@ -8,13 +6,12 @@ class HomePageNewScreenU extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color.fromARGB(255, 115, 3, 12), // fond neutre + backgroundColor: const Color.fromARGB(255, 115, 3, 12), body: SafeArea( child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - // Exemple avec RichText multicolore RichText( textAlign: TextAlign.center, text: const TextSpan( @@ -25,18 +22,18 @@ class HomePageNewScreenU extends StatelessWidget { children: [ TextSpan( text: "Timeless ", - style: TextStyle(color: Colors.green), // Vert Jamaïque + style: TextStyle(color: Colors.green), ), TextSpan( text: "Job ", style: - TextStyle(color: Color(0xFFFED100)), // Jaune Jamaïque + TextStyle(color: Color(0xFFFED100)), ), TextSpan( text: "Search", style: TextStyle( color: Color.fromARGB( - 255, 255, 255, 255)), // Noir Jamaïque + 255, 255, 255, 255)), ), ], ), @@ -44,7 +41,6 @@ class HomePageNewScreenU extends StatelessWidget { const SizedBox(height: 20), - // Slogan statique Text( "Find your future today.", style: const TextStyle( @@ -56,10 +52,9 @@ class HomePageNewScreenU extends StatelessWidget { const SizedBox(height: 40), - // Bouton principal ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: const Color.fromARGB(255, 15, 15, 15), //noir + backgroundColor: const Color.fromARGB(255, 15, 15, 15), foregroundColor: const Color.fromARGB(255, 255, 251, 1), padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 14), @@ -75,7 +70,7 @@ class HomePageNewScreenU extends StatelessWidget { style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w700, - color: Color.fromARGB(255, 255, 255, 0), // texte jaune + color: Color.fromARGB(255, 255, 255, 0), ), ), ), diff --git a/lib/screen/notification_screen/notification_screen.dart b/lib/screen/notification_screen/notification_screen.dart index 11573a70..973d0b74 100644 --- a/lib/screen/notification_screen/notification_screen.dart +++ b/lib/screen/notification_screen/notification_screen.dart @@ -1,4 +1,3 @@ -// lib/screen/notification_screen/notification_screen.dart import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:timeless/services/notification_service.dart'; diff --git a/lib/screen/profile/profile_controller.dart b/lib/screen/profile/profile_controller.dart index 4ed350a1..2c3e8fa8 100644 --- a/lib/screen/profile/profile_controller.dart +++ b/lib/screen/profile/profile_controller.dart @@ -17,7 +17,7 @@ class ProfileController extends GetxController { // Loading state final RxBool isLoading = false.obs; - // Profile data - Observable pour reactivity + // Profile data final RxString fullName = ''.obs; final RxString email = ''.obs; final RxString phoneNumber = ''.obs; @@ -33,7 +33,7 @@ class ProfileController extends GetxController { final RxString salaryRangeMax = ''.obs; final RxString profileImageUrl = ''.obs; - // Controllers pour l'édition + // Text controllers final fullNameController = TextEditingController(); final emailController = TextEditingController(); final phoneController = TextEditingController(); @@ -57,54 +57,43 @@ class ProfileController extends GetxController { _setupRealTimeListeners(); } - // Configurer les listeners pour la mise à jour en temps réel void _setupRealTimeListeners() { - // Listener pour le nom complet - mise à jour en temps réel de l'affichage fullNameController.addListener(() { fullName.value = fullNameController.text.trim(); }); - // Listener pour l'email emailController.addListener(() { email.value = emailController.text.trim(); }); - // Listener pour le téléphone phoneController.addListener(() { phoneNumber.value = phoneController.text.trim(); }); - // Listener pour la ville cityController.addListener(() { city.value = cityController.text.trim(); }); - // Listener pour le pays countryController.addListener(() { country.value = countryController.text.trim(); }); - // Listener pour l'occupation occupationController.addListener(() { occupation.value = occupationController.text.trim(); }); - // Listener pour la bio bioController.addListener(() { bio.value = bioController.text.trim(); }); - // Listener pour le poste jobPositionController.addListener(() { jobPosition.value = jobPositionController.text.trim(); }); - // Listener pour les compétences skillsController.addListener(() { skills.value = skillsController.text.trim(); }); - // Listener pour les salaires salaryMinController.addListener(() { salaryRangeMin.value = salaryMinController.text.trim(); }); @@ -114,14 +103,12 @@ class ProfileController extends GetxController { }); } - // Charger le profil depuis Firebase Future loadProfileFromFirebase() async { try { isLoading.value = true; final user = _auth.currentUser; if (user != null) { - // Charger depuis la collection Auth/User/register (comme utilisé dans sign-in) final doc = await _firestore .collection('Auth') .doc('User') @@ -132,7 +119,6 @@ class ProfileController extends GetxController { if (doc.exists) { final data = doc.data()!; - // Charger toutes les données du profil fullName.value = data['fullName'] ?? user.displayName ?? ''; email.value = data['Email'] ?? user.email ?? ''; phoneNumber.value = data['Phone'] ?? ''; @@ -148,7 +134,6 @@ class ProfileController extends GetxController { salaryRangeMin.value = data['SalaryRangeMin'] ?? ''; salaryRangeMax.value = data['SalaryRangeMax'] ?? ''; - // Remplir aussi les contrôleurs pour l'édition fullNameController.text = fullName.value; emailController.text = email.value; phoneController.text = phoneNumber.value; @@ -164,7 +149,6 @@ class ProfileController extends GetxController { print('✅ Profil chargé depuis Firebase'); } else { - // Si pas de document, utiliser les données de Firebase Auth fullName.value = user.displayName ?? ''; email.value = user.email ?? ''; profileImageUrl.value = user.photoURL ?? ''; @@ -187,12 +171,11 @@ class ProfileController extends GetxController { } } - // Rafraîchir le profil Future refreshProfile() async { await loadProfileFromFirebase(); } - // Getters pour compatibilité avec l'écran + // Getters String get displayName => fullName.value.isNotEmpty ? fullName.value : 'Votre Nom'; String get displayOccupation => occupation.value.isNotEmpty ? occupation.value : ''; String get displayEmail => email.value; @@ -207,7 +190,6 @@ class ProfileController extends GetxController { String get displayJobPosition => jobPosition.value; String get displayDateOfBirth => dateOfBirth.value; - // Méthode pour obtenir les initiales String getInitials() { if (fullName.value.isEmpty) return 'U'; @@ -216,17 +198,14 @@ class ProfileController extends GetxController { return '${names[0][0]}${names[names.length - 1][0]}'.toUpperCase(); } - // Vérifier si une image de profil existe bool hasProfileImage() { return profileImageUrl.value.isNotEmpty; } - // Méthode pour forcer la mise à jour de l'UI void forceUpdate() { update(); } - // Méthodes pour l'édition d'image Future onTapImage() async => await _pickFromCamera(); Future onTapGallery1() async => await _pickFromGallery(); @@ -290,7 +269,6 @@ class ProfileController extends GetxController { } } - // Méthode de sauvegarde Future onTapSubmit() async { try { isLoading.value = true; @@ -378,7 +356,6 @@ class ProfileController extends GetxController { profileData['photoURL'] = imageUrl; } - // Sauvegarder dans Auth/User/register (comme utilisé dans sign-in) await _firestore .collection('Auth') .doc('User') @@ -386,14 +363,12 @@ class ProfileController extends GetxController { .doc(user.uid) .set(profileData, SetOptions(merge: true)); - // Mettre à jour Firebase Auth displayName et photoURL pour synchronisation await user.updateDisplayName(fullNameController.text.trim()); if (imageUrl != null) { await user.updatePhotoURL(imageUrl); } - await user.reload(); // Recharger les données utilisateur + await user.reload(); - // Mettre à jour les préférences locales pour synchronisation avec le reste de l'app await _updateLocalPreferences(); } @@ -412,7 +387,6 @@ class ProfileController extends GetxController { salaryRangeMax.value = salaryMaxController.text.trim(); } - // Mettre à jour les préférences locales pour synchronisation Future _updateLocalPreferences() async { await PreferencesService.setValue(PrefKeys.fullName, fullNameController.text.trim()); await PreferencesService.setValue(PrefKeys.email, emailController.text.trim()); @@ -432,12 +406,10 @@ class ProfileController extends GetxController { } } - // Méthode pour vider le profil Firebase Future clearProfileData() async { try { final user = _auth.currentUser; if (user != null) { - // Supprimer de la collection Auth/User/register await _firestore .collection('Auth') .doc('User') diff --git a/lib/screen/profile/profile_screen.dart b/lib/screen/profile/profile_screen.dart index 321f2003..75ad56c5 100644 --- a/lib/screen/profile/profile_screen.dart +++ b/lib/screen/profile/profile_screen.dart @@ -12,7 +12,6 @@ class ManagerDashBoardScreenController extends GetxController super.onInit(); } - // Gestion des arguments passés à l'écran void _handleInitialArguments() { try { final args = Get.arguments; @@ -28,14 +27,12 @@ class ManagerDashBoardScreenController extends GetxController } } - // Extrait l'index de l'onglet depuis différents formats d'arguments int? _extractTabIndexFromArguments(dynamic args) { if (args is int) { return args; } else if (args is String) { return int.tryParse(args); } else if (args is Map) { - // Cherche différentes clés possibles pour l'index final dynamic value = args['index'] ?? args['tab'] ?? args['currentTab']; if (value is int) return value; @@ -44,16 +41,14 @@ class ManagerDashBoardScreenController extends GetxController return null; } - // Change l'onglet actuel et exécute la logique spécifique void onBottomBarChange(int index) { - if (index == currentTab.value) return; // Évite les traitements inutiles + if (index == currentTab.value) return; currentTab.value = index; _executeTabSpecificLogic(index); update(['bottom_bar']); } - // Exécute la logique spécifique à chaque onglet void _executeTabSpecificLogic(int index) { debugPrint("INDEX IS $index"); @@ -75,7 +70,6 @@ class ManagerDashBoardScreenController extends GetxController } } - // Méthodes spécifiques à chaque onglet void _initHomeTab() { debugPrint("Initializing Home Tab"); } @@ -105,10 +99,8 @@ class ManagerDashBoardScreenController extends GetxController } } - // Méthode utilitaire pour obtenir l'index actuel int get currentIndex => currentTab.value; - // Méthode pour forcer le rafraîchissement de la bottom bar void refreshBottomBar() { update(['bottom_bar']); } diff --git a/lib/screen/profile/profile_view_screen.dart b/lib/screen/profile/profile_view_screen.dart index 29827397..cefcc031 100644 --- a/lib/screen/profile/profile_view_screen.dart +++ b/lib/screen/profile/profile_view_screen.dart @@ -65,7 +65,6 @@ class _ProfileViewScreenState extends State { icon: const Icon(Icons.edit_outlined, color: Colors.black), onPressed: () async { await Get.to(() => EditProfileScreen()); - // Rafraîchir le profil après édition await controller.refreshProfile(); setState(() {}); }, @@ -149,8 +148,7 @@ class _ProfileViewScreenState extends State { child: ElevatedButton.icon( onPressed: () async { await Get.to(() => EditProfileScreen()); - // Rafraîchir le profil après édition - await controller.refreshProfile(); + await controller.refreshProfile(); setState(() {}); }, icon: const Icon(Icons.edit, size: 16), diff --git a/lib/screen/settings/settings_screen.dart b/lib/screen/settings/settings_screen.dart index 824f306a..d8e7b889 100644 --- a/lib/screen/settings/settings_screen.dart +++ b/lib/screen/settings/settings_screen.dart @@ -19,7 +19,6 @@ import 'package:timeless/utils/app_theme.dart'; class SettingsScreenU extends StatelessWidget { const SettingsScreenU({super.key}); - // Obtenir une instance du ProfileController pour la mise à jour en temps réel ProfileController get profileController => Get.put(ProfileController()); AuthService get authService => Get.put(AuthService()); @@ -76,349 +75,29 @@ class SettingsScreenU extends StatelessWidget { ) ], ), - const SizedBox(height: 10), - /* InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (con) => const NotificationScreenU())); - }, - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container( - height: 55, - width: 55, - decoration: BoxDecoration( - color: ColorRes.logoColor, - borderRadius: BorderRadius.circular(15), - ), - child: const Icon( - Icons.notifications, - color: ColorRes.containerColor, - ), - ), - const SizedBox(width: 15), - Text( - Strings.notification, - style: appTextStyle( - fontWeight: FontWeight.w500, - fontSize: 14, - color: ColorRes.black), - ), - ], - ), - const Image( - image: AssetImage(AssetRes.settingaArrow), - height: 15, - ), - ], - ), - ), - ), - const SizedBox(height: 3), - Container( - margin: const EdgeInsets.symmetric(horizontal: 10), - color: ColorRes.lightGrey.withOpacity(0.8), - height: 1, - ), - const SizedBox(height: 10),*/ - // InkWell( - // onTap: () { - // Navigator.push( - // context, - // MaterialPageRoute( - // builder: (con) => const SecurityScreenU())); - // }, - // child: Padding( - // padding: const EdgeInsets.all(12.0), - // child: Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // Row( - // children: [ - // Container( - // height: 55, - // width: 55, - // decoration: BoxDecoration( - // color: ColorRes.logoColor, - // borderRadius: BorderRadius.circular(15), - // ), - // child: const Icon( - // Icons.lock, - // color: ColorRes.containerColor, - // ), - // ), - // const SizedBox(width: 15), - // Text( - // Strings.security, - // style: appTextStyle( - // fontWeight: FontWeight.w500, - // fontSize: 14, - // color: ColorRes.black), - // ), - // ], - // ), - // const Image( - // image: AssetImage(AssetRes.settingaArrow), - // height: 15, - // ), - // ], - // ), - // ), - // ), - // const SizedBox(height: 3), - // Container( - // margin: const EdgeInsets.symmetric(horizontal: 10), - // color: ColorRes.lightGrey.withOpacity(0.8), - // height: 1, - // ), - const SizedBox(height: 10), - // Modifier le nom/email - InkWell( + const SizedBox(height: 20), + SettingsMenuItem( + icon: Icons.edit, + title: "Edit profile", onTap: () => _showEditProfile(context), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container( - height: 55, - width: 55, - decoration: BoxDecoration( - color: const Color(0xFF000647), - borderRadius: BorderRadius.circular(15), - ), - child: const Icon( - Icons.edit, - color: Colors.white, - ), - ), - const SizedBox(width: 15), - Text( - "Edit profile", - style: appTextStyle( - fontWeight: FontWeight.w500, - fontSize: 14, - color: ColorRes.black), - ), - ], - ), - const Icon( - Icons.arrow_forward_ios, - color: Colors.grey, - size: 15, - ), - ], - ), - ), ), - const SizedBox(height: 3), - Container( - margin: const EdgeInsets.symmetric(horizontal: 10), - color: ColorRes.lightGrey.withOpacity(0.8), - height: 1, - ), - const SizedBox(height: 10), - // Modifier le mot de passe - InkWell( + const SettingsDivider(), + SettingsMenuItem( + icon: Icons.lock_reset, + title: "Change password", onTap: () => _showChangePassword(context), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container( - height: 55, - width: 55, - decoration: BoxDecoration( - color: const Color(0xFF000647), - borderRadius: BorderRadius.circular(15), - ), - child: const Icon( - Icons.lock_reset, - color: Colors.white, - ), - ), - const SizedBox(width: 15), - Text( - "Change password", - style: appTextStyle( - fontWeight: FontWeight.w500, - fontSize: 14, - color: ColorRes.black), - ), - ], - ), - const Icon( - Icons.arrow_forward_ios, - color: Colors.grey, - size: 15, - ), - ], - ), - ), ), - const SizedBox(height: 3), - Container( - margin: const EdgeInsets.symmetric(horizontal: 10), - color: ColorRes.lightGrey.withOpacity(0.8), - height: 1, - ), - const SizedBox(height: 10), - // Supprimer le compte - InkWell( + const SettingsDivider(), + SettingsMenuItem( + icon: Icons.delete_forever, + title: "Delete account", onTap: () => _showDeleteAccount(context), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container( - height: 55, - width: 55, - decoration: BoxDecoration( - color: const Color(0xFF000647), - borderRadius: BorderRadius.circular(15), - ), - child: const Icon( - Icons.delete_forever, - color: Colors.white, - ), - ), - const SizedBox(width: 15), - Text( - "Delete account", - style: appTextStyle( - fontWeight: FontWeight.w500, - fontSize: 14, - color: ColorRes.black), - ), - ], - ), - const Icon( - Icons.arrow_forward_ios, - color: Colors.grey, - size: 15, - ), - ], - ), - ), - ), - const SizedBox(height: 3), - Container( - margin: const EdgeInsets.symmetric(horizontal: 10), - color: ColorRes.lightGrey.withOpacity(0.8), - height: 1, - ), - const SizedBox(height: 10), - - // const SizedBox(height: 10), - /* InkWell( - onTap: () { - Navigator.push(context, - MaterialPageRoute(builder: (con) => const HelpScreenU())); - }, - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container( - height: 55, - width: 55, - decoration: BoxDecoration( - color: ColorRes.logoColor, - borderRadius: BorderRadius.circular(15), - ), - child: const Padding( - padding: EdgeInsets.all(17.0), - child: Image( - image: AssetImage(AssetRes.settingHelp), - width: 20, - color: ColorRes.containerColor, - ), - ), - ), - const SizedBox(width: 15), - Text( - Strings.help, - style: appTextStyle( - fontWeight: FontWeight.w500, - fontSize: 14, - color: ColorRes.black), - ), - ], - ), - const Image( - image: AssetImage(AssetRes.settingaArrow), - height: 15, - ), - ], - ), - ), - ), - const SizedBox(height: 3), - Container( - margin: const EdgeInsets.symmetric(horizontal: 10), - color: ColorRes.lightGrey.withOpacity(0.8), - height: 1, ), - */ - - const SizedBox(height: 10), - Padding( - padding: const EdgeInsets.all(12.0), - child: InkWell( - onTap: () => _showLogoutConfirmation(context, controller), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container( - height: 55, - width: 55, - decoration: BoxDecoration( - color: const Color(0xFF000647), - borderRadius: BorderRadius.circular(15), - ), - child: const Icon( - Icons.logout, - color: Colors.white, - ), - ), - const SizedBox(width: 15), - Text( - Strings.logout, - style: appTextStyle( - fontWeight: FontWeight.w500, - fontSize: 14, - color: ColorRes.black), - ), - ], - ), - const Icon( - Icons.arrow_forward_ios, - color: Colors.grey, - size: 15, - ), - ], - ), - ), + const SizedBox(height: 20), + SettingsMenuItem( + icon: Icons.logout, + title: Strings.logout, + onTap: () => _showLogoutConfirmation(context, controller), ), ]), ), @@ -531,7 +210,6 @@ class SettingsScreenU extends StatelessWidget { }); } - // Fonction de déconnexion complètement améliorée void _showLogoutConfirmation(BuildContext context, DashBoardController dashController) async { final profileCtrl = profileController; @@ -687,10 +365,8 @@ class SettingsScreenU extends StatelessWidget { } } - // Fonction pour effectuer la déconnexion Future _performLogout(DashBoardController dashController) async { try { - // Afficher un dialogue de progression Get.dialog( AlertDialog( backgroundColor: Colors.white, @@ -722,26 +398,19 @@ class SettingsScreenU extends StatelessWidget { barrierDismissible: false, ); - // Réinitialiser l'onglet actuel du dashboard dashController.currentTab = 0; dashController.update(["bottom_bar"]); - // Déconnexion avec AuthService (plus robuste) await authService.signOut(); - // Nettoyer le profil controller profileController.clearProfileData(); - // Nettoyer toutes les préférences await _clearAllPreferences(); - // Attendre un peu pour s'assurer que tout est nettoyé await Future.delayed(const Duration(milliseconds: 500)); - // Fermer le dialogue de progression Get.back(); - // Afficher confirmation de déconnexion Get.dialog( AlertDialog( backgroundColor: Colors.white, @@ -804,7 +473,6 @@ class SettingsScreenU extends StatelessWidget { ElevatedButton( onPressed: () { Get.back(); - // Navigation vers l'écran de démarrage Get.offAllNamed('/'); }, style: ElevatedButton.styleFrom( @@ -828,7 +496,6 @@ class SettingsScreenU extends StatelessWidget { ); } catch (e) { - // Fermer le dialogue de progression si ouvert if (Get.isDialogOpen ?? false) { Get.back(); } @@ -844,7 +511,6 @@ class SettingsScreenU extends StatelessWidget { } Future _clearAllPreferences() async { - // Nettoyer toutes les données utilisateur final keysToRemove = [ PrefKeys.password, PrefKeys.rememberMe, @@ -865,15 +531,13 @@ class SettingsScreenU extends StatelessWidget { } } - // Fonction pour modifier le profil avec mise à jour temps réel void _showEditProfile(BuildContext context) { - // Initialiser le ProfileController pour avoir accès aux données final controller = profileController; Get.bottomSheet( Container( constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.75, // Limite la hauteur à 75% de l'écran + maxHeight: MediaQuery.of(context).size.height * 0.75, ), decoration: const BoxDecoration( color: Colors.white, @@ -885,7 +549,6 @@ class SettingsScreenU extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - // Header fixe Container( padding: const EdgeInsets.fromLTRB(20, 20, 20, 10), child: Row( @@ -904,13 +567,11 @@ class SettingsScreenU extends StatelessWidget { ), ), - // Contenu scrollable Flexible( child: SingleChildScrollView( padding: const EdgeInsets.only(bottom: 20), child: Column( children: [ - // Edit Name avec affichage du nom actuel ListTile( leading: const Icon(Icons.person, color: Color(0xFF000647)), title: Text("Edit name", style: appTextStyle(fontSize: 15, color: ColorRes.black)), @@ -929,7 +590,6 @@ class SettingsScreenU extends StatelessWidget { ), const Divider(height: 1, indent: 20, endIndent: 20), - // Edit Email avec affichage de l'email actuel ListTile( leading: const Icon(Icons.email, color: Color(0xFF000647)), title: Text("Edit email", style: appTextStyle(fontSize: 15, color: ColorRes.black)), @@ -948,7 +608,6 @@ class SettingsScreenU extends StatelessWidget { ), const Divider(height: 1, indent: 20, endIndent: 20), - // Edit Phone avec affichage du téléphone actuel ListTile( leading: const Icon(Icons.phone, color: Color(0xFF000647)), title: Text("Edit phone", style: appTextStyle(fontSize: 15, color: ColorRes.black)), @@ -967,7 +626,6 @@ class SettingsScreenU extends StatelessWidget { ), const Divider(height: 1, indent: 20, endIndent: 20), - // Profile picture avec affichage de l'état actuel ListTile( leading: const Icon(Icons.photo_camera, color: Color(0xFF000647)), title: Text("Profile picture", style: appTextStyle(fontSize: 15, color: ColorRes.black)), @@ -985,7 +643,6 @@ class SettingsScreenU extends StatelessWidget { contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4), ), - // Espace en bas pour éviter le débordement SizedBox(height: MediaQuery.of(context).viewInsets.bottom + 20), ], ), @@ -994,11 +651,10 @@ class SettingsScreenU extends StatelessWidget { ], ), ), - isScrollControlled: true, // Permet au bottom sheet d'être responsive + isScrollControlled: true, ); } - // Fonction pour changer le mot de passe améliorée void _showChangePassword(BuildContext context) { final controller = profileController; @@ -1124,7 +780,6 @@ class SettingsScreenU extends StatelessWidget { ); } - // Fonction pour supprimer le compte améliorée void _showDeleteAccount(BuildContext context) { final controller = profileController; @@ -1245,7 +900,6 @@ class SettingsScreenU extends StatelessWidget { ); } - // Widget helper pour les éléments de suppression Widget _buildDeleteItem(String text) { return Padding( padding: const EdgeInsets.symmetric(vertical: 2), @@ -1259,7 +913,6 @@ class SettingsScreenU extends StatelessWidget { ); } - // Fonction pour modifier le nom avec mise à jour temps réel void _showEditNameDialog(BuildContext context) { final controller = profileController; final TextEditingController nameController = TextEditingController(); @@ -1311,10 +964,8 @@ class SettingsScreenU extends StatelessWidget { try { controller.isLoading.value = true; - // Mettre à jour le contrôleur - cela déclenchera la mise à jour temps réel controller.fullNameController.text = newName; - // Sauvegarder dans Firestore avec le ProfileController await controller.onTapSubmit(); Get.back(); @@ -1366,7 +1017,6 @@ class SettingsScreenU extends StatelessWidget { ); } - // Fonction pour modifier l'email avec mise à jour temps réel void _showEditEmailDialog(BuildContext context) { final controller = profileController; final TextEditingController emailController = TextEditingController(); @@ -1418,10 +1068,8 @@ class SettingsScreenU extends StatelessWidget { try { controller.isLoading.value = true; - // Mettre à jour le contrôleur - cela déclenchera la mise à jour temps réel controller.emailController.text = newEmail; - // Sauvegarder dans Firestore avec le ProfileController await controller.onTapSubmit(); Get.back(); @@ -1473,7 +1121,6 @@ class SettingsScreenU extends StatelessWidget { ); } - // Nouvelle fonction pour modifier le téléphone void _showEditPhoneDialog(BuildContext context) { final controller = profileController; final TextEditingController phoneController = TextEditingController(); @@ -1525,10 +1172,8 @@ class SettingsScreenU extends StatelessWidget { try { controller.isLoading.value = true; - // Mettre à jour le contrôleur - cela déclenchera la mise à jour temps réel controller.phoneController.text = newPhone; - // Sauvegarder dans Firestore avec le ProfileController await controller.onTapSubmit(); Get.back(); @@ -1573,7 +1218,6 @@ class SettingsScreenU extends StatelessWidget { ); } - // Nouvelle fonction pour la photo de profil void _showEditProfilePictureDialog(BuildContext context) { final controller = profileController; @@ -1647,14 +1291,12 @@ class SettingsScreenU extends StatelessWidget { ); } - // Fonction pour envoyer l'email de réinitialisation améliorée void _sendPasswordResetEmail() async { final controller = profileController; final email = controller.email.value; if (email.isNotEmpty) { try { - // Utiliser l'AuthService pour envoyer l'email de réinitialisation final success = await authService.resetPassword(email); if (success) { @@ -1745,7 +1387,6 @@ class SettingsScreenU extends StatelessWidget { } } - // Fonction pour confirmer la suppression du compte améliorée void _confirmDeleteAccount() async { final controller = profileController; final TextEditingController confirmController = TextEditingController(); @@ -1908,7 +1549,6 @@ class SettingsScreenU extends StatelessWidget { final controller = profileController; try { - // Afficher un dialogue de progression Get.dialog( AlertDialog( backgroundColor: Colors.white, @@ -1940,25 +1580,19 @@ class SettingsScreenU extends StatelessWidget { barrierDismissible: false, ); - // Supprimer le profil de Firestore await controller.clearProfileData(); - // Supprimer le compte utilisateur Firebase final user = FirebaseAuth.instance.currentUser; if (user != null) { await user.delete(); } - // Déconnecter Google si nécessaire await GoogleAuthService.signOut(); - // Nettoyer les préférences locales await _clearAllPreferences(); - // Fermer le dialogue de progression Get.back(); - // Afficher confirmation et rediriger Get.dialog( AlertDialog( backgroundColor: Colors.white, @@ -2013,7 +1647,6 @@ class SettingsScreenU extends StatelessWidget { ); } catch (e) { - // Fermer le dialogue de progression si ouvert if (Get.isDialogOpen ?? false) { Get.back(); } diff --git a/lib/screen/splashScreen/splash_screen.dart b/lib/screen/splashScreen/splash_screen.dart index 7f2b1e70..13936254 100644 --- a/lib/screen/splashScreen/splash_screen.dart +++ b/lib/screen/splashScreen/splash_screen.dart @@ -1,4 +1,3 @@ -// lib/screen/splashScreen/splash_screen.dart import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:google_fonts/google_fonts.dart'; diff --git a/lib/services/realtime_firestore_service.dart b/lib/services/realtime_firestore_service.dart index 013350cd..dc954815 100644 --- a/lib/services/realtime_firestore_service.dart +++ b/lib/services/realtime_firestore_service.dart @@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart'; class RealtimeFirestoreService { static final FirebaseFirestore _firestore = FirebaseFirestore.instance; - // Stream en temps réel de toutes les offres d'emploi actives + // Real-time stream of all active job offers static Stream getJobOffersStream() { return _firestore .collection('allPost') @@ -14,7 +14,7 @@ class RealtimeFirestoreService { .snapshots(); } - // Stream en temps réel des offres d'emploi par employeur + // Real-time stream of job offers by employer static Stream getEmployerJobsStream(String employerId) { return _firestore .collection('allPost') @@ -23,7 +23,7 @@ class RealtimeFirestoreService { .snapshots(); } - // Stream en temps réel des candidatures pour un employeur + // Real-time stream of applications for employer static Stream getEmployerApplicationsStream( String employerId) { return _firestore @@ -33,7 +33,7 @@ class RealtimeFirestoreService { .snapshots(); } - // Stream en temps réel des candidatures pour un candidat + // Real-time stream of applications for candidate static Stream getCandidateApplicationsStream( String candidateId) { return _firestore @@ -43,12 +43,12 @@ class RealtimeFirestoreService { .snapshots(); } - // Stream en temps réel des données employeur + // Real-time stream of employer data static Stream getEmployerDataStream(String employerId) { return _firestore.collection('employers').doc(employerId).snapshots(); } - // Stream en temps réel des candidatures pour une offre spécifique + // Real-time stream of applications for specific offer static Stream getJobApplicationsStream(String jobId) { return _firestore .collection('applications') @@ -57,12 +57,12 @@ class RealtimeFirestoreService { .snapshots(); } - // Écouter les changements sur une offre d'emploi spécifique + // Listen to changes on specific job offer static Stream getJobOfferStream(String jobId) { return _firestore.collection('allPost').doc(jobId).snapshots(); } - // Mettre à jour le nombre de vues d'une offre en temps réel + // Update job offer view count in real-time static Future incrementJobViews(String jobId) async { try { await _firestore.collection('allPost').doc(jobId).update({ @@ -74,7 +74,7 @@ class RealtimeFirestoreService { } } - // Mettre à jour le statut d'une candidature en temps réel + // Update application status in real-time static Future updateApplicationStatus( String applicationId, String status) async { try { @@ -89,7 +89,7 @@ class RealtimeFirestoreService { } } - // Obtenir les statistiques en temps réel pour un employeur + // Get real-time statistics for employer static Stream> getEmployerStatsStream( String employerId) { return _firestore @@ -106,7 +106,7 @@ class RealtimeFirestoreService { .where('isActive', isEqualTo: true) .get(); - // Compter les candidatures reçues + // Count received applications final applicationsQuery = await _firestore .collection('applications') .where('employerId', isEqualTo: employerId) @@ -127,7 +127,7 @@ class RealtimeFirestoreService { }); } - // Stream combiné des données essentielles pour le dashboard employeur + // Combined stream of essential data for employer dashboard static Stream> getEmployerDashboardStream( String employerId) { return _firestore @@ -138,7 +138,7 @@ class RealtimeFirestoreService { if (!employerDoc.exists) return {'error': 'Employeur non trouvé'}; try { - // Récupérer les offres avec leurs candidatures + // Get offers with their applications final jobsSnapshot = await _firestore .collection('allPost') .where('employerId', isEqualTo: employerId) @@ -156,7 +156,7 @@ class RealtimeFirestoreService { totalApplications += (jobData['applicationsCount'] ?? 0) as int; } - // Récupérer les candidatures récentes + // Get recent applications final recentApplicationsSnapshot = await _firestore .collection('applications') .where('employerId', isEqualTo: employerId) @@ -194,7 +194,7 @@ class RealtimeFirestoreService { }); } - // Écouter les nouvelles candidatures en temps réel pour notifications + // Listen to new applications in real-time for notifications static Stream getNewApplicationsStream(String employerId) { final DateTime oneHourAgo = DateTime.now().subtract(const Duration(hours: 1)); diff --git a/lib/services/unified_translation_service.dart b/lib/services/unified_translation_service.dart index 8f0e5bc4..ef94b525 100644 --- a/lib/services/unified_translation_service.dart +++ b/lib/services/unified_translation_service.dart @@ -484,7 +484,7 @@ class UnifiedTranslationService extends GetxController { 'Are you sure you want to apply for this position?', }, 'fr': { - // Navigation & Général + // Navigation & General 'dashboard': 'Tableau de Bord', 'profile': 'Profil', 'settings': 'Paramètres', @@ -527,7 +527,7 @@ class UnifiedTranslationService extends GetxController { 'contract_type': 'Type de contrat', 'category': 'Catégorie', - // Écrans d'Auth + // Auth Screens 'first_name': 'Prénom', 'last_name': 'Nom', 'password': 'Mot de passe', @@ -554,7 +554,7 @@ class UnifiedTranslationService extends GetxController { 'job_position': 'Poste', 'bio': 'Biographie', 'edit_profile': 'Modifier le Profil', - // 'job_preferences': 'Préférences d\'Emploi', // Removed + // 'job_preferences': 'Job Preferences', // Removed // Emplois 'position': 'Poste', @@ -573,7 +573,7 @@ class UnifiedTranslationService extends GetxController { 'password_too_short': 'Mot de passe trop court', 'please_enter_password': 'Veuillez saisir un mot de passe', - // Termes & Accessibilité + // Terms & Accessibility 'terms_agreement': 'En créant un compte, vous acceptez nos', 'terms_of_service': 'Conditions d\'Utilisation', 'accessibility': 'Accessibilité', @@ -581,7 +581,7 @@ class UnifiedTranslationService extends GetxController { 'welcome': 'Bienvenue', 'app_tagline': 'Combler le fossé avec un talent intemporel', - // Thème & Apparence + // Theme & Appearance 'appearance': 'Apparence', 'dark_mode': 'Mode Sombre', 'light_mode': 'Mode Clair', @@ -617,7 +617,7 @@ class UnifiedTranslationService extends GetxController { 'Êtes-vous sûr de vouloir postuler pour ce poste ?', }, 'es': { - // Navegación y General + // Navigation and General 'dashboard': 'Panel de Control', 'profile': 'Perfil', 'settings': 'Configuración', @@ -641,7 +641,7 @@ class UnifiedTranslationService extends GetxController { 'switched_to_french': 'Cambiado al francés', 'switched_to_english': 'Cambiado al inglés', - // Autenticación + // Authentication 'first_name': 'Nombre', 'last_name': 'Apellido', 'password': 'Contraseña', diff --git a/lib/utils/app_dimensions.dart b/lib/utils/app_dimensions.dart index 04163914..842aa64f 100644 --- a/lib/utils/app_dimensions.dart +++ b/lib/utils/app_dimensions.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; -// Système d'espacements et de dimensions harmonisé pour Timeless -// Basé sur une échelle de 4px pour une cohérence parfaite +// Harmonized spacing and dimensions system for Timeless +// Based on 4px scale for perfect consistency class AppDimensions { // ========================================== - // ESPACEMENT - ÉCHELLE 4PX + // SPACING - 4PX SCALE // ========================================== static const double xxs = 2.0; // Très petit espacement @@ -19,7 +19,7 @@ class AppDimensions { static const double mega = 48.0; // Espacement mega (pages) // ========================================== - // ESPACEMENTS SPÉCIALISÉS + // SPECIALIZED SPACINGS // ========================================== // Marges de page standard @@ -67,12 +67,12 @@ class AppDimensions { // LAYOUT - CONTRAINTES // ========================================== - // Largeurs maximales pour éviter les éléments trop larges + // Maximum widths to avoid oversized elements static const double maxContentWidth = 400.0; // Largeur max contenu static const double maxFormWidth = 320.0; // Largeur max formulaire static const double maxCardWidth = 380.0; // Largeur max carte - // Hauteurs minimales pour éviter les éléments trop petits + // Minimum heights to avoid undersized elements static const double minTapTarget = 44.0; // Cible tactile minimale static const double minInputHeight = 48.0; // Hauteur min input @@ -90,7 +90,7 @@ class AppDimensions { static const double fontSizeXXXL = 28.0; // Grand titre static const double fontSizeHuge = 32.0; // Très grand titre - // Hauteurs de ligne harmonisées + // Harmonized line heights static const double lineHeightTight = 1.2; // Ligne serrée static const double lineHeightNormal = 1.4; // Ligne normale static const double lineHeightRelaxed = 1.6; // Ligne aérée @@ -120,7 +120,7 @@ class AppDimensions { vertical: md, ); - // Espacement entre les éléments de formulaire + // Spacing between form elements static const EdgeInsets formFieldMargin = EdgeInsets.only(bottom: formFieldSpacing); // Espacement entre les sections diff --git a/lib/utils/app_style.dart b/lib/utils/app_style.dart index 2ec3c815..7162eaf2 100644 --- a/lib/utils/app_style.dart +++ b/lib/utils/app_style.dart @@ -4,7 +4,7 @@ import 'package:timeless/utils/color_res.dart'; import 'package:timeless/utils/app_dimensions.dart'; // ========================================== -// STYLES DE TEXTE HARMONISÉS TIMELESS +// HARMONIZED TIMELESS TEXT STYLES // ========================================== class AppTextStyles { @@ -28,7 +28,7 @@ class AppTextStyles { } // ========================================== - // TITRES ET EN-TÊTES + // TITLES AND HEADERS // ========================================== static TextStyle get h1 => _baseStyle( @@ -100,7 +100,7 @@ class AppTextStyles { ); // ========================================== - // STYLES SPÉCIALISÉS + // SPECIALIZED STYLES // ========================================== // Navigation bottom bar @@ -140,14 +140,14 @@ class AppTextStyles { } // ========================================== -// COMPATIBILITÉ AVEC L'ANCIEN CODE +// COMPATIBILITY WITH OLD CODE // ========================================== // Styles pour la bottom navigation bar final TextStyle bottomTitleStyle = AppTextStyles.bottomTabActive; final TextStyle bottomTitleStyleDisable = AppTextStyles.bottomTabInactive; -// Fonction générique pour compatibilité +// Generic function for compatibility TextStyle appTextStyle({ FontWeight? fontWeight, Color? color, diff --git a/lib/utils/color_res.dart b/lib/utils/color_res.dart index 16510fd2..8fb980da 100644 --- a/lib/utils/color_res.dart +++ b/lib/utils/color_res.dart @@ -1,18 +1,18 @@ import 'package:flutter/material.dart'; -// Palette de couleurs Timeless - Design moderne et harmonieux -// Couleurs autorisées: Bleu foncé, Noir, Blanc, Gris, Orange +// Timeless color palette - Modern and harmonious design +// Allowed colors: Dark Blue, Black, White, Grey, Orange class ColorRes { // ========================================== // PALETTE PRINCIPALE TIMELESS // ========================================== - // Bleu foncé principal - identité de marque + // Primary dark blue - brand identity static const primaryBlue = Color(0xFF1E40AF); // Bleu foncé profond static const primaryBlueDark = Color(0xFF1E3A8A); // Bleu très foncé static const primaryBlueLight = Color(0xFF3B82F6); // Bleu moyen - // Orange pour accents et éléments de surbrillance + // Orange for accents and highlights static const primaryOrange = Color(0xFFF97316); // Orange principal static const primaryOrangeLight = Color(0xFFEA580C); // Orange foncé static const primaryOrangeSoft = Color(0xFFFF9742); // Orange doux @@ -46,7 +46,7 @@ class ColorRes { static const textOnPrimary = Color(0xFFFFFFFF); // Texte sur fond bleu/orange // ========================================== - // GRIS - ÉCHELLE HARMONISÉE + // GREY - HARMONIZED SCALE // ========================================== static const grey100 = Color(0xFFF3F4F6); // Gris très clair @@ -60,7 +60,7 @@ class ColorRes { static const grey900 = Color(0xFF111827); // Gris noir // ========================================== - // BORDURES ET SÉPARATEURS + // BORDERS AND SEPARATORS // ========================================== static const borderColor = grey200; // Bordures standard @@ -68,7 +68,7 @@ class ColorRes { static const dividerColor = grey100; // Séparateurs // ========================================== - // ÉTATS FONCTIONNELS + // FUNCTIONAL STATES // ========================================== static const successColor = primaryBlue; // Succès en bleu @@ -77,10 +77,10 @@ class ColorRes { static const infoColor = primaryBlueLight; // Information en bleu clair // ========================================== - // ALIASES POUR COMPATIBILITÉ + // ALIASES FOR COMPATIBILITY // ========================================== - // Couleurs héritées - remappées sur la nouvelle palette + // Legacy colors - remapped to new palette static const royalBlue = primaryBlue; static const darkBlue = primaryBlueDark; static const blueColor = primaryBlue; @@ -91,7 +91,7 @@ class ColorRes { static const iconColor = primaryOrange; static const starColor = primaryOrange; - // Textes (compatibilité) + // Texts (compatibility) static const textColor = textPrimary; static const black2 = textSecondary; static const lightBlack = textTertiary; @@ -99,13 +99,13 @@ class ColorRes { static const lightGrey = grey100; static const charcoal = grey800; - // Surfaces (compatibilité) + // Surfaces (compatibility) static const white2 = backgroundColor; static const creamWhite = backgroundColor; static const deleteColor = grey100; static const invalidColor = grey100; - // Anciens rouges/verts -> remappés + // Old reds/greens -> remapped static const vibrantRed = primaryBlue; static const red = primaryBlue; static const successGreen = primaryBlue; @@ -114,7 +114,7 @@ class ColorRes { static const appleGreen = primaryOrange; static const lightAppleGreen = primaryOrangeSoft; - // Nettoyage des couleurs sables + // Sand colors cleanup static const softSand = backgroundColor; static const warmSand = primaryOrange; static const darkSand = grey200; @@ -128,7 +128,7 @@ class ColorRes { static const secondaryAccent = primaryOrange; static const gradientColor = primaryBlue; - // Ajouts pour compatibility avec l'ancien code + // Additions for compatibility with old code static const brightYellow = primaryOrange; // Remplacé par orange static const deepBordeaux = primaryBlueDark; // Remplacé par bleu foncé } diff --git a/lib/utils/text_field_helper.dart b/lib/utils/text_field_helper.dart index db33194c..846fc0a2 100644 --- a/lib/utils/text_field_helper.dart +++ b/lib/utils/text_field_helper.dart @@ -13,12 +13,7 @@ class TextFieldHelper { return { 'enableInteractiveSelection': true, 'canRequestFocus': true, - 'toolbarOptions': const ToolbarOptions( - copy: true, - cut: true, - paste: true, - selectAll: true, - ), + // Removed complex contextMenuBuilder - using default Flutter behavior }; } @@ -70,12 +65,6 @@ class TextFieldHelper { // Ensure copy/paste is always enabled enableInteractiveSelection: true, canRequestFocus: true, - toolbarOptions: const ToolbarOptions( - copy: true, - cut: true, - paste: true, - selectAll: true, - ), ); } diff --git a/pubspec.yaml b/pubspec.yaml index b68e1bb4..7a5801b2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,9 +33,9 @@ dependencies: # Utilitaires shared_preferences: ^2.3.2 - intl: ^0.20.2 + intl: ^0.19.0 easy_localization: ^3.0.3 - url_launcher: ^6.3.2 + url_launcher: ^6.2.6 permission_handler: ^12.0.1 country_picker: ^2.0.27 advanced_search: ^2.2.6+1 diff --git a/tests/test_email.dart b/tests/test_email.dart index 3581eebf..309a1ab3 100644 --- a/tests/test_email.dart +++ b/tests/test_email.dart @@ -1,26 +1,26 @@ import 'package:cloud_firestore/cloud_firestore.dart'; void main() async { - // Test direct de l'envoi d'email + // Direct email sending test try { final mailDoc = await FirebaseFirestore.instance.collection("mail").add({ "to": ["bryanomane@gmail.com"], "message": { - "subject": "🧪 Test Email - Configuration SMTP", + "subject": "🧪 Test Email - SMTP Configuration", "html": """ -

Test de Configuration Email

-

Si vous recevez cet email, la configuration SMTP fonctionne correctement !

+

Email Configuration Test

+

If you receive this email, the SMTP configuration is working correctly!


- Test envoyé depuis l'app Timeless + Test sent from Timeless app """, - "text": "Test de configuration email - Si vous recevez cet email, la configuration SMTP fonctionne correctement !", + "text": "Email configuration test - If you receive this email, the SMTP configuration is working correctly!", }, }); - print('✅ Email de test ajouté à la queue: ${mailDoc.id}'); - print('📧 Vérifiez votre boîte mail dans quelques minutes...'); + print('✅ Test email added to queue: ${mailDoc.id}'); + print('📧 Check your mailbox in a few minutes...'); } catch (e) { - print('❌ Erreur lors du test: $e'); + print('❌ Error during test: $e'); } } \ No newline at end of file