Implement printing status card and fix transaction screen issues

master
a2nr 2025-09-01 21:08:39 +07:00
parent 9fe79d5ab7
commit a2eedc8efc
3 changed files with 346 additions and 281 deletions

View File

@ -5,6 +5,35 @@ plugins {
id "dev.flutter.flutter-gradle-plugin" 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 { android {
namespace = "com.example.cashumit" namespace = "com.example.cashumit"
compileSdk = 35 compileSdk = 35
@ -26,15 +55,22 @@ android {
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode versionCode = flutter.versionCode.toInteger()
versionName = flutter.versionName versionName = flutter.versionName
} }
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes { buildTypes {
release { release {
// TODO: Add your own signing config for the release build. signingConfig signingConfigs.release
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.debug
} }
} }
} }

View File

@ -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.dart';
import 'package:bluetooth_print/bluetooth_print_model.dart'; import 'package:bluetooth_print/bluetooth_print_model.dart';
import 'package:cashumit/utils/currency_format.dart'; import 'package:cashumit/utils/currency_format.dart';
import 'package:cashumit/widgets/printing_status_card.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
class TransactionScreen extends StatefulWidget { class TransactionScreen extends StatefulWidget {
@ -40,6 +41,9 @@ class _TransactionScreenState extends State<TransactionScreen> {
String? _destinationAccountName; String? _destinationAccountName;
bool _isLoadingAccounts = false; bool _isLoadingAccounts = false;
// Printing status
bool _isPrinting = false;
// Controllers for manual account input // Controllers for manual account input
final TextEditingController _sourceAccountController = TextEditingController(); final TextEditingController _sourceAccountController = TextEditingController();
final TextEditingController _destinationAccountController = TextEditingController(); final TextEditingController _destinationAccountController = TextEditingController();
@ -451,6 +455,11 @@ class _TransactionScreenState extends State<TransactionScreen> {
if (confirmed != true) return; if (confirmed != true) return;
// Tampilkan status printing
setState(() {
_isPrinting = true;
});
// Cetak struk // Cetak struk
final printService = PrintService(); final printService = PrintService();
final printed = await printService.printTransaction( final printed = await printService.printTransaction(
@ -459,6 +468,11 @@ class _TransactionScreenState extends State<TransactionScreen> {
'Jl. Merdeka No. 123, Jakarta', 'Jl. Merdeka No. 123, Jakarta',
); );
// Sembunyikan status printing
setState(() {
_isPrinting = false;
});
if (!printed) { if (!printed) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@ -507,224 +521,236 @@ class _TransactionScreenState extends State<TransactionScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Stack(
appBar: AppBar( children: [
title: const Text('Aplikasi Kasir'), Scaffold(
actions: [ appBar: AppBar(
IconButton( title: const Text('Aplikasi Kasir'),
onPressed: _loadAccounts, actions: [
icon: _isLoadingAccounts IconButton(
? const CircularProgressIndicator() onPressed: _loadAccounts,
: const Icon(Icons.refresh), icon: _isLoadingAccounts
), ? const CircularProgressIndicator()
IconButton( : const Icon(Icons.refresh),
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(),
), ),
onChanged: _searchItems, IconButton(
), onPressed: _startScan,
), icon: _isScanning
// Dropdown printer ? const CircularProgressIndicator()
if (_devices.isNotEmpty) : const Icon(Icons.bluetooth),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: DropdownButton<BluetoothDevice>(
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,
), ),
), ],
// Dropdown metode pembayaran
Padding(
padding: const EdgeInsets.all(8.0),
child: DropdownButton<String>(
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 body: Column(
Card( children: [
margin: const EdgeInsets.all(8.0), // Pencarian item
child: Padding( Padding(
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(8.0),
child: Column( child: TextField(
crossAxisAlignment: CrossAxisAlignment.start, controller: _searchController,
children: [ decoration: const InputDecoration(
const Text( labelText: 'Cari barang...',
'Pengaturan Transaksi:', prefixIcon: Icon(Icons.search),
style: TextStyle(fontWeight: FontWeight.bold), border: OutlineInputBorder(),
), ),
const SizedBox(height: 8), onChanged: _searchItems,
// 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()),
),
],
), ),
), // Dropdown printer
), if (_devices.isNotEmpty)
// Total Padding(
Container( padding: const EdgeInsets.symmetric(horizontal: 8.0),
padding: const EdgeInsets.all(16.0), child: DropdownButton<BluetoothDevice>(
color: Colors.grey[200], hint: const Text('Pilih Printer'),
child: Row( value: _selectedDevice,
mainAxisAlignment: MainAxisAlignment.spaceBetween, items: _devices
children: [ .map((device) => DropdownMenuItem(
const Text( value: device,
'Total:', child: Text(device.name ?? device.address ?? '-'),
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ))
.toList(),
onChanged: (device) {
setState(() {
_selectedDevice = device;
});
},
isExpanded: true,
),
), ),
Text( // Dropdown metode pembayaran
'Rp ${CurrencyFormat.formatRupiahWithoutSymbol(_total)}', Padding(
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), padding: const EdgeInsets.all(8.0),
child: DropdownButton<String>(
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(
// TabBar untuk navigasi margin: const EdgeInsets.all(8.0),
Expanded( child: Padding(
child: DefaultTabController( padding: const EdgeInsets.all(12.0),
length: 2, child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const TabBar( const Text(
tabs: [ 'Pengaturan Transaksi:',
Tab(text: 'Barang'), style: TextStyle(fontWeight: FontWeight.bold),
Tab(text: 'Keranjang'), ),
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: [ // Total
// Tab Barang Container(
_buildItemsTab(), padding: const EdgeInsets.all(16.0),
// Tab Keranjang color: Colors.grey[200],
_buildCartTab(), 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;
});
},
),
],
); );
} }

View File

@ -5,10 +5,10 @@ class PrintingStatusCard extends StatefulWidget {
final VoidCallback? onDismiss; final VoidCallback? onDismiss;
const PrintingStatusCard({ const PrintingStatusCard({
Key? key, super.key,
required this.isVisible, required this.isVisible,
this.onDismiss, this.onDismiss,
}) : super(key: key); }) : super();
@override @override
State<PrintingStatusCard> createState() => _PrintingStatusCardState(); State<PrintingStatusCard> createState() => _PrintingStatusCardState();
@ -71,81 +71,84 @@ class _PrintingStatusCardState extends State<PrintingStatusCard>
return AnimatedBuilder( return AnimatedBuilder(
animation: _controller, animation: _controller,
builder: (context, child) { builder: (context, child) {
return Positioned( return Visibility(
top: 100, visible: widget.isVisible,
left: MediaQuery.of(context).size.width * 0.1, child: Positioned(
right: MediaQuery.of(context).size.width * 0.1, top: 100,
child: IgnorePointer( left: MediaQuery.of(context).size.width * 0.1,
ignoring: !widget.isVisible, right: MediaQuery.of(context).size.width * 0.1,
child: Opacity( child: IgnorePointer(
opacity: _fadeAnimation.value, ignoring: !_fadeAnimation.value.isNaN && _fadeAnimation.value < 0.5,
child: Transform.scale( child: Opacity(
scale: _scaleAnimation.value, opacity: _fadeAnimation.value,
child: Card( child: Transform.scale(
elevation: 12, scale: _scaleAnimation.value,
shape: RoundedRectangleBorder( child: Card(
borderRadius: BorderRadius.circular(20), elevation: 12,
), shape: RoundedRectangleBorder(
child: Container( borderRadius: BorderRadius.circular(20),
padding: const EdgeInsets.all(20), ),
decoration: BoxDecoration( child: Container(
borderRadius: BorderRadius.circular(20), padding: const EdgeInsets.all(20),
gradient: const LinearGradient( decoration: BoxDecoration(
begin: Alignment.topLeft, borderRadius: BorderRadius.circular(20),
end: Alignment.bottomRight, gradient: const LinearGradient(
colors: [ begin: Alignment.topLeft,
Color(0xFF6A11CB), end: Alignment.bottomRight,
Color(0xFF2575FC), 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,
),
],
),
), ),
), ),
), ),