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"
}
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
}
}
}

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_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<TransactionScreen> {
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<TransactionScreen> {
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<TransactionScreen> {
'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<TransactionScreen> {
@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<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,
IconButton(
onPressed: _startScan,
icon: _isScanning
? const CircularProgressIndicator()
: const Icon(Icons.bluetooth),
),
),
// 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
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<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,
),
),
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<String>(
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;
});
},
),
],
);
}

View File

@ -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<PrintingStatusCard> createState() => _PrintingStatusCardState();
@ -71,81 +71,84 @@ class _PrintingStatusCardState extends State<PrintingStatusCard>
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,
),
],
),
),
),
),