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:
BACHIR SOULDI
2026-07-16 14:37:28 +01:00
parent 37b0762921
commit 5a483da01d
110 changed files with 8081 additions and 0 deletions

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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'),
),
);
}
}

View 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,
),
),
),
);
}
}