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.
This commit is contained in:
439
lib/views/login_view.dart
Normal file
439
lib/views/login_view.dart
Normal file
@@ -0,0 +1,439 @@
|
||||
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)],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user