diff --git a/android/app/build.gradle b/android/app/build.gradle index 3679781..c53d5d3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -5,6 +5,35 @@ plugins { id "dev.flutter.flutter-gradle-plugin" } +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +def keystoreProperties = new Properties() +def keystorePropertiesFile = rootProject.file('key.properties') +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} + android { namespace = "com.example.cashumit" compileSdk = 35 @@ -26,15 +55,22 @@ android { // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion targetSdk = flutter.targetSdkVersion - versionCode = flutter.versionCode + versionCode = flutter.versionCode.toInteger() versionName = flutter.versionName } + signingConfigs { + release { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null + storePassword keystoreProperties['storePassword'] + } + } + buildTypes { release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig = signingConfigs.debug + signingConfig signingConfigs.release } } } diff --git a/lib/features/transaction_screen.dart b/lib/features/transaction_screen.dart index becb972..28b0a8e 100644 --- a/lib/features/transaction_screen.dart +++ b/lib/features/transaction_screen.dart @@ -6,6 +6,7 @@ import 'package:cashumit/services/firefly_api_service.dart'; import 'package:bluetooth_print/bluetooth_print.dart'; import 'package:bluetooth_print/bluetooth_print_model.dart'; import 'package:cashumit/utils/currency_format.dart'; +import 'package:cashumit/widgets/printing_status_card.dart'; import 'package:shared_preferences/shared_preferences.dart'; class TransactionScreen extends StatefulWidget { @@ -40,6 +41,9 @@ class _TransactionScreenState extends State { String? _destinationAccountName; bool _isLoadingAccounts = false; + // Printing status + bool _isPrinting = false; + // Controllers for manual account input final TextEditingController _sourceAccountController = TextEditingController(); final TextEditingController _destinationAccountController = TextEditingController(); @@ -451,6 +455,11 @@ class _TransactionScreenState extends State { if (confirmed != true) return; + // Tampilkan status printing + setState(() { + _isPrinting = true; + }); + // Cetak struk final printService = PrintService(); final printed = await printService.printTransaction( @@ -459,6 +468,11 @@ class _TransactionScreenState extends State { 'Jl. Merdeka No. 123, Jakarta', ); + // Sembunyikan status printing + setState(() { + _isPrinting = false; + }); + if (!printed) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -507,224 +521,236 @@ class _TransactionScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Aplikasi Kasir'), - actions: [ - IconButton( - onPressed: _loadAccounts, - icon: _isLoadingAccounts - ? const CircularProgressIndicator() - : const Icon(Icons.refresh), - ), - IconButton( - onPressed: _startScan, - icon: _isScanning - ? const CircularProgressIndicator() - : const Icon(Icons.bluetooth), - ), - ], - ), - body: Column( - children: [ - // Pencarian item - Padding( - padding: const EdgeInsets.all(8.0), - child: TextField( - controller: _searchController, - decoration: const InputDecoration( - labelText: 'Cari barang...', - prefixIcon: Icon(Icons.search), - border: OutlineInputBorder(), + return Stack( + children: [ + Scaffold( + appBar: AppBar( + title: const Text('Aplikasi Kasir'), + actions: [ + IconButton( + onPressed: _loadAccounts, + icon: _isLoadingAccounts + ? const CircularProgressIndicator() + : const Icon(Icons.refresh), ), - onChanged: _searchItems, - ), - ), - // Dropdown printer - if (_devices.isNotEmpty) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: DropdownButton( - hint: const Text('Pilih Printer'), - value: _selectedDevice, - items: _devices - .map((device) => DropdownMenuItem( - value: device, - child: Text(device.name ?? device.address ?? '-'), - )) - .toList(), - onChanged: (device) { - setState(() { - _selectedDevice = device; - }); - }, - isExpanded: true, + IconButton( + onPressed: _startScan, + icon: _isScanning + ? const CircularProgressIndicator() + : const Icon(Icons.bluetooth), ), - ), - // Dropdown metode pembayaran - Padding( - padding: const EdgeInsets.all(8.0), - child: DropdownButton( - value: _paymentMethod, - items: ['Tunai', 'Debit', 'Kredit', 'QRIS'] - .map((method) => DropdownMenuItem( - value: method, - child: Text(method), - )) - .toList(), - onChanged: (method) { - setState(() { - _paymentMethod = method!; - }); - }, - isExpanded: true, - ), + ], ), - // Transaction settings directly in this screen - Card( - margin: const EdgeInsets.all(8.0), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Pengaturan Transaksi:', - style: TextStyle(fontWeight: FontWeight.bold), + body: Column( + children: [ + // Pencarian item + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: _searchController, + decoration: const InputDecoration( + labelText: 'Cari barang...', + prefixIcon: Icon(Icons.search), + border: OutlineInputBorder(), ), - const SizedBox(height: 8), - // Date picker - const Text('Tanggal Transaksi:'), - ListTile( - title: Text( - '${_transactionDate.day}/${_transactionDate.month}/${_transactionDate.year}', - ), - trailing: const Icon(Icons.calendar_today), - onTap: () => _selectDate(context), - ), - const SizedBox(height: 8), - // Source account - const Text('Akun Sumber:'), - _sourceAccountId != null - ? Card( - child: ListTile( - title: Text(_sourceAccountName ?? ''), - trailing: const Icon(Icons.edit), - onTap: _selectSourceAccount, - ), - ) - : Column( - children: [ - TextField( - controller: _sourceAccountController, - decoration: const InputDecoration( - labelText: 'Nama Akun Sumber', - hintText: 'Ketik nama akun atau pilih dari daftar', - ), - onChanged: _onSourceAccountChanged, - ), - ElevatedButton( - onPressed: _selectSourceAccount, - child: const Text('Pilih Akun Sumber dari Daftar'), - ), - ], - ), - const SizedBox(height: 8), - // Destination account - const Text('Akun Tujuan:'), - _destinationAccountId != null - ? Card( - child: ListTile( - title: Text(_destinationAccountName ?? ''), - trailing: const Icon(Icons.edit), - onTap: _selectDestinationAccount, - ), - ) - : Column( - children: [ - TextField( - controller: _destinationAccountController, - decoration: const InputDecoration( - labelText: 'Nama Akun Tujuan', - hintText: 'Ketik nama akun atau pilih dari daftar', - ), - onChanged: _onDestinationAccountChanged, - ), - ElevatedButton( - onPressed: _selectDestinationAccount, - child: const Text('Pilih Akun Tujuan dari Daftar'), - ), - ], - ), - if (_isLoadingAccounts) - const Padding( - padding: EdgeInsets.all(8.0), - child: Center(child: CircularProgressIndicator()), - ), - ], + onChanged: _searchItems, + ), ), - ), - ), - // Total - Container( - padding: const EdgeInsets.all(16.0), - color: Colors.grey[200], - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Total:', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + // Dropdown printer + if (_devices.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: DropdownButton( + hint: const Text('Pilih Printer'), + value: _selectedDevice, + items: _devices + .map((device) => DropdownMenuItem( + value: device, + child: Text(device.name ?? device.address ?? '-'), + )) + .toList(), + onChanged: (device) { + setState(() { + _selectedDevice = device; + }); + }, + isExpanded: true, + ), ), - Text( - 'Rp ${CurrencyFormat.formatRupiahWithoutSymbol(_total)}', - style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + // Dropdown metode pembayaran + Padding( + padding: const EdgeInsets.all(8.0), + child: DropdownButton( + value: _paymentMethod, + items: ['Tunai', 'Debit', 'Kredit', 'QRIS'] + .map((method) => DropdownMenuItem( + value: method, + child: Text(method), + )) + .toList(), + onChanged: (method) { + setState(() { + _paymentMethod = method!; + }); + }, + isExpanded: true, ), - ], - ), - ), - // TabBar untuk navigasi - Expanded( - child: DefaultTabController( - length: 2, - child: Column( - children: [ - const TabBar( - tabs: [ - Tab(text: 'Barang'), - Tab(text: 'Keranjang'), + ), + // Transaction settings directly in this screen + Card( + margin: const EdgeInsets.all(8.0), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Pengaturan Transaksi:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + // Date picker + const Text('Tanggal Transaksi:'), + ListTile( + title: Text( + '${_transactionDate.day}/${_transactionDate.month}/${_transactionDate.year}', + ), + trailing: const Icon(Icons.calendar_today), + onTap: () => _selectDate(context), + ), + const SizedBox(height: 8), + // Source account + const Text('Akun Sumber:'), + _sourceAccountId != null + ? Card( + child: ListTile( + title: Text(_sourceAccountName ?? ''), + trailing: const Icon(Icons.edit), + onTap: _selectSourceAccount, + ), + ) + : Column( + children: [ + TextField( + controller: _sourceAccountController, + decoration: const InputDecoration( + labelText: 'Nama Akun Sumber', + hintText: 'Ketik nama akun atau pilih dari daftar', + ), + onChanged: _onSourceAccountChanged, + ), + ElevatedButton( + onPressed: _selectSourceAccount, + child: const Text('Pilih Akun Sumber dari Daftar'), + ), + ], + ), + const SizedBox(height: 8), + // Destination account + const Text('Akun Tujuan:'), + _destinationAccountId != null + ? Card( + child: ListTile( + title: Text(_destinationAccountName ?? ''), + trailing: const Icon(Icons.edit), + onTap: _selectDestinationAccount, + ), + ) + : Column( + children: [ + TextField( + controller: _destinationAccountController, + decoration: const InputDecoration( + labelText: 'Nama Akun Tujuan', + hintText: 'Ketik nama akun atau pilih dari daftar', + ), + onChanged: _onDestinationAccountChanged, + ), + ElevatedButton( + onPressed: _selectDestinationAccount, + child: const Text('Pilih Akun Tujuan dari Daftar'), + ), + ], + ), + if (_isLoadingAccounts) + const Padding( + padding: EdgeInsets.all(8.0), + child: Center(child: CircularProgressIndicator()), + ), ], ), - Expanded( - child: TabBarView( - children: [ - // Tab Barang - _buildItemsTab(), - // Tab Keranjang - _buildCartTab(), - ], + ), + ), + // Total + Container( + padding: const EdgeInsets.all(16.0), + color: Colors.grey[200], + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Total:', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), + Text( + 'Rp ${CurrencyFormat.formatRupiahWithoutSymbol(_total)}', + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + ], + ), + ), + // TabBar untuk navigasi + Expanded( + child: DefaultTabController( + length: 2, + child: Column( + children: [ + const TabBar( + tabs: [ + Tab(text: 'Barang'), + Tab(text: 'Keranjang'), + ], + ), + Expanded( + child: TabBarView( + children: [ + // Tab Barang + _buildItemsTab(), + // Tab Keranjang + _buildCartTab(), + ], + ), + ), + ], ), - ], + ), + ), + ], + ), + bottomNavigationBar: Padding( + padding: const EdgeInsets.all(8.0), + child: ElevatedButton( + onPressed: _completeTransaction, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.all(16.0), + backgroundColor: Colors.green, + ), + child: const Text( + 'BAYAR', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), ), ), - ], - ), - bottomNavigationBar: Padding( - padding: const EdgeInsets.all(8.0), - child: ElevatedButton( - onPressed: _completeTransaction, - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.all(16.0), - backgroundColor: Colors.green, - ), - child: const Text( - 'BAYAR', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), - ), ), - ), + PrintingStatusCard( + isVisible: _isPrinting, + onDismiss: () { + setState(() { + _isPrinting = false; + }); + }, + ), + ], ); } diff --git a/lib/widgets/printing_status_card.dart b/lib/widgets/printing_status_card.dart index e0e8398..2d90865 100644 --- a/lib/widgets/printing_status_card.dart +++ b/lib/widgets/printing_status_card.dart @@ -5,10 +5,10 @@ class PrintingStatusCard extends StatefulWidget { final VoidCallback? onDismiss; const PrintingStatusCard({ - Key? key, + super.key, required this.isVisible, this.onDismiss, - }) : super(key: key); + }) : super(); @override State createState() => _PrintingStatusCardState(); @@ -71,81 +71,84 @@ class _PrintingStatusCardState extends State return AnimatedBuilder( animation: _controller, builder: (context, child) { - return Positioned( - top: 100, - left: MediaQuery.of(context).size.width * 0.1, - right: MediaQuery.of(context).size.width * 0.1, - child: IgnorePointer( - ignoring: !widget.isVisible, - child: Opacity( - opacity: _fadeAnimation.value, - child: Transform.scale( - scale: _scaleAnimation.value, - child: Card( - elevation: 12, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - child: Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - gradient: const LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Color(0xFF6A11CB), - Color(0xFF2575FC), + return Visibility( + visible: widget.isVisible, + child: Positioned( + top: 100, + left: MediaQuery.of(context).size.width * 0.1, + right: MediaQuery.of(context).size.width * 0.1, + child: IgnorePointer( + ignoring: !_fadeAnimation.value.isNaN && _fadeAnimation.value < 0.5, + child: Opacity( + opacity: _fadeAnimation.value, + child: Transform.scale( + scale: _scaleAnimation.value, + child: Card( + elevation: 12, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF6A11CB), + Color(0xFF2575FC), + ], + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 8), + const Icon( + Icons.print, + color: Colors.white, + size: 36, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Mencetak Struk', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 4), + const Text( + 'Mohon tunggu...', + style: TextStyle( + fontSize: 16, + color: Colors.white70, + ), + ), + const SizedBox(height: 12), + LinearProgressIndicator( + backgroundColor: Colors.white30, + color: Colors.white, + minHeight: 6, + value: null, + ), + ], + ), + ), + IconButton( + icon: const Icon(Icons.close, color: Colors.white70), + onPressed: widget.onDismiss, + ), ], ), ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(width: 8), - const Icon( - Icons.print, - color: Colors.white, - size: 36, - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - 'Mencetak Struk', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - const SizedBox(height: 4), - const Text( - 'Mohon tunggu...', - style: TextStyle( - fontSize: 16, - color: Colors.white70, - ), - ), - const SizedBox(height: 12), - LinearProgressIndicator( - backgroundColor: Colors.white30, - color: Colors.white, - minHeight: 6, - value: null, - ), - ], - ), - ), - IconButton( - icon: const Icon(Icons.close, color: Colors.white70), - onPressed: widget.onDismiss, - ), - ], - ), ), ), ),