login_page.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:go_router/go_router.dart';
  4. import '../../../providers/auth_provider.dart';
  5. import '../../../providers/database_provider.dart';
  6. class LoginPage extends ConsumerStatefulWidget {
  7. const LoginPage({super.key});
  8. @override
  9. ConsumerState<LoginPage> createState() => _LoginPageState();
  10. }
  11. class _LoginPageState extends ConsumerState<LoginPage> {
  12. final _codiceCtrl = TextEditingController();
  13. final _passwordCtrl = TextEditingController();
  14. final _urlCtrl = TextEditingController();
  15. final _formKey = GlobalKey<FormState>();
  16. bool _obscurePassword = true;
  17. bool _rememberMe = false;
  18. @override
  19. void initState() {
  20. super.initState();
  21. _loadUrl();
  22. _loadRememberedCredentials();
  23. }
  24. Future<void> _loadUrl() async {
  25. final url = await ref.read(parametriDaoProvider).read('URL');
  26. if (url != null && url.isNotEmpty) {
  27. _urlCtrl.text = url;
  28. }
  29. }
  30. Future<void> _loadRememberedCredentials() async {
  31. final dao = ref.read(parametriDaoProvider);
  32. final username = await dao.read('REMEMBERED_USERNAME');
  33. final password = await dao.read('REMEMBERED_PASSWORD');
  34. if (username != null &&
  35. username.isNotEmpty &&
  36. password != null &&
  37. password.isNotEmpty &&
  38. mounted) {
  39. _codiceCtrl.text = username;
  40. _passwordCtrl.text = password;
  41. setState(() {
  42. _rememberMe = true;
  43. });
  44. }
  45. }
  46. @override
  47. void dispose() {
  48. _codiceCtrl.dispose();
  49. _passwordCtrl.dispose();
  50. _urlCtrl.dispose();
  51. super.dispose();
  52. }
  53. @override
  54. Widget build(BuildContext context) {
  55. final authState = ref.watch(authProvider);
  56. final theme = Theme.of(context);
  57. final cs = theme.colorScheme;
  58. return Scaffold(
  59. body: SafeArea(
  60. top: false,
  61. child: Stack(
  62. children: [
  63. Positioned(
  64. top: -MediaQuery.of(context).size.height * 0.08,
  65. right: -MediaQuery.of(context).size.width * 0.05,
  66. child: Container(
  67. width: 320,
  68. height: 320,
  69. decoration: BoxDecoration(
  70. color: cs.secondaryContainer.withValues(alpha: 0.15),
  71. shape: BoxShape.circle,
  72. ),
  73. ),
  74. ),
  75. Positioned(
  76. bottom: -MediaQuery.of(context).size.height * 0.08,
  77. left: -MediaQuery.of(context).size.width * 0.05,
  78. child: Container(
  79. width: 320,
  80. height: 320,
  81. decoration: BoxDecoration(
  82. color: cs.primaryContainer.withValues(alpha: 0.08),
  83. shape: BoxShape.circle,
  84. ),
  85. ),
  86. ),
  87. Center(
  88. child: SingleChildScrollView(
  89. padding: const EdgeInsets.symmetric(horizontal: 16),
  90. child: ConstrainedBox(
  91. constraints: const BoxConstraints(maxWidth: 400),
  92. child: Column(
  93. mainAxisSize: MainAxisSize.min,
  94. children: [
  95. const SizedBox(height: 32),
  96. _buildBrandHeader(theme, cs),
  97. const SizedBox(height: 32),
  98. _buildFormCard(authState, theme, cs),
  99. const SizedBox(height: 32),
  100. _buildFooterLinks(theme, cs),
  101. const SizedBox(height: 24),
  102. ],
  103. ),
  104. ),
  105. ),
  106. ),
  107. ],
  108. ),
  109. ),
  110. );
  111. }
  112. Widget _buildBrandHeader(ThemeData theme, ColorScheme cs) {
  113. return Column(
  114. children: [
  115. Container(
  116. width: 64,
  117. height: 64,
  118. decoration: BoxDecoration(
  119. color: cs.primary,
  120. borderRadius: BorderRadius.circular(12),
  121. boxShadow: [
  122. BoxShadow(
  123. color: Colors.black.withValues(alpha: 0.15),
  124. blurRadius: 12,
  125. offset: const Offset(0, 4),
  126. ),
  127. ],
  128. ),
  129. child: const Icon(Icons.bolt, color: Colors.white, size: 40),
  130. ),
  131. const SizedBox(height: 16),
  132. Text(
  133. 'PowerAge',
  134. style: theme.textTheme.headlineLarge?.copyWith(
  135. color: cs.primary,
  136. letterSpacing: -0.02,
  137. ),
  138. ),
  139. const SizedBox(height: 8),
  140. Text(
  141. 'Accedi alla tua piattaforma di vendita ad alte prestazioni.',
  142. textAlign: TextAlign.center,
  143. style: theme.textTheme.bodyMedium?.copyWith(
  144. color: cs.onSurfaceVariant,
  145. ),
  146. ),
  147. ],
  148. );
  149. }
  150. Widget _buildFormCard(AuthState authState, ThemeData theme, ColorScheme cs) {
  151. return Container(
  152. decoration: BoxDecoration(
  153. color: Colors.white,
  154. borderRadius: BorderRadius.circular(12),
  155. border: Border.all(color: cs.outlineVariant),
  156. boxShadow: [
  157. BoxShadow(
  158. color: const Color(0xFF0F172A).withValues(alpha: 0.05),
  159. blurRadius: 12,
  160. offset: const Offset(0, 4),
  161. ),
  162. ],
  163. ),
  164. child: Padding(
  165. padding: const EdgeInsets.all(24),
  166. child: Form(
  167. key: _formKey,
  168. child: Column(
  169. crossAxisAlignment: CrossAxisAlignment.stretch,
  170. children: [
  171. _buildLabeledField(
  172. label: 'Username',
  173. icon: Icons.person_outline,
  174. controller: _codiceCtrl,
  175. hintText: 'Inserisci il tuo username',
  176. validator: (v) => v == null || v.isEmpty
  177. ? 'Inserisci il codice agente'
  178. : null,
  179. ),
  180. const SizedBox(height: 16),
  181. _buildPasswordField(cs),
  182. const SizedBox(height: 16),
  183. _buildLabeledField(
  184. label: 'URL di Connessione',
  185. icon: Icons.link,
  186. controller: _urlCtrl,
  187. hintText: 'https://api.powerage.cloud',
  188. validator: (v) => v == null || v.isEmpty
  189. ? "Inserisci l'URL del backend"
  190. : null,
  191. ),
  192. const SizedBox(height: 8),
  193. _buildRememberRow(theme, cs),
  194. const SizedBox(height: 20),
  195. FilledButton.icon(
  196. onPressed: authState.isLoading
  197. ? null
  198. : () {
  199. if (_formKey.currentState!.validate()) {
  200. ref
  201. .read(authProvider.notifier)
  202. .login(
  203. _codiceCtrl.text,
  204. _passwordCtrl.text,
  205. _urlCtrl.text,
  206. rememberMe: _rememberMe,
  207. );
  208. }
  209. },
  210. icon: authState.isLoading
  211. ? const SizedBox(
  212. width: 20,
  213. height: 20,
  214. child: CircularProgressIndicator(
  215. strokeWidth: 2,
  216. color: Colors.white,
  217. ),
  218. )
  219. : const Icon(Icons.login),
  220. label: Text(
  221. authState.isLoading ? 'Verifica in corso...' : 'Accedi',
  222. ),
  223. ),
  224. if (authState.error != null) ...[
  225. const SizedBox(height: 16),
  226. _buildErrorCard(authState.error!, cs),
  227. ],
  228. Padding(
  229. padding: const EdgeInsets.symmetric(vertical: 24),
  230. child: Row(
  231. children: [
  232. Expanded(child: Divider(color: cs.outlineVariant)),
  233. Padding(
  234. padding: const EdgeInsets.symmetric(horizontal: 12),
  235. child: Text(
  236. 'Configurazione Sistema',
  237. style: theme.textTheme.labelMedium?.copyWith(
  238. color: cs.onSurfaceVariant,
  239. ),
  240. ),
  241. ),
  242. Expanded(child: Divider(color: cs.outlineVariant)),
  243. ],
  244. ),
  245. ),
  246. Text(
  247. 'Inizializza gli archivi di PowerAge',
  248. textAlign: TextAlign.center,
  249. style: theme.textTheme.bodyMedium?.copyWith(
  250. color: cs.onSurfaceVariant,
  251. ),
  252. ),
  253. const SizedBox(height: 12),
  254. OutlinedButton.icon(
  255. onPressed: () => context.go('/sync-settings'),
  256. icon: const Icon(Icons.settings_backup_restore, size: 18),
  257. label: const Text('Inizializza'),
  258. ),
  259. ],
  260. ),
  261. ),
  262. ),
  263. );
  264. }
  265. Widget _buildLabeledField({
  266. required String label,
  267. required IconData icon,
  268. required TextEditingController controller,
  269. required String hintText,
  270. String? Function(String?)? validator,
  271. }) {
  272. final cs = Theme.of(context).colorScheme;
  273. return Column(
  274. crossAxisAlignment: CrossAxisAlignment.start,
  275. children: [
  276. Text(
  277. label,
  278. style: TextStyle(
  279. fontSize: 12,
  280. height: 16 / 12,
  281. fontWeight: FontWeight.w600,
  282. color: cs.onSurface,
  283. ),
  284. ),
  285. const SizedBox(height: 8),
  286. TextFormField(
  287. controller: controller,
  288. decoration: InputDecoration(
  289. hintText: hintText,
  290. prefixIcon: Icon(icon, size: 20),
  291. ),
  292. validator: validator,
  293. ),
  294. ],
  295. );
  296. }
  297. Widget _buildPasswordField(ColorScheme cs) {
  298. return Column(
  299. crossAxisAlignment: CrossAxisAlignment.start,
  300. children: [
  301. Text(
  302. 'Password',
  303. style: TextStyle(
  304. fontSize: 12,
  305. height: 16 / 12,
  306. fontWeight: FontWeight.w600,
  307. color: cs.onSurface,
  308. ),
  309. ),
  310. const SizedBox(height: 8),
  311. TextFormField(
  312. controller: _passwordCtrl,
  313. obscureText: _obscurePassword,
  314. decoration: InputDecoration(
  315. hintText: '',
  316. prefixIcon: const Icon(Icons.lock_outline, size: 20),
  317. suffixIcon: IconButton(
  318. icon: Icon(
  319. _obscurePassword
  320. ? Icons.visibility_outlined
  321. : Icons.visibility_off_outlined,
  322. size: 20,
  323. ),
  324. onPressed: () {
  325. setState(() {
  326. _obscurePassword = !_obscurePassword;
  327. });
  328. },
  329. ),
  330. ),
  331. ),
  332. ],
  333. );
  334. }
  335. Widget _buildRememberRow(ThemeData theme, ColorScheme cs) {
  336. return Row(
  337. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  338. children: [
  339. Row(
  340. mainAxisSize: MainAxisSize.min,
  341. children: [
  342. SizedBox(
  343. height: 24,
  344. width: 24,
  345. child: Checkbox(
  346. value: _rememberMe,
  347. onChanged: (v) {
  348. setState(() {
  349. _rememberMe = v ?? false;
  350. });
  351. },
  352. materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
  353. ),
  354. ),
  355. const SizedBox(width: 8),
  356. Text(
  357. 'Ricordami',
  358. style: theme.textTheme.labelMedium?.copyWith(
  359. color: cs.onSurfaceVariant,
  360. ),
  361. ),
  362. ],
  363. ),
  364. TextButton(
  365. onPressed: () {},
  366. style: TextButton.styleFrom(
  367. padding: EdgeInsets.zero,
  368. minimumSize: Size.zero,
  369. tapTargetSize: MaterialTapTargetSize.shrinkWrap,
  370. ),
  371. child: Text(
  372. 'Password dimenticata?',
  373. style: theme.textTheme.labelMedium?.copyWith(color: cs.secondary),
  374. ),
  375. ),
  376. ],
  377. );
  378. }
  379. Widget _buildErrorCard(String error, ColorScheme cs) {
  380. return Container(
  381. padding: const EdgeInsets.all(12),
  382. decoration: BoxDecoration(
  383. color: cs.errorContainer,
  384. borderRadius: BorderRadius.circular(8),
  385. ),
  386. child: Row(
  387. children: [
  388. Icon(Icons.error_outline, color: cs.error, size: 20),
  389. const SizedBox(width: 8),
  390. Expanded(
  391. child: Text(
  392. error,
  393. style: TextStyle(color: cs.onErrorContainer, fontSize: 14),
  394. ),
  395. ),
  396. ],
  397. ),
  398. );
  399. }
  400. Widget _buildFooterLinks(ThemeData theme, ColorScheme cs) {
  401. return Row(
  402. mainAxisAlignment: MainAxisAlignment.center,
  403. children: [
  404. TextButton(
  405. onPressed: () {},
  406. style: TextButton.styleFrom(
  407. padding: const EdgeInsets.symmetric(horizontal: 12),
  408. minimumSize: Size.zero,
  409. tapTargetSize: MaterialTapTargetSize.shrinkWrap,
  410. ),
  411. child: Text(
  412. 'Termini di Servizio',
  413. style: theme.textTheme.labelMedium?.copyWith(
  414. color: cs.onSurfaceVariant,
  415. ),
  416. ),
  417. ),
  418. Padding(
  419. padding: const EdgeInsets.symmetric(horizontal: 4),
  420. child: Text(
  421. '\u00B7',
  422. style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12),
  423. ),
  424. ),
  425. TextButton(
  426. onPressed: () {},
  427. style: TextButton.styleFrom(
  428. padding: const EdgeInsets.symmetric(horizontal: 12),
  429. minimumSize: Size.zero,
  430. tapTargetSize: MaterialTapTargetSize.shrinkWrap,
  431. ),
  432. child: Text(
  433. 'Privacy Policy',
  434. style: theme.textTheme.labelMedium?.copyWith(
  435. color: cs.onSurfaceVariant,
  436. ),
  437. ),
  438. ),
  439. Padding(
  440. padding: const EdgeInsets.symmetric(horizontal: 4),
  441. child: Text(
  442. '\u00B7',
  443. style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12),
  444. ),
  445. ),
  446. TextButton(
  447. onPressed: () {},
  448. style: TextButton.styleFrom(
  449. padding: const EdgeInsets.symmetric(horizontal: 12),
  450. minimumSize: Size.zero,
  451. tapTargetSize: MaterialTapTargetSize.shrinkWrap,
  452. ),
  453. child: Text(
  454. 'Supporto',
  455. style: theme.textTheme.labelMedium?.copyWith(
  456. color: cs.onSurfaceVariant,
  457. ),
  458. ),
  459. ),
  460. ],
  461. );
  462. }
  463. }