diff --git a/lib/screens/dashboard/dashboard_page.dart b/lib/screens/dashboard/dashboard_page.dart index 944e0ee..315cadf 100644 --- a/lib/screens/dashboard/dashboard_page.dart +++ b/lib/screens/dashboard/dashboard_page.dart @@ -36,6 +36,10 @@ class _DashboardScreenState extends State with RouteAware { _controller.loadDashboard(showLoading: false); } + Future _refreshDashboard() { + return _controller.loadDashboard(showLoading: false); + } + @override Widget build(BuildContext context) { final topPadding = MediaQuery.of(context).padding.top; @@ -69,6 +73,25 @@ class _DashboardScreenState extends State with RouteAware { Row( mainAxisAlignment: MainAxisAlignment.end, children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(10), + ), + child: IconButton( + tooltip: 'Refresh', + icon: const Icon( + Icons.refresh, + color: Colors.white, + size: 20, + ), + onPressed: _refreshDashboard, + padding: EdgeInsets.zero, + ), + ), + const SizedBox(width: 10), Stack( children: [ Container( @@ -188,232 +211,244 @@ class _DashboardScreenState extends State with RouteAware { ), ), ), - body: SingleChildScrollView( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppColors.bgWhite, - borderRadius: BorderRadius.circular(16), - boxShadow: [AppColors.shadowLight], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Penggunaan Bahan', - style: AppTextStyles.headlineSmall - .copyWith( - color: AppColors.textPrimary, - ), - ), - const SizedBox(height: 2), - Text( - 'Top 5 bahan paling sering digunakan', - style: AppTextStyles.bodySmall.copyWith( - color: AppColors.textTertiary, - ), - ), - ], - ), - Icon( - Icons.bar_chart_rounded, - color: AppColors.secondaryBlue, - size: 20, - ), - ], - ), - const SizedBox(height: 16), - if (_controller.isLoading) - const Center(child: CircularProgressIndicator()) - else - Column( - children: - _controller.penggunaanBahan.map((item) { - return Padding( - padding: const EdgeInsets.only( - bottom: 12, - ), - child: _buildUsageRow(item), - ); - }).toList(), - ), - ], - ), - ), - const SizedBox(height: 24), - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppColors.bgWhite, - borderRadius: BorderRadius.circular(16), - boxShadow: [AppColors.shadowLight], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.warning_amber_rounded, - color: AppColors.statusWarning, - size: 22, - ), - const SizedBox(width: 10), - Text( - 'Stok Menipis', - style: AppTextStyles.headlineSmall.copyWith( - color: AppColors.textPrimary, - ), - ), - ], - ), - const SizedBox(height: 16), - if (_controller.isLoading) - const Center(child: CircularProgressIndicator()) - else if (_controller.lowStockItems.isEmpty) - Text( - 'Tidak ada stok kritis', - style: AppTextStyles.bodySmall.copyWith( - color: AppColors.textTertiary, - ), - ) - else - ..._controller.lowStockItems.map((item) { - return Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + body: RefreshIndicator( + onRefresh: _refreshDashboard, + color: AppColors.primaryBrown, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.bgWhite, + borderRadius: BorderRadius.circular(16), + boxShadow: [AppColors.shadowLight], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, children: [ - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - item['name'], - style: AppTextStyles.labelLarge - .copyWith( - color: - AppColors.textPrimary, - ), + Text( + 'Penggunaan Bahan', + style: AppTextStyles.headlineSmall + .copyWith( + color: AppColors.textPrimary, ), - const SizedBox(height: 4), - Text( - 'Stok: ${item['stock']}', - style: AppTextStyles.bodySmall - .copyWith( - color: - AppColors.textTertiary, - ), - ), - ], - ), ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 6, - ), - decoration: BoxDecoration( - color: item['statusColor'], - borderRadius: BorderRadius.circular( - 6, - ), - ), - child: Text( - item['status'], - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Colors.white, - ), + const SizedBox(height: 2), + Text( + 'Top 5 bahan paling sering digunakan', + style: AppTextStyles.bodySmall.copyWith( + color: AppColors.textTertiary, ), ), ], ), - ); - }), + Icon( + Icons.bar_chart_rounded, + color: AppColors.secondaryBlue, + size: 20, + ), + ], + ), + const SizedBox(height: 16), + if (_controller.isLoading) + const Center(child: CircularProgressIndicator()) + else + Column( + children: + _controller.penggunaanBahan.map((item) { + return Padding( + padding: const EdgeInsets.only( + bottom: 12, + ), + child: _buildUsageRow(item), + ); + }).toList(), + ), + ], + ), + ), + const SizedBox(height: 24), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.bgWhite, + borderRadius: BorderRadius.circular(16), + boxShadow: [AppColors.shadowLight], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.warning_amber_rounded, + color: AppColors.statusWarning, + size: 22, + ), + const SizedBox(width: 10), + Text( + 'Stok Menipis', + style: AppTextStyles.headlineSmall.copyWith( + color: AppColors.textPrimary, + ), + ), + ], + ), + const SizedBox(height: 16), + if (_controller.isLoading) + const Center(child: CircularProgressIndicator()) + else if (_controller.lowStockItems.isEmpty) + Text( + 'Tidak ada stok kritis', + style: AppTextStyles.bodySmall.copyWith( + color: AppColors.textTertiary, + ), + ) + else + ..._controller.lowStockItems.map((item) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + item['name'], + style: AppTextStyles.labelLarge + .copyWith( + color: + AppColors.textPrimary, + ), + ), + const SizedBox(height: 4), + Text( + 'Stok: ${item['stock']}', + style: AppTextStyles.bodySmall + .copyWith( + color: + AppColors + .textTertiary, + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: item['statusColor'], + borderRadius: BorderRadius.circular( + 6, + ), + ), + child: Text( + item['status'], + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ], + ), + ); + }), + ], + ), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: () { + Navigator.of( + context, + ).pushNamed('/prediction'); + }, + icon: const Icon(Icons.trending_up, size: 20), + label: const Text( + 'Lihat Prediksi', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.secondaryBlue, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + vertical: 16, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + elevation: 2, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + onPressed: () { + Navigator.of( + context, + ).pushNamed('/transaction'); + }, + icon: const Icon(Icons.check_circle, size: 20), + label: const Text( + 'Tambah Stok', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.statusSuccess, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + vertical: 16, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + elevation: 2, + ), + ), + ), ], ), - ), - const SizedBox(height: 24), - Row( - children: [ - Expanded( - child: ElevatedButton.icon( - onPressed: () { - Navigator.of(context).pushNamed('/prediction'); - }, - icon: const Icon(Icons.trending_up, size: 20), - label: const Text( - 'Lihat Prediksi', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.secondaryBlue, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric( - vertical: 16, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), - ), - elevation: 2, - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: ElevatedButton.icon( - onPressed: () { - Navigator.of(context).pushNamed('/transaction'); - }, - icon: const Icon(Icons.check_circle, size: 20), - label: const Text( - 'Tambah Stok', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.statusSuccess, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric( - vertical: 16, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), - ), - elevation: 2, - ), - ), - ), - ], - ), - const SizedBox(height: 24), - ], + const SizedBox(height: 24), + ], + ), ), - ), - ], + ], + ), ), ), bottomNavigationBar: BottomNavigationBar( diff --git a/lib/screens/prediction/prediction_page.dart b/lib/screens/prediction/prediction_page.dart index 2614738..b5a43b7 100644 --- a/lib/screens/prediction/prediction_page.dart +++ b/lib/screens/prediction/prediction_page.dart @@ -27,6 +27,10 @@ class _PredictionScreenState extends State { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error))); } + Future _refreshRecipes() { + return _loadRecipes(); + } + Future _submitProduction() async { if (_controller.isSubmitting) return; final result = await _controller.submitProduction(); @@ -203,6 +207,24 @@ class _PredictionScreenState extends State { ], ), ), + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.3), + borderRadius: BorderRadius.circular(10), + ), + child: IconButton( + tooltip: 'Refresh', + icon: const Icon( + Icons.refresh, + color: Colors.white, + size: 20, + ), + onPressed: _refreshRecipes, + padding: EdgeInsets.zero, + ), + ), ], ), const SizedBox(height: 12), @@ -267,881 +289,52 @@ class _PredictionScreenState extends State { ), ), ), - body: - _controller.isLoading - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - CircularProgressIndicator(), - SizedBox(height: 16), - Text('Memuat resep...'), - ], - ), - ) - : _controller.recipes.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.error_outline, - size: 48, - color: Color(0xFFDC2626), - ), - const SizedBox(height: 16), - const Text('Gagal memuat resep'), - const SizedBox(height: 8), - ElevatedButton( - onPressed: _loadRecipes, - child: const Text('Coba Lagi'), - ), - ], - ), - ) - : SingleChildScrollView( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Rencana Produksi', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Color(0xFF1F2937), - ), - ), - const SizedBox(height: 16), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - const Text( - 'Pilih Produk', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: Color(0xFF1F2937), - ), - ), - const SizedBox(height: 8), - Container( - decoration: BoxDecoration( - border: Border.all( - color: const Color(0xFFE5E7EB), - width: 1, - ), - borderRadius: BorderRadius.circular( - 10, - ), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - value: _controller.selectedRecipe, - hint: const Padding( - padding: EdgeInsets.symmetric( - horizontal: 12, - ), - child: Text( - '-- Pilih Produk --', - style: TextStyle( - color: Color(0xFF9CA3AF), - fontSize: 14, - ), - ), - ), - isExpanded: true, - icon: const Padding( - padding: EdgeInsets.only( - right: 12, - ), - child: Icon( - Icons.expand_more, - color: Color(0xFF9CA3AF), - ), - ), - items: - _controller.recipes - .map( - ( - recipe, - ) => DropdownMenuItem< - String - >( - value: - recipe['recipe_name'] - as String, - child: Padding( - padding: - const EdgeInsets.symmetric( - horizontal: - 12, - ), - child: Text( - recipe['recipe_name'] - as String, - ), - ), - ), - ) - .toList(), - onChanged: - _controller.setSelectedRecipe, - ), - ), - ), - ], - ), - const SizedBox(height: 16), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - const Text( - 'Jumlah Produksi', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: Color(0xFF1F2937), - ), - ), - const SizedBox(height: 8), - TextField( - controller: _productionController, - keyboardType: TextInputType.number, - onChanged: - _controller.setProductionQuantity, - decoration: InputDecoration( - hintText: '0', - hintStyle: const TextStyle( - color: Color(0xFF9CA3AF), - ), - suffixText: 'pcs', - suffixStyle: const TextStyle( - color: Color(0xFF9CA3AF), - fontSize: 12, - ), - border: OutlineInputBorder( - borderRadius: - BorderRadius.circular(10), - borderSide: const BorderSide( - color: Color(0xFFE5E7EB), - ), - ), - enabledBorder: OutlineInputBorder( - borderRadius: - BorderRadius.circular(10), - borderSide: const BorderSide( - color: Color(0xFFE5E7EB), - ), - ), - contentPadding: - const EdgeInsets.symmetric( - horizontal: 12, - vertical: 12, - ), - ), - ), - ], - ), - const SizedBox(height: 20), - Row( - children: [ - Expanded( - child: ElevatedButton.icon( - onPressed: - _controller.canCalculate - ? () async { - await _controller - .refreshStock(); - _controller.calculate(); - } - : null, - icon: const Icon( - Icons.calculate, - size: 18, - ), - label: const Text( - 'Hitung Kebutuhan', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - ), - ), - style: ElevatedButton.styleFrom( - backgroundColor: const Color( - 0xFFA89080, - ), - foregroundColor: Colors.white, - padding: - const EdgeInsets.symmetric( - vertical: 12, - ), - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(10), - ), - ), - ), - ), - const SizedBox(width: 10), - OutlinedButton( - onPressed: () { - _controller.reset(); - _productionController.clear(); - }, - style: OutlinedButton.styleFrom( - side: const BorderSide( - color: Color(0xFFE5E7EB), - width: 1, - ), - padding: const EdgeInsets.symmetric( - horizontal: 20, - ), - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(10), - ), - ), - child: const Text( - 'Reset', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: Color(0xFF1F2937), - ), - ), - ), - ], - ), - ], - ), - ), - const SizedBox(height: 24), - if (_controller.isCalculated) ...[ + body: RefreshIndicator( + onRefresh: _refreshRecipes, + color: const Color(0xFFA89080), + child: + _controller.isLoading + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Memuat resep...'), + ], + ), + ) + : _controller.recipes.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.error_outline, + size: 48, + color: Color(0xFFDC2626), + ), + const SizedBox(height: 16), + const Text('Gagal memuat resep'), + const SizedBox(height: 8), + ElevatedButton( + onPressed: _loadRecipes, + child: const Text('Coba Lagi'), + ), + ], + ), + ) + : SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Container( padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: - _controller.isStockSufficient - ? const Color( - 0xFF10B981, - ).withOpacity(0.1) - : const Color( - 0xFFDC2626, - ).withOpacity(0.1), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: - _controller.isStockSufficient - ? const Color( - 0xFF10B981, - ).withOpacity(0.3) - : const Color( - 0xFFDC2626, - ).withOpacity(0.3), - width: 1, - ), - ), - child: Row( - children: [ - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: - _controller.isStockSufficient - ? const Color( - 0xFF10B981, - ).withOpacity(0.2) - : const Color( - 0xFFDC2626, - ).withOpacity(0.2), - borderRadius: BorderRadius.circular( - 12, - ), - ), - child: Icon( - _controller.isStockSufficient - ? Icons.check_circle - : Icons.warning_amber, - color: - _controller.isStockSufficient - ? const Color(0xFF10B981) - : const Color(0xFFDC2626), - size: 24, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - _controller.isStockSufficient - ? 'Stok Cukup' - : 'Stok Belum Cukup', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - color: - _controller - .isStockSufficient - ? const Color( - 0xFF10B981, - ) - : const Color( - 0xFFDC2626, - ), - ), - ), - const SizedBox(height: 2), - Text( - _controller.isStockSufficient - ? 'Semua bahan tersedia untuk produksi' - : 'Ada bahan yang perlu ditambah', - style: const TextStyle( - fontSize: 12, - color: Color(0xFF6B7280), - ), - ), - ], - ), - ), - ], - ), - ), - const SizedBox(height: 16), - const Text( - 'Kebutuhan Bahan', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - color: Color(0xFF1F2937), - ), - ), - const SizedBox(height: 12), - if (displayIngredients.isEmpty) - Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: const Text( - 'Belum ada bahan untuk produk ini.', - style: TextStyle( - fontSize: 12, - color: Color(0xFF6B7280), - ), - ), - ) - else - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - children: - displayIngredients.entries.toList().asMap().entries.map(( - entry, - ) { - final isLast = - entry.key == - displayIngredients.length - 1; - final ingredient = entry.value.key; - final details = entry.value.value; - final isSelected = _controller - .isIngredientSelected( - ingredient, - ); - final unit = - (details['unit'] as String?) ?? - _controller.getIngredientUnit( - ingredient, - ); - final quantityPerUnit = - (details['quantity'] as num) - .toDouble(); - final neededAmount = - isSelected - ? quantityPerUnit * - _controller - .productionQuantity - : 0.0; - final stockKg = _controller - .getCurrentStock(ingredient); - final requiredGram = - isSelected - ? _controller.toGram( - amount: neededAmount, - unit: unit, - ) - : 0.0; - final requiredInStockUnit = - _controller - .getRequiredInStockUnit( - ingredient, - ); - final isSufficient = - stockKg >= requiredInStockUnit; - final statusColor = - isSelected - ? (isSufficient - ? const Color( - 0xFF10B981, - ) - : const Color( - 0xFFDC2626, - )) - : const Color(0xFF9CA3AF); - final statusLabel = - isSelected - ? (isSufficient - ? 'Aman' - : 'Kurang') - : 'Diabaikan'; - - return Column( - children: [ - Padding( - padding: const EdgeInsets.all( - 14, - ), - child: Column( - children: [ - Row( - crossAxisAlignment: - CrossAxisAlignment - .start, - children: [ - Checkbox( - value: isSelected, - onChanged: (value) { - _controller - .setIngredientSelection( - ingredient, - value ?? - false, - ); - }, - activeColor: - const Color( - 0xFFA89080, - ), - ), - const SizedBox( - width: 4, - ), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment - .start, - children: [ - Text( - _controller - .cleanIngredientName( - ingredient, - ), - style: const TextStyle( - fontSize: - 13, - fontWeight: - FontWeight - .w600, - color: Color( - 0xFF1F2937, - ), - ), - ), - const SizedBox( - height: 6, - ), - Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - const Text( - 'Kebutuhan', - style: TextStyle( - fontSize: - 11, - color: Color( - 0xFF9CA3AF, - ), - ), - ), - Text( - '${_controller.formatQuantity(requiredGram)} gr', - style: const TextStyle( - fontSize: - 12, - fontWeight: - FontWeight.w700, - color: Color( - 0xFF1F2937, - ), - ), - ), - ], - ), - ), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - const Text( - 'Stok Saat Ini', - style: TextStyle( - fontSize: - 11, - color: Color( - 0xFF9CA3AF, - ), - ), - ), - Text( - _controller.formatStockQuantity( - ingredient, - stockKg, - ), - style: const TextStyle( - fontSize: - 12, - fontWeight: - FontWeight.w700, - color: Color( - 0xFF1F2937, - ), - ), - ), - ], - ), - ), - ], - ), - ], - ), - ), - const SizedBox( - width: 10, - ), - Container( - padding: - const EdgeInsets.symmetric( - horizontal: - 10, - vertical: 6, - ), - decoration: BoxDecoration( - color: statusColor - .withOpacity( - 0.15, - ), - borderRadius: - BorderRadius.circular( - 8, - ), - border: Border.all( - color: statusColor - .withOpacity( - 0.3, - ), - width: 1, - ), - ), - child: Text( - statusLabel, - style: TextStyle( - fontSize: 11, - fontWeight: - FontWeight - .w700, - color: - statusColor, - ), - ), - ), - ], - ), - ], - ), - ), - if (!isLast) - Container( - height: 1, - color: const Color( - 0xFFF3F4F6, - ), - ), - ], - ); - }).toList(), - ), - ), - const SizedBox(height: 16), - if (_controller.stockUsage.isNotEmpty) ...[ - const Text( - 'Ringkasan Pengurangan Stok', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - color: Color(0xFF1F2937), - ), - ), - const SizedBox(height: 12), - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - children: - _controller.stockUsage.entries.map(( - entry, - ) { - final ingredient = entry.key; - final amount = entry.value; - return Padding( - padding: - const EdgeInsets.symmetric( - vertical: 6, - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Expanded( - child: Text( - _controller - .cleanIngredientName( - ingredient, - ), - style: const TextStyle( - fontSize: 12, - fontWeight: - FontWeight.w600, - color: Color( - 0xFF1F2937, - ), - ), - ), - ), - Text( - '-${_controller.formatStockQuantity(ingredient, amount)}', - style: const TextStyle( - fontSize: 12, - fontWeight: - FontWeight.w700, - color: Color(0xFFDC2626), - ), - ), - ], - ), - ); - }).toList(), - ), - ), - const SizedBox(height: 16), - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: - _controller.isSubmitting - ? null - : _submitProduction, - icon: - _controller.isSubmitting - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: - AlwaysStoppedAnimation< - Color - >(Colors.white), - ), - ) - : const Icon(Icons.check_circle), - label: Text( - _controller.isSubmitting - ? 'Memproses...' - : 'Submit Produksi', - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - ), - ), - style: ElevatedButton.styleFrom( - backgroundColor: const Color( - 0xFF10B981, - ), - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric( - vertical: 12, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 10, - ), - ), - ), - ), - ), - const SizedBox(height: 16), - ], - if (!_controller.isStockSufficient) ...[ - const Text( - 'Rekomendasi Penambahan Stok', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - color: Color(0xFF1F2937), - ), - ), - const SizedBox(height: 12), - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color( - 0xFFDC2626, - ).withOpacity(0.05), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: const Color( - 0xFFDC2626, - ).withOpacity(0.2), - width: 1, - ), - ), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: - _controller.insufficientStock.entries.map(( - entry, - ) { - final ingredient = entry.key; - final deficitAmount = entry.value; - final unit = _controller - .getStockUnit(ingredient); - - return Padding( - padding: - const EdgeInsets.symmetric( - vertical: 8, - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Expanded( - child: Text( - _controller - .cleanIngredientName( - ingredient, - ), - style: const TextStyle( - fontSize: 13, - fontWeight: - FontWeight.w600, - color: Color( - 0xFF1F2937, - ), - ), - ), - ), - Container( - padding: - const EdgeInsets.symmetric( - horizontal: 12, - vertical: 6, - ), - decoration: BoxDecoration( - color: const Color( - 0xFFDC2626, - ).withOpacity(0.1), - borderRadius: - BorderRadius.circular( - 8, - ), - ), - child: Text( - '+${_controller.formatQuantity(deficitAmount)} $unit', - style: const TextStyle( - fontSize: 12, - fontWeight: - FontWeight.w700, - color: Color( - 0xFFDC2626, - ), - ), - ), - ), - ], - ), - ); - }).toList(), - ), - ), - ], - ] else - Container( - width: double.infinity, - padding: const EdgeInsets.all(32), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), @@ -1153,37 +346,891 @@ class _PredictionScreenState extends State { ), ], ), - child: const Column( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, children: [ - _EmptyCalcIcon(), - SizedBox(height: 16), - Text( - 'Belum Ada Kalkulasi', + const Text( + 'Rencana Produksi', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1F2937), ), ), - SizedBox(height: 8), - Text( - 'Pilih produk dan masukkan jumlah produksi untuk melihat kebutuhan bahan', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 13, - color: Color(0xFF9CA3AF), - height: 1.5, - ), + const SizedBox(height: 16), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + 'Pilih Produk', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Color(0xFF1F2937), + ), + ), + const SizedBox(height: 8), + Container( + decoration: BoxDecoration( + border: Border.all( + color: const Color(0xFFE5E7EB), + width: 1, + ), + borderRadius: + BorderRadius.circular(10), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: + _controller.selectedRecipe, + hint: const Padding( + padding: EdgeInsets.symmetric( + horizontal: 12, + ), + child: Text( + '-- Pilih Produk --', + style: TextStyle( + color: Color(0xFF9CA3AF), + fontSize: 14, + ), + ), + ), + isExpanded: true, + icon: const Padding( + padding: EdgeInsets.only( + right: 12, + ), + child: Icon( + Icons.expand_more, + color: Color(0xFF9CA3AF), + ), + ), + items: + _controller.recipes + .map( + ( + recipe, + ) => DropdownMenuItem< + String + >( + value: + recipe['recipe_name'] + as String, + child: Padding( + padding: + const EdgeInsets.symmetric( + horizontal: + 12, + ), + child: Text( + recipe['recipe_name'] + as String, + ), + ), + ), + ) + .toList(), + onChanged: + _controller + .setSelectedRecipe, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + 'Jumlah Produksi', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Color(0xFF1F2937), + ), + ), + const SizedBox(height: 8), + TextField( + controller: _productionController, + keyboardType: TextInputType.number, + onChanged: + _controller + .setProductionQuantity, + decoration: InputDecoration( + hintText: '0', + hintStyle: const TextStyle( + color: Color(0xFF9CA3AF), + ), + suffixText: 'pcs', + suffixStyle: const TextStyle( + color: Color(0xFF9CA3AF), + fontSize: 12, + ), + border: OutlineInputBorder( + borderRadius: + BorderRadius.circular(10), + borderSide: const BorderSide( + color: Color(0xFFE5E7EB), + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: + BorderRadius.circular(10), + borderSide: const BorderSide( + color: Color(0xFFE5E7EB), + ), + ), + contentPadding: + const EdgeInsets.symmetric( + horizontal: 12, + vertical: 12, + ), + ), + ), + ], + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: + _controller.canCalculate + ? () async { + await _controller + .refreshStock(); + _controller.calculate(); + } + : null, + icon: const Icon( + Icons.calculate, + size: 18, + ), + label: const Text( + 'Hitung Kebutuhan', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: const Color( + 0xFFA89080, + ), + foregroundColor: Colors.white, + padding: + const EdgeInsets.symmetric( + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(10), + ), + ), + ), + ), + const SizedBox(width: 10), + OutlinedButton( + onPressed: () { + _controller.reset(); + _productionController.clear(); + }, + style: OutlinedButton.styleFrom( + side: const BorderSide( + color: Color(0xFFE5E7EB), + width: 1, + ), + padding: + const EdgeInsets.symmetric( + horizontal: 20, + ), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(10), + ), + ), + child: const Text( + 'Reset', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Color(0xFF1F2937), + ), + ), + ), + ], ), ], ), ), - ], + const SizedBox(height: 24), + if (_controller.isCalculated) ...[ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: + _controller.isStockSufficient + ? const Color( + 0xFF10B981, + ).withOpacity(0.1) + : const Color( + 0xFFDC2626, + ).withOpacity(0.1), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: + _controller.isStockSufficient + ? const Color( + 0xFF10B981, + ).withOpacity(0.3) + : const Color( + 0xFFDC2626, + ).withOpacity(0.3), + width: 1, + ), + ), + child: Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: + _controller.isStockSufficient + ? const Color( + 0xFF10B981, + ).withOpacity(0.2) + : const Color( + 0xFFDC2626, + ).withOpacity(0.2), + borderRadius: BorderRadius.circular( + 12, + ), + ), + child: Icon( + _controller.isStockSufficient + ? Icons.check_circle + : Icons.warning_amber, + color: + _controller.isStockSufficient + ? const Color(0xFF10B981) + : const Color(0xFFDC2626), + size: 24, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + _controller.isStockSufficient + ? 'Stok Cukup' + : 'Stok Belum Cukup', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: + _controller + .isStockSufficient + ? const Color( + 0xFF10B981, + ) + : const Color( + 0xFFDC2626, + ), + ), + ), + const SizedBox(height: 2), + Text( + _controller.isStockSufficient + ? 'Semua bahan tersedia untuk produksi' + : 'Ada bahan yang perlu ditambah', + style: const TextStyle( + fontSize: 12, + color: Color(0xFF6B7280), + ), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 16), + const Text( + 'Kebutuhan Bahan', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: Color(0xFF1F2937), + ), + ), + const SizedBox(height: 12), + if (displayIngredients.isEmpty) + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity( + 0.05, + ), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: const Text( + 'Belum ada bahan untuk produk ini.', + style: TextStyle( + fontSize: 12, + color: Color(0xFF6B7280), + ), + ), + ) + else + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity( + 0.05, + ), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: + displayIngredients.entries.toList().asMap().entries.map(( + entry, + ) { + final isLast = + entry.key == + displayIngredients.length - 1; + final ingredient = + entry.value.key; + final details = entry.value.value; + final isSelected = _controller + .isIngredientSelected( + ingredient, + ); + final unit = + (details['unit'] + as String?) ?? + _controller.getIngredientUnit( + ingredient, + ); + final quantityPerUnit = + (details['quantity'] as num) + .toDouble(); + final neededAmount = + isSelected + ? quantityPerUnit * + _controller + .productionQuantity + : 0.0; + final stockKg = _controller + .getCurrentStock(ingredient); + final requiredGram = + isSelected + ? _controller.toGram( + amount: neededAmount, + unit: unit, + ) + : 0.0; + final requiredInStockUnit = + _controller + .getRequiredInStockUnit( + ingredient, + ); + final isSufficient = + stockKg >= + requiredInStockUnit; + final statusColor = + isSelected + ? (isSufficient + ? const Color( + 0xFF10B981, + ) + : const Color( + 0xFFDC2626, + )) + : const Color(0xFF9CA3AF); + final statusLabel = + isSelected + ? (isSufficient + ? 'Aman' + : 'Kurang') + : 'Diabaikan'; + + return Column( + children: [ + Padding( + padding: + const EdgeInsets.all( + 14, + ), + child: Column( + children: [ + Row( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Checkbox( + value: isSelected, + onChanged: ( + value, + ) { + _controller + .setIngredientSelection( + ingredient, + value ?? + false, + ); + }, + activeColor: + const Color( + 0xFFA89080, + ), + ), + const SizedBox( + width: 4, + ), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Text( + _controller + .cleanIngredientName( + ingredient, + ), + style: const TextStyle( + fontSize: + 13, + fontWeight: + FontWeight + .w600, + color: Color( + 0xFF1F2937, + ), + ), + ), + const SizedBox( + height: 6, + ), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + 'Kebutuhan', + style: TextStyle( + fontSize: + 11, + color: Color( + 0xFF9CA3AF, + ), + ), + ), + Text( + '${_controller.formatQuantity(requiredGram)} gr', + style: const TextStyle( + fontSize: + 12, + fontWeight: + FontWeight.w700, + color: Color( + 0xFF1F2937, + ), + ), + ), + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + 'Stok Saat Ini', + style: TextStyle( + fontSize: + 11, + color: Color( + 0xFF9CA3AF, + ), + ), + ), + Text( + _controller.formatStockQuantity( + ingredient, + stockKg, + ), + style: const TextStyle( + fontSize: + 12, + fontWeight: + FontWeight.w700, + color: Color( + 0xFF1F2937, + ), + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + const SizedBox( + width: 10, + ), + Container( + padding: + const EdgeInsets.symmetric( + horizontal: + 10, + vertical: 6, + ), + decoration: BoxDecoration( + color: statusColor + .withOpacity( + 0.15, + ), + borderRadius: + BorderRadius.circular( + 8, + ), + border: Border.all( + color: statusColor + .withOpacity( + 0.3, + ), + width: 1, + ), + ), + child: Text( + statusLabel, + style: TextStyle( + fontSize: 11, + fontWeight: + FontWeight + .w700, + color: + statusColor, + ), + ), + ), + ], + ), + ], + ), + ), + if (!isLast) + Container( + height: 1, + color: const Color( + 0xFFF3F4F6, + ), + ), + ], + ); + }).toList(), + ), + ), + const SizedBox(height: 16), + if (_controller.stockUsage.isNotEmpty) ...[ + const Text( + 'Ringkasan Pengurangan Stok', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: Color(0xFF1F2937), + ), + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity( + 0.05, + ), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: + _controller.stockUsage.entries.map(( + entry, + ) { + final ingredient = entry.key; + final amount = entry.value; + return Padding( + padding: + const EdgeInsets.symmetric( + vertical: 6, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Expanded( + child: Text( + _controller + .cleanIngredientName( + ingredient, + ), + style: const TextStyle( + fontSize: 12, + fontWeight: + FontWeight.w600, + color: Color( + 0xFF1F2937, + ), + ), + ), + ), + Text( + '-${_controller.formatStockQuantity(ingredient, amount)}', + style: const TextStyle( + fontSize: 12, + fontWeight: + FontWeight.w700, + color: Color( + 0xFFDC2626, + ), + ), + ), + ], + ), + ); + }).toList(), + ), + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: + _controller.isSubmitting + ? null + : _submitProduction, + icon: + _controller.isSubmitting + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: + AlwaysStoppedAnimation< + Color + >(Colors.white), + ), + ) + : const Icon( + Icons.check_circle, + ), + label: Text( + _controller.isSubmitting + ? 'Memproses...' + : 'Submit Produksi', + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: const Color( + 0xFF10B981, + ), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 10, + ), + ), + ), + ), + ), + const SizedBox(height: 16), + ], + if (!_controller.isStockSufficient) ...[ + const Text( + 'Rekomendasi Penambahan Stok', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: Color(0xFF1F2937), + ), + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color( + 0xFFDC2626, + ).withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color( + 0xFFDC2626, + ).withOpacity(0.2), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: + _controller.insufficientStock.entries.map(( + entry, + ) { + final ingredient = entry.key; + final deficitAmount = entry.value; + final unit = _controller + .getStockUnit(ingredient); + + return Padding( + padding: + const EdgeInsets.symmetric( + vertical: 8, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Expanded( + child: Text( + _controller + .cleanIngredientName( + ingredient, + ), + style: const TextStyle( + fontSize: 13, + fontWeight: + FontWeight.w600, + color: Color( + 0xFF1F2937, + ), + ), + ), + ), + Container( + padding: + const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: const Color( + 0xFFDC2626, + ).withOpacity(0.1), + borderRadius: + BorderRadius.circular( + 8, + ), + ), + child: Text( + '+${_controller.formatQuantity(deficitAmount)} $unit', + style: const TextStyle( + fontSize: 12, + fontWeight: + FontWeight.w700, + color: Color( + 0xFFDC2626, + ), + ), + ), + ), + ], + ), + ); + }).toList(), + ), + ), + ], + ] else + Container( + width: double.infinity, + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: const Column( + children: [ + _EmptyCalcIcon(), + SizedBox(height: 16), + Text( + 'Belum Ada Kalkulasi', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color(0xFF1F2937), + ), + ), + SizedBox(height: 8), + Text( + 'Pilih produk dan masukkan jumlah produksi untuk melihat kebutuhan bahan', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13, + color: Color(0xFF9CA3AF), + height: 1.5, + ), + ), + ], + ), + ), + ], + ), ), - ), - ], + ], + ), ), - ), + ), bottomNavigationBar: BottomNavigationBar( currentIndex: 3, type: BottomNavigationBarType.fixed, diff --git a/lib/screens/products/product_list_page.dart b/lib/screens/products/product_list_page.dart index 2be1345..a3ff9af 100644 --- a/lib/screens/products/product_list_page.dart +++ b/lib/screens/products/product_list_page.dart @@ -45,6 +45,10 @@ class _ProductListScreenState extends State with RouteAware { _controller.loadProducts(); } + Future _refreshProducts() { + return _controller.loadProducts(); + } + @override Widget build(BuildContext context) { return AnimatedBuilder( @@ -111,6 +115,25 @@ class _ProductListScreenState extends State with RouteAware { ), ], ), + const Spacer(), + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(10), + ), + child: IconButton( + tooltip: 'Refresh', + icon: const Icon( + Icons.refresh, + color: Colors.white, + size: 20, + ), + onPressed: _refreshProducts, + padding: EdgeInsets.zero, + ), + ), ], ), ), @@ -155,7 +178,7 @@ class _ProductListScreenState extends State with RouteAware { ), ), body: RefreshIndicator( - onRefresh: _controller.loadProducts, + onRefresh: _refreshProducts, color: AppColors.primaryBrown, child: _controller.isLoading diff --git a/lib/screens/reports/report_page.dart b/lib/screens/reports/report_page.dart index ddb8796..5375238 100644 --- a/lib/screens/reports/report_page.dart +++ b/lib/screens/reports/report_page.dart @@ -61,6 +61,10 @@ class _ReportScreenState extends State { _controller.startAutoRefresh(); } + Future _refreshReports() { + return _controller.loadReports(); + } + double _sectionMaxHeight() { final height = MediaQuery.of(context).size.height * 0.35; if (height < 220) return 220; @@ -82,6 +86,13 @@ class _ReportScreenState extends State { 'Laporan & Analitik', style: AppTextStyles.headlineLarge.copyWith(color: Colors.white), ), + actions: [ + IconButton( + tooltip: 'Refresh', + icon: const Icon(Icons.refresh, color: Colors.white), + onPressed: _refreshReports, + ), + ], ), body: AnimatedBuilder( animation: _controller, @@ -98,7 +109,7 @@ class _ReportScreenState extends State { final List demandTrend = _controller.demandTrend; return RefreshIndicator( - onRefresh: () => _controller.loadReports(), + onRefresh: _refreshReports, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(16), diff --git a/lib/screens/settings/settings_page.dart b/lib/screens/settings/settings_page.dart index de299bc..857892b 100644 --- a/lib/screens/settings/settings_page.dart +++ b/lib/screens/settings/settings_page.dart @@ -125,45 +125,57 @@ class _SettingsScreenState extends State { 'Pengaturan', style: AppTextStyles.headlineLarge.copyWith(color: Colors.white), ), + actions: [ + IconButton( + tooltip: 'Refresh', + icon: const Icon(Icons.refresh, color: Colors.white), + onPressed: _loadAccount, + ), + ], ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSectionTitle('Akun'), - const SizedBox(height: 16), - _buildAccountCard(), - const SizedBox(height: 32), - _buildSectionTitle('Keamanan'), - const SizedBox(height: 16), - _buildActionCard( - icon: Icons.lock_outlined, - title: 'Ubah Password', - subtitle: 'Verifikasi OTP melalui email akun', - onTap: _showResetPasswordDialog, - ), - const SizedBox(height: 32), - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: _showLogoutDialog, - icon: const Icon(Icons.logout), - label: const Text('Logout'), - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.statusError, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + body: RefreshIndicator( + onRefresh: _loadAccount, + color: AppColors.primaryBrown, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionTitle('Akun'), + const SizedBox(height: 16), + _buildAccountCard(), + const SizedBox(height: 32), + _buildSectionTitle('Keamanan'), + const SizedBox(height: 16), + _buildActionCard( + icon: Icons.lock_outlined, + title: 'Ubah Password', + subtitle: 'Verifikasi OTP melalui email akun', + onTap: _showResetPasswordDialog, + ), + const SizedBox(height: 32), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _showLogoutDialog, + icon: const Icon(Icons.logout), + label: const Text('Logout'), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.statusError, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), ), ), ), - ), - const SizedBox(height: 32), - _buildAppInfo(), - ], + const SizedBox(height: 32), + _buildAppInfo(), + ], + ), ), ), ), diff --git a/lib/screens/transaction/transaction_page.dart b/lib/screens/transaction/transaction_page.dart index a032d57..7620877 100644 --- a/lib/screens/transaction/transaction_page.dart +++ b/lib/screens/transaction/transaction_page.dart @@ -48,6 +48,20 @@ class _TransactionScreenState extends State { ); } + Future _refreshProducts() async { + try { + await _controller.loadProducts(); + } catch (_) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Gagal memuat data produk'), + backgroundColor: AppColors.statusError, + ), + ); + } + } + Future _submitAllTransactions() async { final result = await _controller.submitAllTransactions(); @@ -504,115 +518,24 @@ class _TransactionScreenState extends State { icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: () => Navigator.pop(context), ), + actions: [ + IconButton( + tooltip: 'Refresh', + icon: const Icon(Icons.refresh, color: Colors.white), + onPressed: _refreshProducts, + ), + ], ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppColors.bgWhite, - borderRadius: BorderRadius.circular(16), - boxShadow: [AppColors.shadowLight], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Pilih Produk', - style: AppTextStyles.headlineSmall.copyWith( - color: AppColors.textPrimary, - ), - ), - const SizedBox(height: 16), - Text( - 'Produk', - style: AppTextStyles.labelLarge.copyWith( - color: AppColors.textPrimary, - ), - ), - const SizedBox(height: 8), - DropdownButtonFormField( - value: _controller.selectedProduct, - decoration: InputDecoration( - hintText: 'Pilih produk', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - ), - filled: true, - fillColor: AppColors.bgLight, - ), - items: - _controller.products - .map( - (product) => DropdownMenuItem( - value: product, - child: Text(product), - ), - ) - .toList(), - onChanged: _controller.setSelectedProduct, - ), - const SizedBox(height: 24), - Text( - 'Jumlah', - style: AppTextStyles.labelLarge.copyWith( - color: AppColors.textPrimary, - ), - ), - const SizedBox(height: 8), - TextField( - controller: _controller.quantityController, - keyboardType: TextInputType.number, - decoration: InputDecoration( - hintText: 'Masukkan jumlah unit', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - ), - filled: true, - fillColor: AppColors.bgLight, - ), - ), - const SizedBox(height: 24), - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: _addToCart, - icon: const Icon(Icons.add_shopping_cart), - label: const Text('Tambah ke Keranjang'), - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primaryBrown, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ), - ), - const SizedBox(height: 16), - SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - onPressed: _showAddProductDialog, - icon: const Icon(Icons.add_circle_outline), - label: const Text('Tambah Produk Baru'), - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - side: BorderSide(color: AppColors.secondaryBlue), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ), - ), - ], - ), - ), - const SizedBox(height: 24), - if (_controller.cartItems.isNotEmpty) ...[ + body: RefreshIndicator( + onRefresh: _refreshProducts, + color: AppColors.primaryBrown, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( @@ -623,282 +546,96 @@ class _TransactionScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Keranjang (${_controller.cartItems.length})', - style: AppTextStyles.headlineSmall.copyWith( - color: AppColors.textPrimary, - ), - ), - GestureDetector( - onTap: _controller.clearCart, - child: Text( - 'Hapus Semua', - style: AppTextStyles.labelMedium.copyWith( - color: AppColors.statusError, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - const SizedBox(height: 16), - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: _controller.cartItems.length, - itemBuilder: (context, index) { - final item = _controller.cartItems[index]; - return Container( - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: AppColors.bgLight, - borderRadius: BorderRadius.circular(10), - border: Border.all(color: AppColors.grey300), - ), - child: Column( - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - item.productName, - style: AppTextStyles.labelLarge - .copyWith( - color: - AppColors.textPrimary, - ), - ), - const SizedBox(height: 4), - Text( - 'Rp ${item.unitPrice} / unit', - style: AppTextStyles.labelSmall - .copyWith( - color: - AppColors - .textSecondary, - ), - ), - ], - ), - ), - Text( - 'Rp ${item.totalPrice}', - style: AppTextStyles.labelLarge - .copyWith( - color: AppColors.statusSuccess, - fontWeight: FontWeight.w700, - ), - ), - ], - ), - const SizedBox(height: 12), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - GestureDetector( - onTap: - () => _controller - .updateQuantity( - index, - item.quantity - 1, - ), - child: Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: AppColors.primaryBrown, - borderRadius: - BorderRadius.circular(6), - ), - child: const Icon( - Icons.remove, - color: Colors.white, - size: 18, - ), - ), - ), - const SizedBox(width: 12), - Text( - item.quantity.toString(), - style: AppTextStyles.labelLarge - .copyWith( - color: - AppColors.textPrimary, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(width: 12), - GestureDetector( - onTap: - () => _controller - .updateQuantity( - index, - item.quantity + 1, - ), - child: Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: AppColors.primaryBrown, - borderRadius: - BorderRadius.circular(6), - ), - child: const Icon( - Icons.add, - color: Colors.white, - size: 18, - ), - ), - ), - ], - ), - GestureDetector( - onTap: - () => _controller.removeFromCart( - index, - ), - child: Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: AppColors.statusError - .withValues(alpha: 0.1), - borderRadius: - BorderRadius.circular(6), - ), - child: Icon( - Icons.delete, - color: AppColors.statusError, - size: 18, - ), - ), - ), - ], - ), - ], - ), - ); - }, - ), - const SizedBox(height: 16), - const Divider(), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Total Pembayaran', - style: AppTextStyles.labelLarge.copyWith( - color: AppColors.textPrimary, - ), - ), - Text( - 'Rp ${_controller.totalPrice}', - style: AppTextStyles.headlineSmall.copyWith( - color: AppColors.primaryBrown, - fontWeight: FontWeight.w700, - ), - ), - ], + Text( + 'Pilih Produk', + style: AppTextStyles.headlineSmall.copyWith( + color: AppColors.textPrimary, + ), ), const SizedBox(height: 16), Text( - 'Tanggal Stok Masuk', + 'Produk', style: AppTextStyles.labelLarge.copyWith( color: AppColors.textPrimary, ), ), const SizedBox(height: 8), - GestureDetector( - onTap: () async { - final picked = await showDatePicker( - context: context, - initialDate: - _controller.selectedDate ?? DateTime.now(), - firstDate: DateTime(2020), - lastDate: DateTime.now(), - ); - if (picked != null && - picked != _controller.selectedDate) { - _controller.setSelectedDate(picked); - } - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, + DropdownButtonFormField( + value: _controller.selectedProduct, + decoration: InputDecoration( + hintText: 'Pilih produk', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), ), - decoration: BoxDecoration( - border: Border.all(color: AppColors.grey300), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - children: [ - const Icon( - Icons.calendar_today, - color: AppColors.primaryBrown, - ), - const SizedBox(width: 12), - Text( - _controller.selectedDate != null - ? '${_controller.selectedDate!.day}/${_controller.selectedDate!.month}/${_controller.selectedDate!.year}' - : 'Pilih tanggal', - style: AppTextStyles.bodyMedium, - ), - ], + filled: true, + fillColor: AppColors.bgLight, + ), + items: + _controller.products + .map( + (product) => DropdownMenuItem( + value: product, + child: Text(product), + ), + ) + .toList(), + onChanged: _controller.setSelectedProduct, + ), + const SizedBox(height: 24), + Text( + 'Jumlah', + style: AppTextStyles.labelLarge.copyWith( + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 8), + TextField( + controller: _controller.quantityController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: 'Masukkan jumlah unit', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), ), + filled: true, + fillColor: AppColors.bgLight, ), ), const SizedBox(height: 24), SizedBox( width: double.infinity, child: ElevatedButton.icon( - onPressed: - _controller.isLoading - ? null - : _submitAllTransactions, + onPressed: _addToCart, + icon: const Icon(Icons.add_shopping_cart), + label: const Text('Tambah ke Keranjang'), style: ElevatedButton.styleFrom( backgroundColor: AppColors.primaryBrown, - foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( - vertical: 14, + vertical: 12, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ), - icon: - _controller.isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: - AlwaysStoppedAnimation( - Colors.white, - ), - ), - ) - : const Icon(Icons.check_circle), - label: Text( - _controller.isLoading - ? 'Menyimpan...' - : 'Simpan Stok Masuk', - style: AppTextStyles.labelLarge, + ), + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: _showAddProductDialog, + icon: const Icon(Icons.add_circle_outline), + label: const Text('Tambah Produk Baru'), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric( + vertical: 12, + ), + side: BorderSide( + color: AppColors.secondaryBlue, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), ), ), ), @@ -906,98 +643,406 @@ class _TransactionScreenState extends State { ), ), const SizedBox(height: 24), - ], - Text( - 'Riwayat Stok Masuk (${_controller.transactions.length})', - style: AppTextStyles.headlineSmall.copyWith( - color: AppColors.textPrimary, - ), - ), - const SizedBox(height: 16), - if (_controller.transactions.isEmpty) - Center( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 32.0), + if (_controller.cartItems.isNotEmpty) ...[ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.bgWhite, + borderRadius: BorderRadius.circular(16), + boxShadow: [AppColors.shadowLight], + ), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon( - Icons.inbox, - size: 64, - color: AppColors.grey300, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Keranjang (${_controller.cartItems.length})', + style: AppTextStyles.headlineSmall.copyWith( + color: AppColors.textPrimary, + ), + ), + GestureDetector( + onTap: _controller.clearCart, + child: Text( + 'Hapus Semua', + style: AppTextStyles.labelMedium.copyWith( + color: AppColors.statusError, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _controller.cartItems.length, + itemBuilder: (context, index) { + final item = _controller.cartItems[index]; + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.bgLight, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: AppColors.grey300, + ), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + item.productName, + style: AppTextStyles + .labelLarge + .copyWith( + color: + AppColors + .textPrimary, + ), + ), + const SizedBox(height: 4), + Text( + 'Rp ${item.unitPrice} / unit', + style: AppTextStyles + .labelSmall + .copyWith( + color: + AppColors + .textSecondary, + ), + ), + ], + ), + ), + Text( + 'Rp ${item.totalPrice}', + style: AppTextStyles.labelLarge + .copyWith( + color: + AppColors.statusSuccess, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + GestureDetector( + onTap: + () => _controller + .updateQuantity( + index, + item.quantity - 1, + ), + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: + AppColors.primaryBrown, + borderRadius: + BorderRadius.circular( + 6, + ), + ), + child: const Icon( + Icons.remove, + color: Colors.white, + size: 18, + ), + ), + ), + const SizedBox(width: 12), + Text( + item.quantity.toString(), + style: AppTextStyles.labelLarge + .copyWith( + color: + AppColors.textPrimary, + fontWeight: + FontWeight.w600, + ), + ), + const SizedBox(width: 12), + GestureDetector( + onTap: + () => _controller + .updateQuantity( + index, + item.quantity + 1, + ), + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: + AppColors.primaryBrown, + borderRadius: + BorderRadius.circular( + 6, + ), + ), + child: const Icon( + Icons.add, + color: Colors.white, + size: 18, + ), + ), + ), + ], + ), + GestureDetector( + onTap: + () => _controller + .removeFromCart(index), + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: AppColors.statusError + .withValues(alpha: 0.1), + borderRadius: + BorderRadius.circular(6), + ), + child: Icon( + Icons.delete, + color: AppColors.statusError, + size: 18, + ), + ), + ), + ], + ), + ], + ), + ); + }, + ), + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Total Pembayaran', + style: AppTextStyles.labelLarge.copyWith( + color: AppColors.textPrimary, + ), + ), + Text( + 'Rp ${_controller.totalPrice}', + style: AppTextStyles.headlineSmall.copyWith( + color: AppColors.primaryBrown, + fontWeight: FontWeight.w700, + ), + ), + ], ), const SizedBox(height: 16), Text( - 'Belum ada Stok Masuk', - style: AppTextStyles.bodyMedium.copyWith( - color: AppColors.textSecondary, + 'Tanggal Stok Masuk', + style: AppTextStyles.labelLarge.copyWith( + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 8), + GestureDetector( + onTap: () async { + final picked = await showDatePicker( + context: context, + initialDate: + _controller.selectedDate ?? + DateTime.now(), + firstDate: DateTime(2020), + lastDate: DateTime.now(), + ); + if (picked != null && + picked != _controller.selectedDate) { + _controller.setSelectedDate(picked); + } + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + decoration: BoxDecoration( + border: Border.all(color: AppColors.grey300), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + const Icon( + Icons.calendar_today, + color: AppColors.primaryBrown, + ), + const SizedBox(width: 12), + Text( + _controller.selectedDate != null + ? '${_controller.selectedDate!.day}/${_controller.selectedDate!.month}/${_controller.selectedDate!.year}' + : 'Pilih tanggal', + style: AppTextStyles.bodyMedium, + ), + ], + ), + ), + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: + _controller.isLoading + ? null + : _submitAllTransactions, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryBrown, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + vertical: 14, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + icon: + _controller.isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: + AlwaysStoppedAnimation( + Colors.white, + ), + ), + ) + : const Icon(Icons.check_circle), + label: Text( + _controller.isLoading + ? 'Menyimpan...' + : 'Simpan Stok Masuk', + style: AppTextStyles.labelLarge, + ), ), ), ], ), ), - ) - else - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: _controller.transactions.length, - itemBuilder: (context, index) { - final tx = _controller.transactions[index]; - return Container( - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: AppColors.bgWhite, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.grey200), - ), + const SizedBox(height: 24), + ], + Text( + 'Riwayat Stok Masuk (${_controller.transactions.length})', + style: AppTextStyles.headlineSmall.copyWith( + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 16), + if (_controller.transactions.isEmpty) + Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 32.0), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - tx.productName, - style: AppTextStyles.labelLarge.copyWith( - color: AppColors.textPrimary, - ), - ), - Text( - 'Rp ${tx.totalPrice}', - style: AppTextStyles.labelLarge.copyWith( - color: AppColors.statusSuccess, - fontWeight: FontWeight.w700, - ), - ), - ], + Icon( + Icons.inbox, + size: 64, + color: AppColors.grey300, ), - const SizedBox(height: 8), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - '${tx.quantity} unit × Rp ${tx.unitPrice}', - style: AppTextStyles.labelMedium.copyWith( - color: AppColors.textSecondary, - ), - ), - Text( - '${tx.date.day}/${tx.date.month}/${tx.date.year}', - style: AppTextStyles.labelSmall.copyWith( - color: AppColors.textSecondary, - ), - ), - ], + const SizedBox(height: 16), + Text( + 'Belum ada Stok Masuk', + style: AppTextStyles.bodyMedium.copyWith( + color: AppColors.textSecondary, + ), ), ], ), - ); - }, - ), - ], + ), + ) + else + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _controller.transactions.length, + itemBuilder: (context, index) { + final tx = _controller.transactions[index]; + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.bgWhite, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.grey200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + tx.productName, + style: AppTextStyles.labelLarge.copyWith( + color: AppColors.textPrimary, + ), + ), + Text( + 'Rp ${tx.totalPrice}', + style: AppTextStyles.labelLarge.copyWith( + color: AppColors.statusSuccess, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + '${tx.quantity} unit × Rp ${tx.unitPrice}', + style: AppTextStyles.labelMedium.copyWith( + color: AppColors.textSecondary, + ), + ), + Text( + '${tx.date.day}/${tx.date.month}/${tx.date.year}', + style: AppTextStyles.labelSmall.copyWith( + color: AppColors.textSecondary, + ), + ), + ], + ), + ], + ), + ); + }, + ), + ], + ), ), ), ),