login_page.dart 14 KB

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