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.
440 lines
16 KiB
Dart
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)],
|
|
),
|
|
);
|
|
}
|
|
}
|