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:
580
lib/views/bureau_inventory_page.dart
Normal file
580
lib/views/bureau_inventory_page.dart
Normal file
@@ -0,0 +1,580 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:inventory_app/constants.dart';
|
||||
import 'package:inventory_app/models/inventory_line.dart';
|
||||
import 'package:inventory_app/service/axelor_client.dart';
|
||||
import 'package:inventory_app/widgets/glass_widgets.dart';
|
||||
|
||||
class BureauInventoryPage extends StatefulWidget {
|
||||
final int locationId;
|
||||
final String locationName;
|
||||
|
||||
const BureauInventoryPage({
|
||||
super.key,
|
||||
required this.locationId,
|
||||
required this.locationName,
|
||||
});
|
||||
|
||||
@override
|
||||
State<BureauInventoryPage> createState() => _BureauInventoryPageState();
|
||||
}
|
||||
|
||||
class _BureauInventoryPageState extends State<BureauInventoryPage> {
|
||||
Future<List<InventoryLine>?>? _future;
|
||||
List<InventoryLine> _allLines = [];
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
String _query = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController.addListener(() {
|
||||
setState(() => _query = _searchController.text.trim().toLowerCase());
|
||||
});
|
||||
_load();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
final client = await AxelorClient.create();
|
||||
setState(() {
|
||||
_future = client.fetchInventoryLinesByLocation(widget.locationId).then((
|
||||
lines,
|
||||
) {
|
||||
_allLines = lines ?? [];
|
||||
return lines;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
List<InventoryLine> get _filteredLines {
|
||||
if (_query.isEmpty) return _allLines;
|
||||
return _allLines.where((line) {
|
||||
return line.productName.toLowerCase().contains(_query) ||
|
||||
line.ticketId.toLowerCase().contains(_query) ||
|
||||
(line.observation?.toLowerCase().contains(_query) ?? false) ||
|
||||
(line.description?.toLowerCase().contains(_query) ?? false);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
const FancyBackground(),
|
||||
SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
BackAppBar(
|
||||
title: widget.locationName,
|
||||
icon: Icons.business_rounded,
|
||||
),
|
||||
Expanded(
|
||||
child: FutureBuilder<List<InventoryLine>?>(
|
||||
future: _future,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (snapshot.hasError || snapshot.data == null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
color: Colors.redAccent,
|
||||
size: 48,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Impossible de charger les articles.',
|
||||
style: TextStyle(color: Colors.redAccent),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Réessayer'),
|
||||
onPressed: _load,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_allLines.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 64,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Aucun article trouvé dans ce bureau.',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey.shade600,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final filtered = _filteredLines;
|
||||
return Column(
|
||||
children: [
|
||||
_SearchBar(
|
||||
controller: _searchController,
|
||||
isDark: isDark,
|
||||
total: _allLines.length,
|
||||
shown: filtered.length,
|
||||
),
|
||||
Expanded(
|
||||
child:
|
||||
filtered.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
'Aucun résultat pour "${_searchController.text}"',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
)
|
||||
: RefreshIndicator(
|
||||
onRefresh: _load,
|
||||
child: ListView.builder(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
16,
|
||||
4,
|
||||
16,
|
||||
100,
|
||||
),
|
||||
itemCount: filtered.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _LineCard(
|
||||
line: filtered[index],
|
||||
isDark: isDark,
|
||||
onTap:
|
||||
() => _showDetails(
|
||||
context,
|
||||
filtered[index],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDetails(BuildContext context, InventoryLine line) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final textColor = isDark ? Colors.white : Colors.blue.shade700;
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
barrierColor: Colors.black.withValues(alpha: 0.3),
|
||||
isScrollControlled: true,
|
||||
builder: (_) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: GlassCard(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SheetHandle(Colors.blue),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Text(
|
||||
line.productName,
|
||||
style: kTextFormFieldStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue.shade700,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_detail('N° Ticket', line.ticketId, textColor),
|
||||
if (line.description != null && line.description!.isNotEmpty)
|
||||
_detail('État', line.description!, textColor),
|
||||
if (line.observation != null && line.observation!.isNotEmpty)
|
||||
_detail('N° Série', line.observation!, textColor),
|
||||
if (line.firstCounting != null && line.firstCounting! > 0)
|
||||
_detail(
|
||||
'Comptage 1',
|
||||
line.firstCounting!.toStringAsFixed(0),
|
||||
textColor,
|
||||
),
|
||||
if (line.secondCounting != null && line.secondCounting! > 0)
|
||||
_detail(
|
||||
'Comptage 2',
|
||||
line.secondCounting!.toStringAsFixed(0),
|
||||
textColor,
|
||||
),
|
||||
if (line.thirdCounting != null && line.thirdCounting! > 0)
|
||||
_detail(
|
||||
'Comptage 3',
|
||||
line.thirdCounting!.toStringAsFixed(0),
|
||||
textColor,
|
||||
),
|
||||
if (line.firstCountingDate != null)
|
||||
_detail(
|
||||
'Date C1',
|
||||
_formatDate(line.firstCountingDate!),
|
||||
textColor,
|
||||
),
|
||||
if (line.firstCountingByUser != null)
|
||||
_detail(
|
||||
'Agent C1',
|
||||
line.firstCountingByUser!['fullName'] ?? '-',
|
||||
textColor,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(
|
||||
Icons.close_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
label: const Text(
|
||||
'Fermer',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _detail(String label, String value, Color textColor) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 110,
|
||||
child: Text(
|
||||
label,
|
||||
style: kTextFormFieldStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: kTextFormFieldStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDate(String iso) {
|
||||
try {
|
||||
final dt = DateTime.parse(iso).toLocal();
|
||||
return '${dt.day.toString().padLeft(2, '0')}/${dt.month.toString().padLeft(2, '0')}/${dt.year}';
|
||||
} catch (_) {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Search bar ───────────────────────────────────────────────────────────────
|
||||
|
||||
class _SearchBar extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
final bool isDark;
|
||||
final int total;
|
||||
final int shown;
|
||||
|
||||
const _SearchBar({
|
||||
required this.controller,
|
||||
required this.isDark,
|
||||
required this.total,
|
||||
required this.shown,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
style: TextStyle(color: isDark ? Colors.white : Colors.black87),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Rechercher par nom, ticket, état...',
|
||||
hintStyle: TextStyle(
|
||||
color: isDark ? Colors.white38 : Colors.black38,
|
||||
fontSize: 14,
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search_rounded,
|
||||
color: isDark ? Colors.white54 : Colors.black45,
|
||||
),
|
||||
suffixIcon:
|
||||
controller.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
Icons.close_rounded,
|
||||
color: isDark ? Colors.white54 : Colors.black45,
|
||||
),
|
||||
onPressed: () => controller.clear(),
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Text(
|
||||
'$total articles',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isDark ? Colors.white38 : Colors.black38,
|
||||
),
|
||||
),
|
||||
),
|
||||
filled: true,
|
||||
fillColor:
|
||||
isDark
|
||||
? Colors.white.withValues(alpha: 0.08)
|
||||
: Colors.white.withValues(alpha: 0.75),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Card for each inventory line ─────────────────────────────────────────────
|
||||
|
||||
class _LineCard extends StatelessWidget {
|
||||
final InventoryLine line;
|
||||
final bool isDark;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _LineCard({
|
||||
required this.line,
|
||||
required this.isDark,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textColor = isDark ? Colors.white : Colors.blue.shade700;
|
||||
final subColor = isDark ? Colors.white70 : Colors.blue.shade400;
|
||||
|
||||
final qty =
|
||||
(line.thirdCounting ?? 0) > 0
|
||||
? line.thirdCounting!
|
||||
: (line.secondCounting ?? 0) > 0
|
||||
? line.secondCounting!
|
||||
: (line.firstCounting ?? 0);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: GlassCard(
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
onTap: () {
|
||||
HapticFeedback.selectionClick();
|
||||
onTap();
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withValues(alpha: 0.12),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.inventory_2_rounded,
|
||||
color: Colors.blue,
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
line.productName,
|
||||
style: kTextFormFieldStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (line.ticketId.isNotEmpty)
|
||||
Text(
|
||||
'Ticket: ${line.ticketId}',
|
||||
style: kTextFormFieldStyle(
|
||||
fontSize: 12.0,
|
||||
color: subColor,
|
||||
),
|
||||
),
|
||||
if (line.description != null &&
|
||||
line.description!.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(
|
||||
'État: ${line.description}',
|
||||
style: kTextFormFieldStyle(
|
||||
fontSize: 12.0,
|
||||
color: subColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (line.observation != null &&
|
||||
line.observation!.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(
|
||||
'N° Série: ${line.observation}',
|
||||
style: kTextFormFieldStyle(
|
||||
fontSize: 12.0,
|
||||
color: subColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
children: [
|
||||
if ((line.firstCounting ?? 0) > 0)
|
||||
_CountChip(
|
||||
label:
|
||||
'C1: ${line.firstCounting!.toStringAsFixed(0)}',
|
||||
),
|
||||
if ((line.secondCounting ?? 0) > 0)
|
||||
_CountChip(
|
||||
label:
|
||||
'C2: ${line.secondCounting!.toStringAsFixed(0)}',
|
||||
color: Colors.orange,
|
||||
),
|
||||
if ((line.thirdCounting ?? 0) > 0)
|
||||
_CountChip(
|
||||
label:
|
||||
'C3: ${line.thirdCounting!.toStringAsFixed(0)}',
|
||||
color: Colors.green,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
qty.toStringAsFixed(0),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CountChip extends StatelessWidget {
|
||||
final String label;
|
||||
final Color color;
|
||||
|
||||
const _CountChip({required this.label, this.color = Colors.blue});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: color.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
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)],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
264
lib/views/my_scans.dart
Normal file
264
lib/views/my_scans.dart
Normal file
@@ -0,0 +1,264 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:inventory_app/constants.dart';
|
||||
import 'package:inventory_app/models/inventory_line.dart';
|
||||
import 'package:inventory_app/service/axelor_client.dart';
|
||||
import 'package:inventory_app/widgets/glass_widgets.dart';
|
||||
|
||||
class MyScans extends StatefulWidget {
|
||||
const MyScans({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<MyScans> createState() => _MyScansState();
|
||||
}
|
||||
|
||||
class _MyScansState extends State<MyScans> {
|
||||
Future<List<InventoryLine>?>? _futureInventoryLines;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadInventoryLines();
|
||||
}
|
||||
|
||||
Future<void> _loadInventoryLines() async {
|
||||
final client = await AxelorClient.create();
|
||||
setState(() {
|
||||
_futureInventoryLines = client.getMyInventoryLines();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final MaterialColor primary = Colors.blue;
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
const FancyBackground(),
|
||||
SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
BackAppBar(title: '📦 Mes Scans', icon: Icons.inventory_2_rounded),
|
||||
Expanded(
|
||||
child: FutureBuilder<List<InventoryLine>?>(
|
||||
future: _futureInventoryLines,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'❌ Erreur: ${snapshot.error}',
|
||||
style: const TextStyle(color: Colors.redAccent),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final lines = snapshot.data ?? [];
|
||||
if (lines.isEmpty) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
'Aucun scan trouvé.',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: _loadInventoryLines,
|
||||
child: ListView.builder(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 100),
|
||||
itemCount: lines.length,
|
||||
itemBuilder: (context, index) {
|
||||
final line = lines[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: GlassCard(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: primary.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.inventory_2_rounded,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
"${line.product!.code!}-${line.productName}",
|
||||
style: kTextFormFieldStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color:
|
||||
isDark
|
||||
? Colors.white
|
||||
: primary.shade700,
|
||||
),
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Stock ID: ${line.id}",
|
||||
style: kTextFormFieldStyle(
|
||||
fontSize: 13.0,
|
||||
color:
|
||||
isDark
|
||||
? Colors.white
|
||||
: primary.shade700,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"Comptage: ${line.countingTypeSelect}",
|
||||
style: kTextFormFieldStyle(
|
||||
fontSize: 13.0,
|
||||
color:
|
||||
isDark
|
||||
? Colors.white
|
||||
: primary.shade700,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"Quantité Réelle: ${line.realQty}",
|
||||
style: kTextFormFieldStyle(
|
||||
fontSize: 13.0,
|
||||
color:
|
||||
isDark
|
||||
? Colors.white
|
||||
: primary.shade700,
|
||||
),
|
||||
),
|
||||
if (line.observation != null &&
|
||||
line.observation!.isNotEmpty)
|
||||
Text(
|
||||
"Observation: ${line.observation}",
|
||||
style: kTextFormFieldStyle(
|
||||
fontSize: 13.0,
|
||||
color:
|
||||
isDark
|
||||
? Colors.white
|
||||
: primary.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
HapticFeedback.selectionClick();
|
||||
_showLineDetails(context, line);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showLineDetails(BuildContext context, InventoryLine line) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
barrierColor: Colors.black.withValues(alpha: 0.3),
|
||||
builder: (_) {
|
||||
return GlassBottomSheet(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SheetHandle(Colors.blue),
|
||||
Text(
|
||||
line.productName,
|
||||
style: kTextFormFieldStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue.shade700,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
_lineDetail("ID", line.id?.toString() ?? "-", context),
|
||||
_lineDetail("Inventaire", "${line.inventoryId}", context),
|
||||
_lineDetail(
|
||||
"Type de comptage",
|
||||
"${line.countingTypeSelect}",
|
||||
context,
|
||||
),
|
||||
if (line.firstCountingDate != null)
|
||||
_lineDetail("1er comptage", line.firstCountingDate!, context),
|
||||
if (line.secondCountingDate != null)
|
||||
_lineDetail("2ème comptage", line.secondCountingDate!, context),
|
||||
if (line.thirdCountingDate != null)
|
||||
_lineDetail("3ème comptage", line.thirdCountingDate!, context),
|
||||
SizedBox(height: 14),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: ElevatedButton.icon(
|
||||
icon: Icon(Icons.close_rounded, color: Colors.white),
|
||||
label: Text("Fermer"),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _lineDetail(String label, String value, BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final MaterialColor primary = Colors.blue;
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: kTextFormFieldStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isDark ? Colors.white : primary.shade700,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: kTextFormFieldStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.white : primary.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
89
lib/views/panoramic.dart
Normal file
89
lib/views/panoramic.dart
Normal file
@@ -0,0 +1,89 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:inventory_app/constants.dart';
|
||||
import 'package:inventory_app/controllers/home_controller.dart';
|
||||
import 'package:panorama_viewer/panorama_viewer.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class Panoramic extends StatefulWidget {
|
||||
const Panoramic({super.key});
|
||||
|
||||
@override
|
||||
State<Panoramic> createState() => _PanoramicState();
|
||||
}
|
||||
|
||||
class _PanoramicState extends State<Panoramic> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = Get.put(HomeController());
|
||||
Size size = MediaQuery.of(context).size;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Obx(() {
|
||||
final depot = controller.selectedDepot.value;
|
||||
if (depot?.picture != null) {
|
||||
final pictureId = depot!.picture;
|
||||
return FutureBuilder<Uint8List?>(
|
||||
future: controller.fetchImageBytes(pictureId!.id!),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Center(
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
} else if (snapshot.hasData) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: PanoramaViewer(
|
||||
child: Image.memory(
|
||||
snapshot.data!,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(12.0),
|
||||
child: Text('❌ Failed to load depot image'),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink(); // No image to show
|
||||
}
|
||||
}),
|
||||
// PanoramaViewer(child: Image.asset("assets/desk.jpg")),
|
||||
Positioned(
|
||||
top: 50,
|
||||
left: 10,
|
||||
child: Container(
|
||||
width: size.width - 20,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(20),
|
||||
bottomLeft: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Obx(() {
|
||||
return Center(
|
||||
child: Text( controller.selectedDepot.value != null ?
|
||||
controller.selectedDepot.value!.name.toString() : "Loading .....",
|
||||
style: kLoginTitleStyle(Size(size.width, size.height * 0.3),color: Colors.blue),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
579
lib/views/product_view.dart
Normal file
579
lib/views/product_view.dart
Normal file
@@ -0,0 +1,579 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:inventory_app/models/famille_produit.dart';
|
||||
import 'package:inventory_app/models/product.dart';
|
||||
import 'package:inventory_app/service/axelor_client.dart';
|
||||
import 'package:inventory_app/controllers/home_controller.dart';
|
||||
import 'package:inventory_app/service/product_image_updater.dart';
|
||||
import 'package:inventory_app/widgets/glass_widgets.dart';
|
||||
import 'package:quickalert/quickalert.dart';
|
||||
import 'package:inventory_app/utils.dart';
|
||||
|
||||
class CreateProductPage extends StatefulWidget {
|
||||
const CreateProductPage({super.key});
|
||||
|
||||
@override
|
||||
State<CreateProductPage> createState() => _CreateProductPageState();
|
||||
}
|
||||
|
||||
class _CreateProductPageState extends State<CreateProductPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
// ----- Controllers -----
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _nameCtrl = TextEditingController();
|
||||
final _serialCtrl = TextEditingController();
|
||||
final _obsCtrl = TextEditingController();
|
||||
|
||||
// ----- State -----
|
||||
final MaterialColor primary = Colors.blue;
|
||||
String _selectedState = 'Neuf';
|
||||
bool _saving = false;
|
||||
|
||||
// Success animation controller
|
||||
late final AnimationController _animCtrl;
|
||||
late final Animation<double> _scale;
|
||||
|
||||
late AxelorClient client;
|
||||
late final HomeController controller;
|
||||
Product? _createdProduct;
|
||||
|
||||
Future<List<FamilleProduit>?> getFamilleProduit() async {
|
||||
client = await AxelorClient.create();
|
||||
return client.getFamilleProduit();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animCtrl = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 420),
|
||||
);
|
||||
_scale = CurvedAnimation(parent: _animCtrl, curve: Curves.easeOutBack);
|
||||
controller = Get.find<HomeController>();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
controller.fetchFamilleProduits();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameCtrl.dispose();
|
||||
_serialCtrl.dispose();
|
||||
_obsCtrl.dispose();
|
||||
_animCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// ----- UI -----
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
const FancyBackground(),
|
||||
SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
BackAppBar(
|
||||
title: 'Créer un article',
|
||||
icon: Icons.inventory_2_rounded,
|
||||
),
|
||||
|
||||
SectionHeader(
|
||||
icon: Icons.inventory_2_rounded,
|
||||
title: 'Informations de base',
|
||||
accent: primary,
|
||||
),
|
||||
GlassCard(
|
||||
child: Column(
|
||||
children: [
|
||||
FancyTextField(
|
||||
controller: _nameCtrl,
|
||||
label: "Nom de l'article",
|
||||
hint: 'Ex: HP LaserJet Pro M404',
|
||||
icon: Icons.badge_rounded,
|
||||
validator:
|
||||
(v) =>
|
||||
v == null || v.trim().isEmpty
|
||||
? 'Product name is required'
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Famille Produit
|
||||
Obx(() {
|
||||
if (controller.isLoading.value &&
|
||||
controller.familleProduits.isEmpty) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
return _FancyDropdown<FamilleProduit>(
|
||||
label: "Famille d'article",
|
||||
value: controller.selectedFamilleProduit.value,
|
||||
icon: Icons.category_rounded,
|
||||
items: controller.familleProduits,
|
||||
onChanged: controller.onFamilleSelected,
|
||||
validator:
|
||||
(v) =>
|
||||
v == null
|
||||
? 'Sélectionnez une famille'
|
||||
: null,
|
||||
);
|
||||
}),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Obx(() {
|
||||
if (controller.isLoading.value &&
|
||||
controller.sousFamilleProduits.isEmpty) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final items = controller.sousFamilleProduits;
|
||||
return _FancyDropdown<FamilleProduit>(
|
||||
label: "Sous-Famille d'article",
|
||||
value:
|
||||
controller.selectedSousFamilleProduit.value,
|
||||
icon: Icons.subdirectory_arrow_right_rounded,
|
||||
items: items,
|
||||
onChanged:
|
||||
items.isEmpty
|
||||
? null
|
||||
: (val) =>
|
||||
controller
|
||||
.selectedSousFamilleProduit
|
||||
.value = val,
|
||||
validator:
|
||||
(v) =>
|
||||
v == null
|
||||
? 'Sélectionnez une sous-famille'
|
||||
: null,
|
||||
);
|
||||
}),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
_FancyDropdown<String>(
|
||||
label: 'Etat',
|
||||
value: _selectedState,
|
||||
icon: Icons.fact_check_rounded,
|
||||
items: const [
|
||||
'Ancien',
|
||||
'Moyen',
|
||||
'Neuf',
|
||||
'Réformée',
|
||||
],
|
||||
onChanged: (val) {
|
||||
if (val != null) {
|
||||
HapticFeedback.selectionClick();
|
||||
setState(() => _selectedState = val);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
SectionHeader(
|
||||
icon: Icons.confirmation_number_rounded,
|
||||
title: 'Identification',
|
||||
accent: primary,
|
||||
),
|
||||
GlassCard(
|
||||
child: Column(
|
||||
children: [
|
||||
FancyTextField(
|
||||
controller: _serialCtrl,
|
||||
label: 'N° de serie',
|
||||
hint: 'e.g. SN-ABC-123456',
|
||||
icon: Icons.numbers_rounded,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
FancyTextField(
|
||||
controller: _obsCtrl,
|
||||
label: 'Observation',
|
||||
hint: 'Notes supplémentaires (optionnel)',
|
||||
icon: Icons.notes_rounded,
|
||||
maxLines: 4,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Buttons Row
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _PrimaryButton(
|
||||
text: _saving ? 'Enregistrement...' : 'Sauvegarder',
|
||||
icon: Icons.save_rounded,
|
||||
primary: primary,
|
||||
onPressed: _saving ? null : _onSave,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_CameraButton(primary: primary, onTap: _onTakeImage),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_saving) const _SavingOverlay(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ----- Actions -----
|
||||
|
||||
Future<void> _onSave() async {
|
||||
HapticFeedback.vibrate();
|
||||
client = await AxelorClient.create();
|
||||
if (!mounted) return;
|
||||
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
QuickAlert.show(
|
||||
context: context,
|
||||
type: QuickAlertType.error,
|
||||
title: 'Validation',
|
||||
text: 'Veuillez remplir tous les champs obligatoires.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (controller.selectedFamilleProduit.value?.id == null ||
|
||||
controller.selectedSousFamilleProduit.value?.id == null) {
|
||||
QuickAlert.show(
|
||||
context: context,
|
||||
type: QuickAlertType.error,
|
||||
title: 'Missing',
|
||||
text: 'Sélectionnez la famille et la sous-famille.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _saving = true);
|
||||
|
||||
try {
|
||||
final product = Product(
|
||||
name: _nameCtrl.text.trim(),
|
||||
familleProduit: controller.selectedFamilleProduit.value?.id,
|
||||
sousFamilleProduit: controller.selectedSousFamilleProduit.value?.id,
|
||||
productTypeSelect: 'storable',
|
||||
internalDescription: _serialCtrl.text.trim(),
|
||||
description: _obsCtrl.text.trim(),
|
||||
unit: Unit(id: 4),
|
||||
purchaseUnit: Unit(id: 4),
|
||||
procurementMethodSelect: 'buy',
|
||||
);
|
||||
|
||||
_createdProduct = await client.createProducts(
|
||||
product,
|
||||
controller.selectedDepot.value,
|
||||
);
|
||||
if (!mounted) return;
|
||||
|
||||
if (_createdProduct != null) {
|
||||
QuickAlert.show(
|
||||
context: context,
|
||||
type: QuickAlertType.success,
|
||||
title: 'Succès',
|
||||
text:
|
||||
"L'article ${_nameCtrl.text.trim()} a été créé avec succès !.",
|
||||
);
|
||||
await _showSuccessAnimation();
|
||||
} else {
|
||||
if (!mounted) return;
|
||||
QuickAlert.show(
|
||||
context: context,
|
||||
type: QuickAlertType.error,
|
||||
title: 'Erreur',
|
||||
text: "L'article ${_nameCtrl.text.trim()} existe déjà.",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
QuickAlert.show(
|
||||
context: context,
|
||||
type: QuickAlertType.error,
|
||||
title: 'Erreur',
|
||||
text: "Échec de la création de l'article: $e",
|
||||
);
|
||||
} finally {
|
||||
if (mounted) setState(() => _saving = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onTakeImage() async {
|
||||
HapticFeedback.lightImpact();
|
||||
client = await AxelorClient.create();
|
||||
if (!mounted) return;
|
||||
|
||||
if (!_formKey.currentState!.validate() ||
|
||||
controller.selectedFamilleProduit.value?.id == null ||
|
||||
controller.selectedSousFamilleProduit.value?.id == null) {
|
||||
QuickAlert.show(
|
||||
context: context,
|
||||
type: QuickAlertType.info,
|
||||
title: 'Info',
|
||||
text: 'Please create the product first (fill required fields).',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final createdProduct = _createdProduct;
|
||||
if (createdProduct == null) {
|
||||
QuickAlert.show(
|
||||
context: context,
|
||||
type: QuickAlertType.loading,
|
||||
title: "Create product first",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final updater = ProductImageUpdater(
|
||||
baseUrl: Utils.url,
|
||||
);
|
||||
await updater.updateProductPictureFlow(context, createdProduct);
|
||||
if (!mounted) return;
|
||||
|
||||
QuickAlert.show(
|
||||
context: context,
|
||||
type: QuickAlertType.success,
|
||||
title: 'Image Updated',
|
||||
text: 'Product image captured and uploaded!',
|
||||
);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
QuickAlert.show(
|
||||
context: context,
|
||||
type: QuickAlertType.error,
|
||||
title: 'Upload Failed',
|
||||
text: 'Could not upload image: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showSuccessAnimation() async {
|
||||
await _animCtrl.forward();
|
||||
if (!mounted) return;
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
barrierColor: Colors.black.withValues(alpha: 0.15),
|
||||
builder:
|
||||
(_) => Center(
|
||||
child: ScaleTransition(
|
||||
scale: _scale,
|
||||
child: _SuccessBadge(primary: primary),
|
||||
),
|
||||
),
|
||||
);
|
||||
await Future.delayed(const Duration(milliseconds: 420));
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
_animCtrl.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------- UI Pieces -----------------
|
||||
|
||||
class _FancyDropdown<T> extends StatelessWidget {
|
||||
final String label;
|
||||
final T? value;
|
||||
final List<T> items;
|
||||
final IconData icon;
|
||||
final void Function(T?)? onChanged;
|
||||
final String? Function(T?)? validator;
|
||||
|
||||
const _FancyDropdown({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.items,
|
||||
required this.icon,
|
||||
this.onChanged,
|
||||
this.validator,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final border = OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: BorderSide.none,
|
||||
);
|
||||
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
return DropdownButtonFormField<T>(
|
||||
value: value,
|
||||
isExpanded: true,
|
||||
dropdownColor:
|
||||
isDark
|
||||
? const Color(0xFF1E1E1E)
|
||||
: Colors.white.withValues(alpha: 0.95),
|
||||
items:
|
||||
items
|
||||
.map(
|
||||
(e) => DropdownMenuItem<T>(
|
||||
value: e,
|
||||
child: Text(
|
||||
'$e',
|
||||
style: TextStyle(
|
||||
color:
|
||||
isDark
|
||||
? Colors.white.withValues(alpha: 0.9)
|
||||
: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: onChanged,
|
||||
validator: validator,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
prefixIcon: Icon(icon, color: isDark ? Colors.white70 : Colors.black54),
|
||||
filled: true,
|
||||
fillColor:
|
||||
isDark
|
||||
? Colors.white.withValues(alpha: 0.08)
|
||||
: Colors.white.withValues(alpha: 0.75),
|
||||
enabledBorder: border,
|
||||
focusedBorder: border,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PrimaryButton extends StatelessWidget {
|
||||
final String text;
|
||||
final IconData icon;
|
||||
final MaterialColor primary;
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
const _PrimaryButton({
|
||||
required this.text,
|
||||
required this.icon,
|
||||
required this.primary,
|
||||
this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
opacity: onPressed == null ? 0.6 : 1,
|
||||
child: ElevatedButton.icon(
|
||||
icon: Icon(icon, color: Colors.white),
|
||||
label: Text(text, style: const TextStyle(color: Colors.white)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: primary,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
elevation: 8,
|
||||
shadowColor: primary.withValues(alpha: 0.35),
|
||||
),
|
||||
onPressed: onPressed,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CameraButton extends StatelessWidget {
|
||||
final MaterialColor primary;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _CameraButton({required this.primary, required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: null,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [primary.shade400, primary.shade900],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: primary.withValues(alpha: 0.28),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.camera_alt_rounded, color: Colors.white),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SavingOverlay extends StatelessWidget {
|
||||
const _SavingOverlay();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IgnorePointer(
|
||||
child: Container(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
child: const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SuccessBadge extends StatelessWidget {
|
||||
final MaterialColor primary;
|
||||
|
||||
const _SuccessBadge({required this.primary});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 132,
|
||||
height: 132,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(colors: [primary.shade400, primary.shade600]),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: primary.withValues(alpha: 0.35),
|
||||
blurRadius: 28,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: const Icon(Icons.check_rounded, size: 64, color: Colors.white),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
209
lib/views/profile.dart
Normal file
209
lib/views/profile.dart
Normal file
@@ -0,0 +1,209 @@
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:inventory_app/controllers/user_controller.dart';
|
||||
import 'package:inventory_app/home.dart';
|
||||
import 'package:inventory_app/widgets/glass_widgets.dart';
|
||||
import 'package:inventory_app/service/axelor_client.dart';
|
||||
import 'package:inventory_app/controllers/theme_controller.dart';
|
||||
import '../constants.dart';
|
||||
|
||||
class ProfilePage extends StatefulWidget {
|
||||
const ProfilePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ProfilePage> createState() => _ProfilePageState();
|
||||
}
|
||||
|
||||
class _ProfilePageState extends State<ProfilePage> {
|
||||
Map<String, dynamic>? userData;
|
||||
bool loading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Get.find<UserController>().loadUser();
|
||||
|
||||
// loadProfile();
|
||||
}
|
||||
|
||||
Future<void> loadProfile() async {
|
||||
setState(() => loading = true);
|
||||
try {
|
||||
final client = await AxelorClient.create();
|
||||
final data = await client.fetchUserProfile();
|
||||
|
||||
setState(() {
|
||||
userData = data;
|
||||
loading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() => loading = false);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text("Failed to load profile: $e")));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final userController = Get.find<UserController>();
|
||||
final primary = Colors.blue;
|
||||
|
||||
return Scaffold(
|
||||
drawer: FancyDrawer(primary: primary, onLogout: () {}),
|
||||
body: Stack(
|
||||
children: [
|
||||
const FancyBackground(),
|
||||
|
||||
Obx(() {
|
||||
if (userController.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final user = userController.user.value;
|
||||
if (user == null) {
|
||||
return const Center(
|
||||
child: Text("Impossible de charger le profil."),
|
||||
);
|
||||
}
|
||||
|
||||
return _buildContent(primary, user);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(MaterialColor primary, Map<String, dynamic> userData) {
|
||||
final themeController = Get.find<ThemeController>();
|
||||
final isDark = themeController.isDarkMode.value;
|
||||
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
GlassAppBar(
|
||||
title: "Mon Profil",
|
||||
primary: primary,
|
||||
onLogout: () {},
|
||||
onCamera: null,
|
||||
),
|
||||
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.fromLTRB(16, 6, 16, 50),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SectionHeader(
|
||||
icon: Icons.person_rounded,
|
||||
title: "Informations personnelles",
|
||||
accent: primary,
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
GlassCard(
|
||||
child: Column(
|
||||
children: [
|
||||
_profileField(
|
||||
icon: Icons.person,
|
||||
label: "Nom complet",
|
||||
value: userData?["fullName"],
|
||||
color: isDark ? Colors.white : primary.shade700,
|
||||
),
|
||||
_profileField(
|
||||
icon: Icons.badge_rounded,
|
||||
label: "Login",
|
||||
value: userData?["code"],
|
||||
color: isDark ? Colors.white : primary.shade700,
|
||||
),
|
||||
_profileField(
|
||||
icon: Icons.email_rounded,
|
||||
label: "Email",
|
||||
value: userData?["email"],
|
||||
color: isDark ? Colors.white : primary.shade700,
|
||||
),
|
||||
_profileField(
|
||||
icon: Icons.language_rounded,
|
||||
label: "Langue",
|
||||
value: userData?["language"],
|
||||
color: isDark ? Colors.white : primary.shade700,
|
||||
),
|
||||
_profileField(
|
||||
icon: Icons.group,
|
||||
label: "Groupe",
|
||||
value: userData?["group"]?["name"],
|
||||
color: isDark ? Colors.white : primary.shade700,
|
||||
),
|
||||
_profileField(
|
||||
icon: Icons.lock_person_rounded,
|
||||
label: "Bloqué",
|
||||
value: userData?["blocked"] == true ? "Oui" : "Non",
|
||||
color: isDark ? Colors.white : primary.shade700,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 25),
|
||||
|
||||
Center(
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(
|
||||
Icons.refresh_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
label: const Text(
|
||||
"Actualiser",
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: primary,
|
||||
elevation: 8,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
HapticFeedback.lightImpact();
|
||||
Get.find<UserController>().loadUser(); // ✅ correct
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _profileField({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required String? value,
|
||||
required Color color,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: Colors.blue),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"$label : ${value ?? 'N/A'}",
|
||||
style: kTextFormFieldStyle(fontSize: 16.0, color: color),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
182
lib/views/qr_view.dart
Normal file
182
lib/views/qr_view.dart
Normal file
@@ -0,0 +1,182 @@
|
||||
import 'dart:io';
|
||||
import 'dart:ui' show ImageFilter;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
|
||||
import 'package:inventory_app/constants.dart';
|
||||
|
||||
class QRViewExample extends StatefulWidget {
|
||||
const QRViewExample({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _QRViewExampleState();
|
||||
}
|
||||
|
||||
class _QRViewExampleState extends State<QRViewExample> {
|
||||
Barcode? result;
|
||||
QRViewController? controller;
|
||||
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
|
||||
|
||||
@override
|
||||
void reassemble() {
|
||||
super.reassemble();
|
||||
if (Platform.isAndroid) {
|
||||
controller!.pauseCamera();
|
||||
}
|
||||
controller!.resumeCamera();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
Expanded(flex: 4, child: _buildQrView(context)),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.white.withOpacity(0.08),
|
||||
Colors.white.withOpacity(0.04),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.12)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
if (result != null)
|
||||
Text(
|
||||
'Barcode Type: ${describeEnum(result!.format)} Data: ${result!.code}',
|
||||
style: kTextFormFieldStyle(),
|
||||
)
|
||||
else
|
||||
Text('Scan a code', style: kTextFormFieldStyle()),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
_glassButton(
|
||||
child: FutureBuilder(
|
||||
future: controller?.getFlashStatus(),
|
||||
builder: (context, snapshot) =>
|
||||
Text('Flash: ${snapshot.data}'),
|
||||
),
|
||||
onPressed: () async {
|
||||
await controller?.toggleFlash();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
_glassButton(
|
||||
child: FutureBuilder(
|
||||
future: controller?.getCameraInfo(),
|
||||
builder: (context, snapshot) =>
|
||||
snapshot.data != null
|
||||
? Text('Camera facing ${describeEnum(snapshot.data!)}')
|
||||
: const Text('loading'),
|
||||
),
|
||||
onPressed: () async {
|
||||
await controller?.flipCamera();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
_glassButton(
|
||||
child: const Text('pause', style: TextStyle(fontSize: 20)),
|
||||
onPressed: () async => controller?.pauseCamera(),
|
||||
),
|
||||
_glassButton(
|
||||
child: const Text('resume', style: TextStyle(fontSize: 20)),
|
||||
onPressed: () async => controller?.resumeCamera(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _glassButton({required Widget child, required VoidCallback onPressed}) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(8),
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
backgroundColor: Colors.white.withOpacity(0.06),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(color: Colors.white.withOpacity(0.12)),
|
||||
),
|
||||
),
|
||||
onPressed: onPressed,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQrView(BuildContext context) {
|
||||
final scanArea =
|
||||
(MediaQuery.of(context).size.width < 500 ||
|
||||
MediaQuery.of(context).size.height < 500)
|
||||
? 300.0
|
||||
: 450.0;
|
||||
return QRView(
|
||||
key: qrKey,
|
||||
onQRViewCreated: _onQRViewCreated,
|
||||
overlay: QrScannerOverlayShape(
|
||||
borderColor: Colors.blue,
|
||||
borderRadius: 10,
|
||||
borderLength: 30,
|
||||
borderWidth: 10,
|
||||
cutOutSize: scanArea,
|
||||
),
|
||||
onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p),
|
||||
);
|
||||
}
|
||||
|
||||
void _onQRViewCreated(QRViewController controller) {
|
||||
setState(() => this.controller = controller);
|
||||
controller.scannedDataStream.listen((scanData) {
|
||||
setState(() => result = scanData);
|
||||
Get.back(result: result!.code);
|
||||
});
|
||||
}
|
||||
|
||||
void _onPermissionSet(BuildContext context, QRViewController ctrl, bool p) {
|
||||
if (!p) Get.snackbar("Permission", 'no Permission');
|
||||
}
|
||||
}
|
||||
302
lib/views/sign_up_view.dart
Normal file
302
lib/views/sign_up_view.dart
Normal file
@@ -0,0 +1,302 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'package:inventory_app/controllers/simple_ui_controller.dart';
|
||||
|
||||
import '../views/login_view.dart';
|
||||
import '../constants.dart';
|
||||
|
||||
|
||||
class SignUpView extends StatefulWidget {
|
||||
const SignUpView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<SignUpView> createState() => _SignUpViewState();
|
||||
}
|
||||
|
||||
class _SignUpViewState extends State<SignUpView> {
|
||||
TextEditingController nameController = TextEditingController();
|
||||
TextEditingController emailController = TextEditingController();
|
||||
TextEditingController passwordController = TextEditingController();
|
||||
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
nameController.dispose();
|
||||
emailController.dispose();
|
||||
passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
SimpleUIController simpleUIController = Get.put(SimpleUIController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var size = MediaQuery.of(context).size;
|
||||
var theme = Theme.of(context);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth > 600) {
|
||||
return _buildLargeScreen(size, simpleUIController, theme);
|
||||
} else {
|
||||
return _buildSmallScreen(size, simpleUIController, theme);
|
||||
}
|
||||
},
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
/// For large screens
|
||||
Widget _buildLargeScreen(
|
||||
Size size, SimpleUIController simpleUIController, ThemeData theme) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: RotatedBox(
|
||||
quarterTurns: 3,
|
||||
child: Lottie.asset(
|
||||
'assets/coin.json',
|
||||
height: size.height * 0.3,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: size.width * 0.06),
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: _buildMainBody(size, simpleUIController, theme),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// For Small screens
|
||||
Widget _buildSmallScreen(
|
||||
Size size, SimpleUIController simpleUIController, ThemeData theme) {
|
||||
return Center(
|
||||
child: _buildMainBody(size, simpleUIController, theme),
|
||||
);
|
||||
}
|
||||
|
||||
/// Main Body
|
||||
Widget _buildMainBody(
|
||||
Size size, SimpleUIController simpleUIController, ThemeData theme) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment:
|
||||
size.width > 600 ? MainAxisAlignment.center : MainAxisAlignment.start,
|
||||
children: [
|
||||
size.width > 600
|
||||
? Container()
|
||||
: Lottie.asset(
|
||||
'assets/wave.json',
|
||||
height: size.height * 0.2,
|
||||
width: size.width,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
SizedBox(
|
||||
height: size.height * 0.03,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 20.0),
|
||||
child: Text(
|
||||
'Sign Up',
|
||||
style: kLoginTitleStyle(size),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 20.0),
|
||||
child: Text(
|
||||
'Create Account',
|
||||
style: kLoginSubtitleStyle(size),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: size.height * 0.03,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 20.0, right: 20),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
/// username
|
||||
TextFormField(
|
||||
style: kTextFormFieldStyle(),
|
||||
decoration: const InputDecoration(
|
||||
prefixIcon: Icon(Icons.person),
|
||||
hintText: 'Username',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(15)),
|
||||
),
|
||||
),
|
||||
|
||||
controller: nameController,
|
||||
// The validator receives the text that the user has entered.
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter username';
|
||||
} else if (value.length < 4) {
|
||||
return 'at least enter 4 characters';
|
||||
} else if (value.length > 13) {
|
||||
return 'maximum character is 13';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
height: size.height * 0.02,
|
||||
),
|
||||
|
||||
/// Gmail
|
||||
TextFormField(
|
||||
style: kTextFormFieldStyle(),
|
||||
controller: emailController,
|
||||
decoration: const InputDecoration(
|
||||
prefixIcon: Icon(Icons.email_rounded),
|
||||
hintText: 'gmail',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(15)),
|
||||
),
|
||||
),
|
||||
// The validator receives the text that the user has entered.
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter gmail';
|
||||
} else if (!value.endsWith('@gmail.com')) {
|
||||
return 'please enter valid gmail';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
height: size.height * 0.02,
|
||||
),
|
||||
|
||||
/// password
|
||||
Obx(
|
||||
() => TextFormField(
|
||||
style: kTextFormFieldStyle(),
|
||||
controller: passwordController,
|
||||
obscureText: simpleUIController.isObscure.value,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.lock_open),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
simpleUIController.isObscure.value
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
),
|
||||
onPressed: () {
|
||||
simpleUIController.isObscureActive();
|
||||
},
|
||||
),
|
||||
hintText: 'Password',
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(15)),
|
||||
),
|
||||
),
|
||||
// The validator receives the text that the user has entered.
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter some text';
|
||||
} else if (value.length < 7) {
|
||||
return 'at least enter 6 characters';
|
||||
} else if (value.length > 13) {
|
||||
return 'maximum character is 13';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: size.height * 0.01,
|
||||
),
|
||||
Text(
|
||||
'Creating an account means you\'re okay with our Terms of Services and our Privacy Policy',
|
||||
style: kLoginTermsAndPrivacyStyle(size),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(
|
||||
height: size.height * 0.02,
|
||||
),
|
||||
|
||||
/// SignUp Button
|
||||
signUpButton(theme),
|
||||
SizedBox(
|
||||
height: size.height * 0.03,
|
||||
),
|
||||
|
||||
/// Navigate To Login Screen
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (ctx) => const LoginView()));
|
||||
nameController.clear();
|
||||
emailController.clear();
|
||||
passwordController.clear();
|
||||
_formKey.currentState?.reset();
|
||||
|
||||
simpleUIController.isObscure.value = true;
|
||||
},
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: 'Already have an account?',
|
||||
style: kHaveAnAccountStyle(size),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: " Login",
|
||||
style: kLoginOrSignUpTextStyle(size)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// SignUp Button
|
||||
Widget signUpButton(ThemeData theme) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 55,
|
||||
child: ElevatedButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all(Colors.deepPurpleAccent),
|
||||
shape: MaterialStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
// Validate returns true if the form is valid, or false otherwise.
|
||||
if (_formKey.currentState!.validate()) {
|
||||
// ... Navigate To your Home Page
|
||||
}
|
||||
},
|
||||
child: const Text('Sign up'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
25
lib/views/splash_screen.dart
Normal file
25
lib/views/splash_screen.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
|
||||
class SplashScreen extends StatelessWidget {
|
||||
const SplashScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
return Scaffold(
|
||||
body: SizedBox(
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
child: Center(
|
||||
child: Lottie.asset(
|
||||
'assets/wave.json',
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
fit: BoxFit.fitWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user