Files
ERP-APP-INVENTORY/lib/views/login_view.dart
BACHIR SOULDI 5a483da01d Add full app source
Adds the Flutter inventory app source, cleaned up for a shared repo:
generic package name, no hardcoded ERP endpoints (moved to gitignored
local config), no dead auth code, no debug logging of session data.
2026-07-16 14:37:28 +01:00

440 lines
16 KiB
Dart

import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:inventory_app/models/depot.dart';
import 'package:inventory_app/my_app.dart';
import 'package:inventory_app/service/axelor_client.dart';
import 'package:lottie/lottie.dart';
import 'package:quickalert/quickalert.dart';
import 'package:inventory_app/home.dart';
import '../constants.dart';
import '../controllers/simple_ui_controller.dart';
import 'package:inventory_app/controllers/theme_controller.dart';
class LoginView extends StatefulWidget {
const LoginView({super.key});
@override
State<LoginView> createState() => _LoginViewState();
}
class _LoginViewState extends State<LoginView> {
TextEditingController nameController = TextEditingController(text: "");
TextEditingController passwordController = TextEditingController(text: "");
final _formKey = GlobalKey<FormState>();
bool _loading = false;
late AxelorClient client;
bool _clientInitialized = false;
SimpleUIController simpleUIController = Get.put(SimpleUIController());
@override
void initState() {
super.initState();
_initClient();
}
Future<void> _initClient() async {
client = await AxelorClient.create();
setState(() => _clientInitialized = true);
}
@override
Widget build(BuildContext context) {
final themeController = Get.find<ThemeController>();
final isDark = themeController.isDarkMode.value;
final size = MediaQuery.of(context).size;
return Scaffold(
resizeToAvoidBottomInset: true,
body: Stack(
children: [
const _FancyBackground(), // 🧊 Adaptive gradient background
Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: _GlassCard(
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 🌊 Animated wave header
Lottie.asset(
'assets/wave.json',
height: size.height * 0.25,
repeat: true,
),
const SizedBox(height: 20),
Text(
"Se connecter",
style: kLoginTitleStyle(size * 0.80).copyWith(
color:
isDark
? Colors.white.withOpacity(0.95)
: Colors.blue.shade700,
),
),
const SizedBox(height: 8),
Text(
"Bienvenue",
style: kLoginSubtitleStyle(size).copyWith(
color:
isDark
? Colors.white
: Colors.black.withOpacity(0.7),
),
),
const SizedBox(height: 28),
// 👤 Username
TextFormField(
controller: nameController,
style: kTextFormFieldStyle(
color:
isDark
? Colors.white.withOpacity(0.9)
: Colors.black87,
),
decoration: _inputDecoration(
hint: 'Nom d\'utilisateur',
icon: Icons.person,
isDark: isDark,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Entrer un nom d\'utilisateur';
} else if (value.length < 4) {
return 'Au moins 4 caractères';
} else if (value.length > 20) {
return 'Maximum 20 caractères';
}
return null;
},
),
const SizedBox(height: 16),
// 🔒 Password
Obx(
() => TextFormField(
controller: passwordController,
obscureText: simpleUIController.isObscure.value,
style: kTextFormFieldStyle(
color:
isDark
? Colors.white.withOpacity(0.9)
: Colors.black87,
),
decoration: _inputDecoration(
hint: 'Mot de passe',
icon: Icons.lock_open_rounded,
isDark: isDark,
suffix: IconButton(
icon: Icon(
simpleUIController.isObscure.value
? Icons.visibility
: Icons.visibility_off,
color: isDark ? Colors.white70 : Colors.black54,
),
onPressed: simpleUIController.isObscureActive,
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Entrer un mot de passe';
} else if (value.length < 6) {
return 'Au moins 6 caractères';
}
return null;
},
),
),
const SizedBox(height: 14),
Text(
"Contactez votre administrateur si vous avez oublié votre mot de passe",
style: kLoginTermsAndPrivacyStyle(size).copyWith(
color:
isDark
? Colors.white54
: Colors.black.withOpacity(0.5),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
// 🚪 Login button
SizedBox(
width: double.infinity,
height: 55,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
isDark ? Colors.blue.shade400 : Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
shadowColor: Colors.blue.withOpacity(0.4),
elevation: 8,
),
onPressed:
_loading
? null
: () async {
if (_formKey.currentState!.validate()) {
setState(() => _loading = true);
if (!_clientInitialized) {
QuickAlert.show(
context: context,
type: QuickAlertType.error,
title: 'Erreur',
text:
'Client non initialisé. Veuillez réessayer.',
);
setState(() => _loading = false);
return;
}
final loggedIn = await client.login(
nameController.text.trim(),
passwordController.text.trim(),
);
if (loggedIn) {
// final locations =
// await client.fetchLocations();
Get.off(
() => const MyApp(),
// arguments: locations,
);
} else {
QuickAlert.show(
context: context,
type: QuickAlertType.error,
title: 'Erreur',
text:
'Nom d\'utilisateur ou mot de passe incorrect',
);
}
setState(() => _loading = false);
}
},
child:
_loading
? const Center(
child: CircularProgressIndicator(
color: Colors.white,
),
)
: const Text(
'Se connecter',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
),
),
const SizedBox(height: 16),
// 🌗 Toggle theme
Obx(() {
final isDark = themeController.isDarkMode.value;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
isDark
? Icons.dark_mode_rounded
: Icons.light_mode_rounded,
color:
isDark
? Colors.white70
: Colors.blue.shade700,
),
Switch(
value: isDark,
onChanged: (v) {
themeController.toggleTheme();
setState(() {});
},
activeColor: Colors.blue,
),
Text(
isDark ? 'Mode sombre' : 'Mode clair',
style: kTextFormFieldStyle(
color: isDark ? Colors.white70 : Colors.black87,
),
),
],
);
}),
],
),
),
),
),
),
],
),
);
}
InputDecoration _inputDecoration({
required String hint,
required IconData icon,
bool isDark = false,
Widget? suffix,
}) {
return InputDecoration(
prefixIcon: Icon(icon, color: isDark ? Colors.white70 : Colors.black54),
suffixIcon: suffix,
hintText: hint,
hintStyle: TextStyle(color: isDark ? Colors.white54 : Colors.black45),
filled: true,
fillColor:
isDark
? Colors.white.withOpacity(0.08)
: Colors.white.withOpacity(0.7),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
);
}
}
/// Glass card reused
class _GlassCard extends StatelessWidget {
final Widget child;
const _GlassCard({required this.child});
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
colors:
isDark
? [
Colors.white.withOpacity(0.06),
Colors.white.withOpacity(0.03),
]
: [
Colors.white.withOpacity(0.4),
Colors.white.withOpacity(0.2),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
border: Border.all(
color:
isDark
? Colors.white.withOpacity(0.08)
: Colors.white.withOpacity(0.5),
),
boxShadow: [
BoxShadow(
color:
isDark
? Colors.black.withOpacity(0.4)
: Colors.blueGrey.withOpacity(0.15),
blurRadius: 18,
offset: const Offset(0, 10),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Padding(padding: const EdgeInsets.all(20), child: child),
),
),
);
}
}
/// Gradient background reused
class _FancyBackground extends StatelessWidget {
const _FancyBackground();
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors:
isDark
? [
const Color(0xFF0A0A0A),
const Color(0xFF121212),
const Color(0xFF1E1E1E),
]
: [
const Color(0xFFEEF2FF),
const Color(0xFFE0F2FE),
const Color(0xFFE6FFFA),
],
),
),
child: Stack(
children: [
Positioned(
top: -60,
right: -30,
child: _Blob(
color:
isDark
? Colors.blue.withOpacity(0.1)
: Colors.blue.withOpacity(0.18),
size: 180,
),
),
Positioned(
bottom: -40,
left: -30,
child: _Blob(
color:
isDark
? Colors.cyanAccent.withOpacity(0.08)
: Colors.cyan.withOpacity(0.16),
size: 160,
),
),
],
),
);
}
}
class _Blob extends StatelessWidget {
final Color color;
final double size;
const _Blob({required this.color, required this.size});
@override
Widget build(BuildContext context) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
boxShadow: [BoxShadow(color: color, blurRadius: 60, spreadRadius: 30)],
),
);
}
}