fitur petugas
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_aset_controller.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||
import '../widgets/petugas_side_navbar.dart';
|
||||
@ -23,26 +24,12 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = Get.find<PetugasAsetController>();
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
|
||||
// Listen to tab changes and update controller
|
||||
_tabController.addListener(() {
|
||||
if (!_tabController.indexIsChanging) {
|
||||
controller.changeTab(_tabController.index);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen to controller tab changes and update TabController
|
||||
ever(controller.selectedTabIndex, (index) {
|
||||
if (_tabController.index != index) {
|
||||
_tabController.animateTo(index);
|
||||
}
|
||||
});
|
||||
// Initialize with default tab (sewa)
|
||||
controller.changeTab(0);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -82,7 +69,7 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
body: Column(
|
||||
children: [
|
||||
_buildSearchBar(),
|
||||
_buildTabBar(),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(child: _buildAssetList()),
|
||||
],
|
||||
),
|
||||
@ -93,7 +80,13 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () => Get.toNamed(Routes.PETUGAS_TAMBAH_ASET),
|
||||
onPressed: () {
|
||||
// Navigate to PetugasTambahAsetView in add mode
|
||||
Get.toNamed(
|
||||
Routes.PETUGAS_TAMBAH_ASET,
|
||||
arguments: {'isEditing': false, 'assetData': null},
|
||||
);
|
||||
},
|
||||
backgroundColor: AppColorsPetugas.babyBlueBright,
|
||||
icon: Icon(Icons.add, color: AppColorsPetugas.blueGrotto),
|
||||
label: Text(
|
||||
@ -144,60 +137,19 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabBar() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: Colors.white,
|
||||
unselectedLabelColor: AppColorsPetugas.textSecondary,
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
indicator: BoxDecoration(
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
dividerColor: Colors.transparent,
|
||||
tabs: const [
|
||||
Tab(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.shopping_cart, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text('Sewa', style: TextStyle(fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.subscriptions, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Langganan',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
// Tab bar has been removed as per requirements
|
||||
|
||||
Widget _buildAssetList() {
|
||||
return Obx(() {
|
||||
debugPrint('_buildAssetList: isLoading=${controller.isLoading.value}');
|
||||
debugPrint(
|
||||
'_buildAssetList: filteredAsetList length=${controller.filteredAsetList.length}',
|
||||
);
|
||||
if (controller.filteredAsetList.isNotEmpty) {
|
||||
debugPrint(
|
||||
'_buildAssetList: First item name=${controller.filteredAsetList[0]['nama']}',
|
||||
);
|
||||
}
|
||||
if (controller.isLoading.value) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
@ -255,10 +207,15 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: controller.filteredAsetList.length,
|
||||
itemCount: controller.filteredAsetList.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
final aset = controller.filteredAsetList[index];
|
||||
return _buildAssetCard(context, aset);
|
||||
if (index < controller.filteredAsetList.length) {
|
||||
final aset = controller.filteredAsetList[index];
|
||||
return _buildAssetCard(context, aset);
|
||||
} else {
|
||||
// Blank space at the end
|
||||
return const SizedBox(height: 80);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -266,7 +223,31 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
}
|
||||
|
||||
Widget _buildAssetCard(BuildContext context, Map<String, dynamic> aset) {
|
||||
final isAvailable = aset['tersedia'] == true;
|
||||
debugPrint('\n--- Building Asset Card ---');
|
||||
debugPrint('Asset data: $aset');
|
||||
|
||||
// Extract and validate all asset properties with proper null safety
|
||||
final status =
|
||||
aset['status']?.toString().toLowerCase() ?? 'tidak_diketahui';
|
||||
final isAvailable = status == 'tersedia';
|
||||
final imageUrl = aset['imageUrl']?.toString() ?? '';
|
||||
final harga =
|
||||
aset['harga'] is int
|
||||
? aset['harga'] as int
|
||||
: (int.tryParse(aset['harga']?.toString() ?? '0') ?? 0);
|
||||
final satuanWaktu =
|
||||
aset['satuan_waktu']?.toString().capitalizeFirst ?? 'Hari';
|
||||
final nama = aset['nama']?.toString().trim() ?? 'Nama tidak tersedia';
|
||||
final kategori = aset['kategori']?.toString().trim() ?? 'Umum';
|
||||
final orderId = aset['order_id']?.toString() ?? '';
|
||||
|
||||
// Debug prints for development
|
||||
debugPrint('Image URL: $imageUrl');
|
||||
debugPrint('Harga: $harga');
|
||||
debugPrint('Satuan Waktu: $satuanWaktu');
|
||||
debugPrint('Nama: $nama');
|
||||
debugPrint('Kategori: $kategori');
|
||||
debugPrint('Status: $status (Available: $isAvailable)');
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
@ -290,21 +271,46 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
child: Row(
|
||||
children: [
|
||||
// Asset image
|
||||
Container(
|
||||
SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
bottomLeft: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
_getAssetIcon(aset['kategori']),
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
size: 32,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
placeholder:
|
||||
(context, url) => Container(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
_getAssetIcon(
|
||||
kategori,
|
||||
), // Show category icon as placeholder
|
||||
color: AppColorsPetugas.navyBlue.withOpacity(
|
||||
0.5,
|
||||
),
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
errorWidget:
|
||||
(context, url, error) => Container(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons
|
||||
.broken_image, // Or your preferred error icon
|
||||
color: AppColorsPetugas.navyBlue.withOpacity(
|
||||
0.5,
|
||||
),
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -323,8 +329,8 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
aset['nama'],
|
||||
style: TextStyle(
|
||||
nama,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
@ -333,12 +339,63 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${controller.formatPrice(aset['harga'])} ${aset['satuan']}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
// Harga dan satuan waktu (multi-line, tampilkan semua dari satuanWaktuSewa)
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final satuanWaktuList =
|
||||
(aset['satuanWaktuSewa'] is List)
|
||||
? List<Map<String, dynamic>>.from(
|
||||
aset['satuanWaktuSewa'],
|
||||
)
|
||||
: [];
|
||||
final validSatuanWaktu =
|
||||
satuanWaktuList
|
||||
.where(
|
||||
(sw) =>
|
||||
(sw['harga'] ?? 0) > 0 &&
|
||||
(sw['nama_satuan_waktu'] !=
|
||||
null &&
|
||||
(sw['nama_satuan_waktu']
|
||||
as String)
|
||||
.isNotEmpty),
|
||||
)
|
||||
.toList();
|
||||
|
||||
if (validSatuanWaktu.isNotEmpty) {
|
||||
return Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children:
|
||||
validSatuanWaktu.map((sw) {
|
||||
final harga = sw['harga'] ?? 0;
|
||||
final satuan =
|
||||
sw['nama_satuan_waktu'] ?? '';
|
||||
return Text(
|
||||
'${controller.formatPrice(harga)} / $satuan',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color:
|
||||
AppColorsPetugas
|
||||
.textSecondary,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
} else {
|
||||
// fallback: harga tunggal
|
||||
return Text(
|
||||
'${controller.formatPrice(aset['harga'] ?? 0)} / ${aset['satuan_waktu']?.toString().capitalizeFirst ?? 'Hari'}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -383,11 +440,36 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
children: [
|
||||
// Edit icon
|
||||
GestureDetector(
|
||||
onTap:
|
||||
() => _showAddEditAssetDialog(
|
||||
context,
|
||||
aset: aset,
|
||||
),
|
||||
onTap: () {
|
||||
// Navigate to PetugasTambahAsetView in edit mode with only the asset ID
|
||||
final assetId =
|
||||
aset['id']?.toString() ??
|
||||
''; // Changed from 'id_aset' to 'id'
|
||||
debugPrint(
|
||||
'[DEBUG] Navigating to edit asset with ID: $assetId',
|
||||
);
|
||||
debugPrint(
|
||||
'[DEBUG] Full asset data: $aset',
|
||||
); // Log full asset data for debugging
|
||||
|
||||
if (assetId.isEmpty) {
|
||||
debugPrint('[ERROR] Asset ID is empty!');
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'ID Aset tidak valid',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Get.toNamed(
|
||||
Routes.PETUGAS_TAMBAH_ASET,
|
||||
arguments: {
|
||||
'isEditing': true,
|
||||
'assetId': assetId,
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
|
@ -5,6 +5,7 @@ import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||
import '../widgets/petugas_side_navbar.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../../../utils/format_utils.dart';
|
||||
|
||||
class PetugasBumdesDashboardView
|
||||
extends GetView<PetugasBumdesDashboardController> {
|
||||
@ -23,12 +24,7 @@ class PetugasBumdesDashboardView
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
onPressed: () => _showLogoutConfirmation(context),
|
||||
),
|
||||
],
|
||||
// actions: [],
|
||||
),
|
||||
drawer: PetugasSideNavbar(controller: controller),
|
||||
drawerEdgeDragWidth: 60,
|
||||
@ -118,8 +114,6 @@ class PetugasBumdesDashboardView
|
||||
),
|
||||
_buildRevenueStatistics(),
|
||||
const SizedBox(height: 16),
|
||||
_buildRevenueSources(),
|
||||
const SizedBox(height: 16),
|
||||
_buildRevenueTrend(),
|
||||
|
||||
// Add some padding at the bottom for better scrolling
|
||||
@ -156,25 +150,51 @@ class PetugasBumdesDashboardView
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 3),
|
||||
Obx(() {
|
||||
final avatar = controller.avatarUrl.value;
|
||||
if (avatar.isNotEmpty) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Image.network(
|
||||
avatar,
|
||||
width: 48,
|
||||
height: 48,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) => Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
child: const Icon(
|
||||
Icons.person,
|
||||
color: Colors.white,
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.person,
|
||||
color: Colors.white,
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.person,
|
||||
color: Colors.white,
|
||||
size: 30,
|
||||
),
|
||||
);
|
||||
}
|
||||
}),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@ -208,15 +228,17 @@ class PetugasBumdesDashboardView
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Obx(
|
||||
() => Text(
|
||||
controller.userEmail.value,
|
||||
Obx(() {
|
||||
final name = controller.userName.value;
|
||||
final email = controller.userEmail.value;
|
||||
return Text(
|
||||
name.isNotEmpty ? name : email,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -642,19 +664,24 @@ class PetugasBumdesDashboardView
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Obx(
|
||||
() => Text(
|
||||
controller.totalPendapatanBulanIni.value,
|
||||
Obx(() {
|
||||
final stats = controller.pembayaranStats;
|
||||
final total = stats['totalThisMonth'] ?? 0.0;
|
||||
return Text(
|
||||
formatRupiah(total),
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.success,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 6),
|
||||
Obx(
|
||||
() => Row(
|
||||
Obx(() {
|
||||
final stats = controller.pembayaranStats;
|
||||
final percent = stats['percentComparedLast'] ?? 0.0;
|
||||
final isPositive = percent >= 0;
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@ -663,7 +690,7 @@ class PetugasBumdesDashboardView
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
controller.isKenaikanPositif.value
|
||||
isPositive
|
||||
? AppColorsPetugas.success.withOpacity(
|
||||
0.1,
|
||||
)
|
||||
@ -676,23 +703,23 @@ class PetugasBumdesDashboardView
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
controller.isKenaikanPositif.value
|
||||
isPositive
|
||||
? Icons.arrow_upward
|
||||
: Icons.arrow_downward,
|
||||
size: 14,
|
||||
color:
|
||||
controller.isKenaikanPositif.value
|
||||
isPositive
|
||||
? AppColorsPetugas.success
|
||||
: AppColorsPetugas.error,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
controller.persentaseKenaikan.value,
|
||||
'${percent.toStringAsFixed(1)}%',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color:
|
||||
controller.isKenaikanPositif.value
|
||||
isPositive
|
||||
? AppColorsPetugas.success
|
||||
: AppColorsPetugas.error,
|
||||
),
|
||||
@ -709,8 +736,8 @@ class PetugasBumdesDashboardView
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -747,12 +774,29 @@ class PetugasBumdesDashboardView
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildRevenueQuickInfo(
|
||||
'Pendapatan Sewa',
|
||||
controller.pendapatanSewa.value,
|
||||
AppColorsPetugas.navyBlue,
|
||||
Icons.shopping_cart_outlined,
|
||||
),
|
||||
child: Obx(() {
|
||||
final stats = controller.pembayaranStats;
|
||||
final totalTunai = stats['totalTunai'] ?? 0.0;
|
||||
return _buildRevenueQuickInfo(
|
||||
'Tunai',
|
||||
formatRupiah(totalTunai),
|
||||
AppColorsPetugas.navyBlue,
|
||||
Icons.payments,
|
||||
);
|
||||
}),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
final stats = controller.pembayaranStats;
|
||||
final totalTransfer = stats['totalTransfer'] ?? 0.0;
|
||||
return _buildRevenueQuickInfo(
|
||||
'Transfer',
|
||||
formatRupiah(totalTransfer),
|
||||
AppColorsPetugas.blueGrotto,
|
||||
Icons.account_balance,
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
@ -811,81 +855,6 @@ class PetugasBumdesDashboardView
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRevenueSources() {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shadowColor: AppColorsPetugas.shadowColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Sumber Pendapatan',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
// Revenue Donut Chart
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.navyBlue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Sewa Aset',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Obx(
|
||||
() => Text(
|
||||
controller.pendapatanSewa.value,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'100% dari total pendapatan',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRevenueTrend() {
|
||||
final months = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun'];
|
||||
|
||||
@ -912,6 +881,9 @@ class PetugasBumdesDashboardView
|
||||
child: Obx(() {
|
||||
// Get the trend data from controller
|
||||
final List<double> trendData = controller.trendPendapatan;
|
||||
if (trendData.isEmpty) {
|
||||
return Center(child: Text('Tidak ada data'));
|
||||
}
|
||||
final double maxValue = trendData.reduce(
|
||||
(curr, next) => curr > next ? curr : next,
|
||||
);
|
||||
@ -925,28 +897,28 @@ class PetugasBumdesDashboardView
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${maxValue.toStringAsFixed(1)}M',
|
||||
formatRupiah(maxValue),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${(maxValue * 0.75).toStringAsFixed(1)}M',
|
||||
formatRupiah(maxValue * 0.75),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${(maxValue * 0.5).toStringAsFixed(1)}M',
|
||||
formatRupiah(maxValue * 0.5),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${(maxValue * 0.25).toStringAsFixed(1)}M',
|
||||
formatRupiah(maxValue * 0.25),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,11 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/petugas_paket_controller.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import 'package:bumrent_app/app/modules/petugas_bumdes/controllers/petugas_paket_controller.dart';
|
||||
import 'package:bumrent_app/app/routes/app_pages.dart';
|
||||
import 'package:bumrent_app/app/data/models/paket_model.dart';
|
||||
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||
import '../widgets/petugas_side_navbar.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
|
||||
class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
const PetugasPaketView({Key? key}) : super(key: key);
|
||||
@ -53,7 +55,11 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () => Get.toNamed(Routes.PETUGAS_TAMBAH_PAKET),
|
||||
onPressed:
|
||||
() => Get.toNamed(
|
||||
Routes.PETUGAS_TAMBAH_PAKET,
|
||||
arguments: {'isEditing': false},
|
||||
),
|
||||
label: Text(
|
||||
'Tambah Paket',
|
||||
style: TextStyle(
|
||||
@ -115,7 +121,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.filteredPaketList.isEmpty) {
|
||||
if (controller.filteredPackages.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@ -136,7 +142,11 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => Get.toNamed(Routes.PETUGAS_TAMBAH_PAKET),
|
||||
onPressed:
|
||||
() => Get.toNamed(
|
||||
Routes.PETUGAS_TAMBAH_PAKET,
|
||||
arguments: {'isEditing': false},
|
||||
),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Tambah Paket'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
@ -161,18 +171,192 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: controller.filteredPaketList.length,
|
||||
itemCount: controller.filteredPackages.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
final paket = controller.filteredPaketList[index];
|
||||
return _buildPaketCard(context, paket);
|
||||
if (index < controller.filteredPackages.length) {
|
||||
final paket = controller.filteredPackages[index];
|
||||
return _buildPaketCard(context, paket);
|
||||
} else {
|
||||
// Blank space at the end
|
||||
return const SizedBox(height: 80);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildPaketCard(BuildContext context, Map<String, dynamic> paket) {
|
||||
final isAvailable = paket['tersedia'] == true;
|
||||
// Format price helper method
|
||||
String _formatPrice(dynamic price) {
|
||||
if (price == null) return '0';
|
||||
// If price is a string that can be parsed to a number
|
||||
if (price is String) {
|
||||
final number = double.tryParse(price) ?? 0;
|
||||
return number
|
||||
.toStringAsFixed(0)
|
||||
.replaceAllMapped(
|
||||
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
||||
(Match m) => '${m[1]}.',
|
||||
);
|
||||
}
|
||||
// If price is already a number
|
||||
if (price is num) {
|
||||
return price
|
||||
.toStringAsFixed(0)
|
||||
.replaceAllMapped(
|
||||
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
||||
(Match m) => '${m[1]}.',
|
||||
);
|
||||
}
|
||||
return '0';
|
||||
}
|
||||
|
||||
// Helper method to get time unit name based on ID
|
||||
String _getTimeUnitName(dynamic unitId) {
|
||||
if (unitId == null) return 'unit';
|
||||
|
||||
// Convert to string in case it's not already
|
||||
final unitIdStr = unitId.toString().toLowerCase();
|
||||
|
||||
// Map of known time unit IDs to their display names
|
||||
final timeUnitMap = {
|
||||
'6eaa32d9-855d-4214-b5b5-5c73d3edd9c5': 'jam',
|
||||
'582b7e66-6869-4495-9856-cef4a46683b0': 'hari',
|
||||
// Add more mappings as needed
|
||||
};
|
||||
|
||||
// If the unitId is a known ID, return the corresponding name
|
||||
if (timeUnitMap.containsKey(unitIdStr)) {
|
||||
return timeUnitMap[unitIdStr]!;
|
||||
}
|
||||
|
||||
// Check if the unit is already a name (like 'jam' or 'hari')
|
||||
final knownUnits = ['jam', 'hari', 'minggu', 'bulan'];
|
||||
if (knownUnits.contains(unitIdStr)) {
|
||||
return unitIdStr;
|
||||
}
|
||||
|
||||
// If the unit is a Map, try to extract the name from common fields
|
||||
if (unitId is Map) {
|
||||
return unitId['nama']?.toString().toLowerCase() ??
|
||||
unitId['name']?.toString().toLowerCase() ??
|
||||
unitId['satuan_waktu']?.toString().toLowerCase() ??
|
||||
'unit';
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return 'unit';
|
||||
}
|
||||
|
||||
// Helper method to log time unit details
|
||||
void _logTimeUnitDetails(
|
||||
String packageName,
|
||||
List<Map<String, dynamic>> timeUnits,
|
||||
) {
|
||||
debugPrint('\n📦 [DEBUG] Package: $packageName');
|
||||
debugPrint('🔄 Found ${timeUnits.length} time units:');
|
||||
|
||||
for (var i = 0; i < timeUnits.length; i++) {
|
||||
final unit = timeUnits[i];
|
||||
debugPrint('\n ⏱️ Time Unit #${i + 1}:');
|
||||
|
||||
// Log all available keys and values
|
||||
debugPrint(' ├─ All fields: $unit');
|
||||
|
||||
// Log specific fields we're interested in
|
||||
unit.forEach((key, value) {
|
||||
debugPrint(' ├─ $key: $value (${value.runtimeType})');
|
||||
});
|
||||
|
||||
// Special handling for satuan_waktu if it's a map
|
||||
if (unit['satuan_waktu'] is Map) {
|
||||
final satuanWaktu = unit['satuan_waktu'] as Map;
|
||||
debugPrint(' └─ satuan_waktu details:');
|
||||
satuanWaktu.forEach((k, v) {
|
||||
debugPrint(' ├─ $k: $v (${v.runtimeType})');
|
||||
});
|
||||
}
|
||||
}
|
||||
debugPrint('\n');
|
||||
}
|
||||
|
||||
Widget _buildPaketCard(BuildContext context, dynamic paket) {
|
||||
// Handle both Map and PaketModel for backward compatibility
|
||||
final isPaketModel = paket is PaketModel;
|
||||
|
||||
debugPrint('\n🔍 [_buildPaketCard] Paket type: ${paket.runtimeType}');
|
||||
debugPrint('📋 Paket data: $paket');
|
||||
|
||||
// Extract status based on type
|
||||
final String status =
|
||||
isPaketModel
|
||||
? (paket.status?.toString().capitalizeFirst ?? 'Tidak Diketahui')
|
||||
: (paket['status']?.toString().capitalizeFirst ??
|
||||
'Tidak Diketahui');
|
||||
|
||||
debugPrint('🏷️ Extracted status: $status (isPaketModel: $isPaketModel)');
|
||||
|
||||
// Extract availability based on type
|
||||
final bool isAvailable =
|
||||
isPaketModel
|
||||
? (paket.kuantitas > 0)
|
||||
: ((paket['kuantitas'] as int?) ?? 0) > 0;
|
||||
|
||||
final String nama =
|
||||
isPaketModel
|
||||
? paket.nama
|
||||
: (paket['nama']?.toString() ?? 'Paket Tanpa Nama');
|
||||
|
||||
// Debug package info
|
||||
debugPrint('\n📦 [PACKAGE] ${paket.runtimeType} - $nama');
|
||||
debugPrint('├─ isPaketModel: $isPaketModel');
|
||||
debugPrint('├─ Available: $isAvailable');
|
||||
|
||||
// Get the first rental time unit price if available, otherwise use the base price
|
||||
final dynamic harga;
|
||||
if (isPaketModel) {
|
||||
if (paket.satuanWaktuSewa.isNotEmpty) {
|
||||
_logTimeUnitDetails(nama, paket.satuanWaktuSewa);
|
||||
|
||||
// Get the first time unit with its price
|
||||
final firstUnit = paket.satuanWaktuSewa.first;
|
||||
final firstUnitPrice = firstUnit['harga'];
|
||||
|
||||
debugPrint('💰 First time unit price: $firstUnitPrice');
|
||||
debugPrint('⏱️ First time unit ID: ${firstUnit['satuan_waktu_id']}');
|
||||
debugPrint('📝 First time unit details: $firstUnit');
|
||||
|
||||
// Always use the first time unit's price if available
|
||||
harga = firstUnitPrice ?? 0;
|
||||
} else {
|
||||
debugPrint('⚠️ No time units found for package: $nama');
|
||||
debugPrint('ℹ️ Using base price: ${paket.harga}');
|
||||
harga = paket.harga;
|
||||
}
|
||||
} else {
|
||||
// For non-PaketModel (Map) data
|
||||
if (isPaketModel && paket.satuanWaktuSewa.isNotEmpty) {
|
||||
final firstUnit = paket.satuanWaktuSewa.first;
|
||||
final firstUnitPrice = firstUnit['harga'];
|
||||
debugPrint('💰 [MAP] First time unit price: $firstUnitPrice');
|
||||
harga = firstUnitPrice ?? 0;
|
||||
} else {
|
||||
debugPrint('⚠️ [MAP] No time units found for package: $nama');
|
||||
debugPrint('ℹ️ [MAP] Using base price: ${paket['harga']}');
|
||||
harga = paket['harga'] ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('💵 Final price being used: $harga\n');
|
||||
|
||||
// Get the main photo URL
|
||||
final String? foto =
|
||||
isPaketModel
|
||||
? (paket.images?.isNotEmpty == true
|
||||
? paket.images!.first
|
||||
: paket.foto_paket)
|
||||
: (paket['foto_paket']?.toString() ??
|
||||
(paket['foto'] is String ? paket['foto'] : null));
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
@ -196,22 +380,83 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
child: Row(
|
||||
children: [
|
||||
// Paket image or icon
|
||||
Container(
|
||||
SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
bottomLeft: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
_getPaketIcon(paket['kategori']),
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
size: 32,
|
||||
),
|
||||
child:
|
||||
foto != null && foto.isNotEmpty
|
||||
? Image.network(
|
||||
foto,
|
||||
width: 80,
|
||||
height: 80,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) => Container(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
_getPaketIcon(
|
||||
_getTimeUnitName(
|
||||
isPaketModel
|
||||
? (paket
|
||||
.satuanWaktuSewa
|
||||
.isNotEmpty
|
||||
? paket
|
||||
.satuanWaktuSewa
|
||||
.first['satuan_waktu_id'] ??
|
||||
'hari'
|
||||
: 'hari')
|
||||
: (paket['satuanWaktuSewa'] !=
|
||||
null &&
|
||||
paket['satuanWaktuSewa']
|
||||
.isNotEmpty
|
||||
? paket['satuanWaktuSewa'][0]['satuan_waktu_id']
|
||||
?.toString() ??
|
||||
'hari'
|
||||
: 'hari'),
|
||||
),
|
||||
),
|
||||
color: AppColorsPetugas.navyBlue
|
||||
.withOpacity(0.5),
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
_getPaketIcon(
|
||||
_getTimeUnitName(
|
||||
isPaketModel
|
||||
? (paket.satuanWaktuSewa.isNotEmpty
|
||||
? paket
|
||||
.satuanWaktuSewa
|
||||
.first['satuan_waktu_id'] ??
|
||||
'hari'
|
||||
: 'hari')
|
||||
: (paket['satuanWaktuSewa'] != null &&
|
||||
paket['satuanWaktuSewa']
|
||||
.isNotEmpty
|
||||
? paket['satuanWaktuSewa'][0]['satuan_waktu_id']
|
||||
?.toString() ??
|
||||
'hari'
|
||||
: 'hari'),
|
||||
),
|
||||
),
|
||||
color: AppColorsPetugas.navyBlue.withOpacity(
|
||||
0.5,
|
||||
),
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -228,9 +473,10 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Package name
|
||||
Text(
|
||||
paket['nama'],
|
||||
style: TextStyle(
|
||||
nama,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
@ -239,13 +485,119 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Rp ${_formatPrice(paket['harga'])}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
// Prices with time units
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final List<Map<String, dynamic>> timeUnits =
|
||||
[];
|
||||
|
||||
// Get all time units
|
||||
if (isPaketModel &&
|
||||
paket.satuanWaktuSewa.isNotEmpty) {
|
||||
timeUnits.addAll(paket.satuanWaktuSewa);
|
||||
} else if (!isPaketModel &&
|
||||
paket['satuanWaktuSewa'] != null &&
|
||||
paket['satuanWaktuSewa'].isNotEmpty) {
|
||||
timeUnits.addAll(
|
||||
List<Map<String, dynamic>>.from(
|
||||
paket['satuanWaktuSewa'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// If no time units, show nothing
|
||||
if (timeUnits.isEmpty)
|
||||
return const SizedBox.shrink();
|
||||
|
||||
// Filter out time units with price 0 or null
|
||||
final validTimeUnits =
|
||||
timeUnits.where((unit) {
|
||||
final price =
|
||||
unit['harga'] is int
|
||||
? unit['harga']
|
||||
: int.tryParse(
|
||||
unit['harga']
|
||||
?.toString() ??
|
||||
'0',
|
||||
) ??
|
||||
0;
|
||||
return price > 0;
|
||||
}).toList();
|
||||
|
||||
if (validTimeUnits.isEmpty)
|
||||
return const SizedBox.shrink();
|
||||
|
||||
return Column(
|
||||
children:
|
||||
validTimeUnits
|
||||
.asMap()
|
||||
.entries
|
||||
.map((entry) {
|
||||
final index = entry.key;
|
||||
final unit = entry.value;
|
||||
final unitPrice =
|
||||
unit['harga'] is int
|
||||
? unit['harga']
|
||||
: int.tryParse(
|
||||
unit['harga']
|
||||
?.toString() ??
|
||||
'0',
|
||||
) ??
|
||||
0;
|
||||
final unitName = _getTimeUnitName(
|
||||
unit['satuan_waktu_id'],
|
||||
);
|
||||
final isFirst = index == 0;
|
||||
|
||||
if (unitPrice <= 0)
|
||||
return const SizedBox.shrink();
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
'Rp ${_formatPrice(unitPrice)}/$unitName',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color:
|
||||
AppColorsPetugas
|
||||
.textSecondary,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow:
|
||||
TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
})
|
||||
.where(
|
||||
(widget) => widget is! SizedBox,
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (!isPaketModel &&
|
||||
paket['harga'] != null &&
|
||||
(paket['harga'] is int
|
||||
? paket['harga']
|
||||
: int.tryParse(
|
||||
paket['harga']?.toString() ??
|
||||
'0',
|
||||
) ??
|
||||
0) >
|
||||
0) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Rp ${_formatPrice(paket['harga'])}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -258,25 +610,31 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isAvailable
|
||||
status.toLowerCase() == 'tersedia'
|
||||
? AppColorsPetugas.successLight
|
||||
: status.toLowerCase() == 'pemeliharaan'
|
||||
? AppColorsPetugas.warningLight
|
||||
: AppColorsPetugas.errorLight,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color:
|
||||
isAvailable
|
||||
status.toLowerCase() == 'tersedia'
|
||||
? AppColorsPetugas.success
|
||||
: status.toLowerCase() == 'pemeliharaan'
|
||||
? AppColorsPetugas.warning
|
||||
: AppColorsPetugas.error,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
isAvailable ? 'Aktif' : 'Nonaktif',
|
||||
status,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color:
|
||||
isAvailable
|
||||
status.toLowerCase() == 'tersedia'
|
||||
? AppColorsPetugas.success
|
||||
: status.toLowerCase() == 'pemeliharaan'
|
||||
? AppColorsPetugas.warning
|
||||
: AppColorsPetugas.error,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
@ -290,9 +648,12 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
// Edit icon
|
||||
GestureDetector(
|
||||
onTap:
|
||||
() => _showAddEditPaketDialog(
|
||||
context,
|
||||
paket: paket,
|
||||
() => Get.toNamed(
|
||||
Routes.PETUGAS_TAMBAH_PAKET,
|
||||
arguments: {
|
||||
'isEditing': true,
|
||||
'paket': paket,
|
||||
},
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(5),
|
||||
@ -350,33 +711,42 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
);
|
||||
}
|
||||
|
||||
String _formatPrice(dynamic price) {
|
||||
if (price == null) return '0';
|
||||
|
||||
// Convert the price to string and handle formatting
|
||||
String priceStr = price.toString();
|
||||
|
||||
// Add thousand separators
|
||||
final RegExp reg = RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))');
|
||||
String formatted = priceStr.replaceAllMapped(reg, (Match m) => '${m[1]}.');
|
||||
|
||||
return formatted;
|
||||
// Add this helper method to get color based on status
|
||||
Color _getStatusColor(String status) {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'aktif':
|
||||
return AppColorsPetugas.success;
|
||||
case 'tidak aktif':
|
||||
case 'nonaktif':
|
||||
return AppColorsPetugas.error;
|
||||
case 'dalam perbaikan':
|
||||
case 'maintenance':
|
||||
return AppColorsPetugas.warning;
|
||||
case 'tersedia':
|
||||
return AppColorsPetugas.success;
|
||||
case 'pemeliharaan':
|
||||
return AppColorsPetugas.warning;
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getPaketIcon(String? category) {
|
||||
if (category == null) return Icons.category;
|
||||
IconData _getPaketIcon(String? timeUnit) {
|
||||
if (timeUnit == null) return Icons.access_time;
|
||||
|
||||
switch (category.toLowerCase()) {
|
||||
case 'bulanan':
|
||||
return Icons.calendar_month;
|
||||
case 'tahunan':
|
||||
switch (timeUnit.toLowerCase()) {
|
||||
case 'jam':
|
||||
return Icons.access_time;
|
||||
case 'hari':
|
||||
return Icons.calendar_today;
|
||||
case 'premium':
|
||||
return Icons.star;
|
||||
case 'bisnis':
|
||||
return Icons.business;
|
||||
case 'minggu':
|
||||
return Icons.date_range;
|
||||
case 'bulan':
|
||||
return Icons.calendar_month;
|
||||
case 'tahun':
|
||||
return Icons.calendar_view_month;
|
||||
default:
|
||||
return Icons.category;
|
||||
return Icons.access_time;
|
||||
}
|
||||
}
|
||||
|
||||
@ -426,7 +796,27 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showPaketDetails(BuildContext context, Map<String, dynamic> paket) {
|
||||
void _showPaketDetails(BuildContext context, dynamic paket) {
|
||||
// Handle both Map and PaketModel for backward compatibility
|
||||
final isPaketModel = paket is PaketModel;
|
||||
final String nama =
|
||||
isPaketModel
|
||||
? paket.nama
|
||||
: (paket['nama']?.toString() ?? 'Paket Tanpa Nama');
|
||||
final String? deskripsi =
|
||||
isPaketModel ? paket.deskripsi : paket['deskripsi']?.toString();
|
||||
final bool isAvailable =
|
||||
isPaketModel
|
||||
? (paket.kuantitas > 0)
|
||||
: ((paket['kuantitas'] as int?) ?? 0) > 0;
|
||||
final dynamic harga =
|
||||
isPaketModel
|
||||
? (paket.satuanWaktuSewa.isNotEmpty
|
||||
? paket.satuanWaktuSewa.first['harga']
|
||||
: paket.harga)
|
||||
: (paket['harga'] ?? 0);
|
||||
// Items are not part of the PaketModel, so we'll use an empty list
|
||||
final List<Map<String, dynamic>> items = [];
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
@ -448,7 +838,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
paket['nama'],
|
||||
nama,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -473,16 +863,15 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildDetailItem('Kategori', paket['kategori']),
|
||||
_buildDetailItem(
|
||||
'Harga',
|
||||
controller.formatPrice(paket['harga']),
|
||||
'Rp ${_formatPrice(harga)}',
|
||||
),
|
||||
_buildDetailItem(
|
||||
'Status',
|
||||
paket['tersedia'] ? 'Tersedia' : 'Tidak Tersedia',
|
||||
isAvailable ? 'Tersedia' : 'Tidak Tersedia',
|
||||
),
|
||||
_buildDetailItem('Deskripsi', paket['deskripsi']),
|
||||
_buildDetailItem('Deskripsi', deskripsi ?? '-'),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -502,11 +891,11 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
child: ListView.separated(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount: paket['items'].length,
|
||||
itemCount: items.length,
|
||||
separatorBuilder:
|
||||
(context, index) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final item = paket['items'][index];
|
||||
final item = items[index];
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: AppColorsPetugas.babyBlue,
|
||||
@ -601,10 +990,11 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddEditPaketDialog(
|
||||
BuildContext context, {
|
||||
Map<String, dynamic>? paket,
|
||||
}) {
|
||||
void _showAddEditPaketDialog(BuildContext context, {dynamic paket}) {
|
||||
// Handle both Map and PaketModel for backward compatibility
|
||||
final isPaketModel = paket is PaketModel;
|
||||
final String? id = isPaketModel ? paket.id : paket?['id'];
|
||||
final String title = id == null ? 'Tambah Paket' : 'Edit Paket';
|
||||
final isEditing = paket != null;
|
||||
|
||||
// This would be implemented with proper form validation in a real app
|
||||
@ -613,7 +1003,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
isEditing ? 'Edit Paket' : 'Tambah Paket Baru',
|
||||
title,
|
||||
style: TextStyle(color: AppColorsPetugas.navyBlue),
|
||||
),
|
||||
content: const Text(
|
||||
@ -652,10 +1042,13 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteConfirmation(
|
||||
BuildContext context,
|
||||
Map<String, dynamic> paket,
|
||||
) {
|
||||
void _showDeleteConfirmation(BuildContext context, dynamic paket) {
|
||||
// Handle both Map and PaketModel for backward compatibility
|
||||
final isPaketModel = paket is PaketModel;
|
||||
final String id = isPaketModel ? paket.id : (paket['id']?.toString() ?? '');
|
||||
final String nama =
|
||||
isPaketModel ? paket.nama : (paket['nama']?.toString() ?? 'Paket');
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
@ -664,9 +1057,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
'Konfirmasi Hapus',
|
||||
style: TextStyle(color: AppColorsPetugas.navyBlue),
|
||||
),
|
||||
content: Text(
|
||||
'Apakah Anda yakin ingin menghapus paket "${paket['nama']}"?',
|
||||
),
|
||||
content: Text('Apakah Anda yakin ingin menghapus paket "$nama"?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
@ -678,7 +1069,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
controller.deletePaket(paket['id']);
|
||||
controller.deletePaket(id);
|
||||
Get.snackbar(
|
||||
'Paket Dihapus',
|
||||
'Paket berhasil dihapus dari sistem',
|
||||
|
@ -6,6 +6,7 @@ import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||
import '../widgets/petugas_side_navbar.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import 'petugas_detail_sewa_view.dart';
|
||||
import '../../../data/models/rental_booking_model.dart';
|
||||
|
||||
class PetugasSewaView extends StatefulWidget {
|
||||
const PetugasSewaView({Key? key}) : super(key: key);
|
||||
@ -160,6 +161,10 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
}
|
||||
|
||||
Widget _buildSearchSection() {
|
||||
// Tambahkan controller untuk TextField agar bisa dikosongkan
|
||||
final TextEditingController searchController = TextEditingController(
|
||||
text: controller.searchQuery.value,
|
||||
);
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
|
||||
decoration: BoxDecoration(
|
||||
@ -173,9 +178,9 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
onChanged: (value) {
|
||||
controller.setSearchQuery(value);
|
||||
controller.setOrderIdQuery(value);
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cari nama warga atau ID pesanan...',
|
||||
@ -204,10 +209,21 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
isDense: true,
|
||||
suffixIcon: Icon(
|
||||
Icons.tune_rounded,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
size: 20,
|
||||
suffixIcon: Obx(
|
||||
() =>
|
||||
controller.searchQuery.value.isNotEmpty
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
controller.setSearchQuery('');
|
||||
},
|
||||
)
|
||||
: SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -241,17 +257,44 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
final filteredList =
|
||||
status == 'Semua'
|
||||
? controller.filteredSewaList
|
||||
: status == 'Menunggu Pembayaran'
|
||||
? controller.sewaList
|
||||
.where(
|
||||
(sewa) =>
|
||||
sewa.status.toUpperCase() == 'MENUNGGU PEMBAYARAN' ||
|
||||
sewa.status.toUpperCase() == 'PEMBAYARAN DENDA',
|
||||
)
|
||||
.toList()
|
||||
: status == 'Periksa Pembayaran'
|
||||
? controller.sewaList
|
||||
.where(
|
||||
(sewa) =>
|
||||
sewa['status'] == 'Periksa Pembayaran' ||
|
||||
sewa['status'] == 'Pembayaran Denda' ||
|
||||
sewa['status'] == 'Periksa Denda',
|
||||
sewa.status.toUpperCase() == 'PERIKSA PEMBAYARAN' ||
|
||||
sewa.status.toUpperCase() == 'PERIKSA PEMBAYARAN DENDA',
|
||||
)
|
||||
.toList()
|
||||
: status == 'Diterima'
|
||||
? controller.sewaList
|
||||
.where((sewa) => sewa.status.toUpperCase() == 'DITERIMA')
|
||||
.toList()
|
||||
: status == 'Aktif'
|
||||
? controller.sewaList
|
||||
.where((sewa) => sewa.status.toUpperCase() == 'AKTIF')
|
||||
.toList()
|
||||
: status == 'Dikembalikan'
|
||||
? controller.sewaList
|
||||
.where((sewa) => sewa.status.toUpperCase() == 'DIKEMBALIKAN')
|
||||
.toList()
|
||||
: status == 'Selesai'
|
||||
? controller.sewaList
|
||||
.where((sewa) => sewa.status.toUpperCase() == 'SELESAI')
|
||||
.toList()
|
||||
: status == 'Dibatalkan'
|
||||
? controller.sewaList
|
||||
.where((sewa) => sewa.status.toUpperCase() == 'DIBATALKAN')
|
||||
.toList()
|
||||
: controller.sewaList
|
||||
.where((sewa) => sewa['status'] == status)
|
||||
.where((sewa) => sewa.status == status)
|
||||
.toList();
|
||||
|
||||
if (filteredList.isEmpty) {
|
||||
@ -313,40 +356,25 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildSewaCard(BuildContext context, Map<String, dynamic> sewa) {
|
||||
final statusColor = controller.getStatusColor(sewa['status']);
|
||||
final status = sewa['status'];
|
||||
Widget _buildSewaCard(BuildContext context, SewaModel sewa) {
|
||||
final statusColor = controller.getStatusColor(sewa.status);
|
||||
final status = sewa.status;
|
||||
|
||||
// Get appropriate icon for status
|
||||
IconData statusIcon;
|
||||
switch (status) {
|
||||
case 'Menunggu Pembayaran':
|
||||
statusIcon = Icons.payments_outlined;
|
||||
break;
|
||||
case 'Periksa Pembayaran':
|
||||
statusIcon = Icons.fact_check_outlined;
|
||||
break;
|
||||
case 'Diterima':
|
||||
statusIcon = Icons.check_circle_outlined;
|
||||
break;
|
||||
case 'Pembayaran Denda':
|
||||
statusIcon = Icons.money_off_csred_outlined;
|
||||
break;
|
||||
case 'Periksa Denda':
|
||||
statusIcon = Icons.assignment_late_outlined;
|
||||
break;
|
||||
case 'Dikembalikan':
|
||||
statusIcon = Icons.assignment_return_outlined;
|
||||
break;
|
||||
case 'Selesai':
|
||||
statusIcon = Icons.task_alt_outlined;
|
||||
break;
|
||||
case 'Dibatalkan':
|
||||
statusIcon = Icons.cancel_outlined;
|
||||
break;
|
||||
default:
|
||||
statusIcon = Icons.help_outline_rounded;
|
||||
}
|
||||
IconData statusIcon = controller.getStatusIcon(status);
|
||||
|
||||
// Flag untuk membedakan tipe pesanan
|
||||
final bool isAset = sewa.tipePesanan == 'tunggal';
|
||||
final bool isPaket = sewa.tipePesanan == 'paket';
|
||||
|
||||
// Pilih nama aset/paket
|
||||
final String namaAsetAtauPaket =
|
||||
isAset
|
||||
? (sewa.asetNama ?? '-')
|
||||
: (isPaket ? (sewa.paketNama ?? '-') : '-');
|
||||
// Pilih foto aset/paket jika ingin digunakan
|
||||
final String? fotoAsetAtauPaket =
|
||||
isAset ? sewa.asetFoto : (isPaket ? sewa.paketFoto : null);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
@ -370,6 +398,35 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Status header inside the card
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.12),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Icon(statusIcon, size: 16, color: statusColor),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
status,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 0),
|
||||
child: Row(
|
||||
@ -378,14 +435,22 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundColor: AppColorsPetugas.babyBlueLight,
|
||||
child: Text(
|
||||
sewa['nama_warga'].substring(0, 1).toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
backgroundImage:
|
||||
(sewa.wargaAvatar != null &&
|
||||
sewa.wargaAvatar.isNotEmpty)
|
||||
? NetworkImage(sewa.wargaAvatar)
|
||||
: null,
|
||||
child:
|
||||
(sewa.wargaAvatar == null || sewa.wargaAvatar.isEmpty)
|
||||
? Text(
|
||||
sewa.wargaNama.substring(0, 1).toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
@ -395,55 +460,22 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
sewa['nama_warga'],
|
||||
sewa.wargaNama,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
statusIcon,
|
||||
size: 12,
|
||||
color: statusColor,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
status,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'#${sewa['order_id']}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
Text(
|
||||
'Tanggal Pesan: ' +
|
||||
(sewa.tanggalPemesanan != null
|
||||
? '${sewa.tanggalPemesanan.day.toString().padLeft(2, '0')}-${sewa.tanggalPemesanan.month.toString().padLeft(2, '0')}-${sewa.tanggalPemesanan.year}'
|
||||
: '-'),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -460,7 +492,7 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
controller.formatPrice(sewa['total_biaya']),
|
||||
controller.formatPrice(sewa.totalTagihan),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -481,33 +513,51 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
child: Divider(height: 1, color: Colors.grey.shade200),
|
||||
),
|
||||
|
||||
// Asset details
|
||||
// Asset/Paket details
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 16),
|
||||
child: Row(
|
||||
children: [
|
||||
// Asset icon
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
// Asset/Paket image or icon
|
||||
if (fotoAsetAtauPaket != null &&
|
||||
fotoAsetAtauPaket.isNotEmpty)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Image.network(
|
||||
fotoAsetAtauPaket,
|
||||
width: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) => Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 28,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 20,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 20,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Asset name and duration
|
||||
// Asset/Paket name and duration
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
sewa['nama_aset'],
|
||||
namaAsetAtauPaket,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
@ -524,7 +574,7 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${sewa['tanggal_mulai']} - ${sewa['tanggal_selesai']}',
|
||||
'${sewa.waktuMulai.toIso8601String().substring(0, 10)} - ${sewa.waktuSelesai.toIso8601String().substring(0, 10)}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
@ -9,32 +10,51 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Tambah Aset',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [_buildHeaderSection(), _buildFormSection(context)],
|
||||
return GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Obx(() => Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
controller.isEditing.value ? 'Edit Aset' : 'Tambah Aset',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: _buildBottomBar(),
|
||||
body: Stack(
|
||||
children: [
|
||||
SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeaderSection(),
|
||||
_buildFormSection(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (controller.isLoading.value)
|
||||
Container(
|
||||
color: Colors.black54,
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(AppColorsPetugas.blueGrotto),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: _buildBottomBar(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderSection() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.only(top: 10, left: 20, right: 20, bottom: 5), // Reduced padding
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColorsPetugas.navyBlue, AppColorsPetugas.blueGrotto],
|
||||
@ -42,50 +62,8 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
color: Colors.white,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Informasi Aset Baru',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Isi data dengan lengkap untuk menambahkan aset',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
child: Container(
|
||||
height: 12, // Further reduced height
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -131,69 +109,36 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
||||
_buildImageUploader(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Category Section
|
||||
_buildSectionHeader(icon: Icons.category, title: 'Kategori & Status'),
|
||||
// Status Section
|
||||
_buildSectionHeader(icon: Icons.check_circle, title: 'Status'),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Category and Status as cards
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildCategorySelect(
|
||||
title: 'Kategori',
|
||||
options: controller.categoryOptions,
|
||||
selectedOption: controller.selectedCategory,
|
||||
onChanged: controller.setCategory,
|
||||
icon: Icons.inventory_2,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildCategorySelect(
|
||||
title: 'Status',
|
||||
options: controller.statusOptions,
|
||||
selectedOption: controller.selectedStatus,
|
||||
onChanged: controller.setStatus,
|
||||
icon: Icons.check_circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
// Status card
|
||||
_buildCategorySelect(
|
||||
title: 'Status',
|
||||
options: controller.statusOptions,
|
||||
selectedOption: controller.selectedStatus,
|
||||
onChanged: controller.setStatus,
|
||||
icon: Icons.check_circle,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Quantity Section
|
||||
_buildSectionHeader(
|
||||
icon: Icons.format_list_numbered,
|
||||
title: 'Kuantitas & Pengukuran',
|
||||
title: 'Kuantitas',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Quantity fields
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: _buildTextField(
|
||||
label: 'Kuantitas',
|
||||
hint: 'Jumlah aset',
|
||||
controller: controller.quantityController,
|
||||
isRequired: true,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
prefixIcon: Icons.numbers,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: _buildTextField(
|
||||
label: 'Satuan Ukur',
|
||||
hint: 'contoh: Unit, Buah',
|
||||
controller: controller.unitOfMeasureController,
|
||||
prefixIcon: Icons.straighten,
|
||||
),
|
||||
),
|
||||
],
|
||||
// Quantity field
|
||||
_buildTextField(
|
||||
label: 'Kuantitas',
|
||||
hint: 'Jumlah aset',
|
||||
controller: controller.quantityController,
|
||||
isRequired: true,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
prefixIcon: Icons.numbers,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
@ -654,6 +599,114 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
||||
);
|
||||
}
|
||||
|
||||
// Show image source options
|
||||
void _showImageSourceOptions() {
|
||||
Get.bottomSheet(
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Pilih Sumber Gambar',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildImageSourceOption(
|
||||
icon: Icons.camera_alt,
|
||||
label: 'Kamera',
|
||||
onTap: () {
|
||||
Get.back();
|
||||
controller.pickImageFromCamera();
|
||||
},
|
||||
),
|
||||
_buildImageSourceOption(
|
||||
icon: Icons.photo_library,
|
||||
label: 'Galeri',
|
||||
onTap: () {
|
||||
Get.back();
|
||||
controller.pickImageFromGallery();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
isScrollControlled: true,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImageSourceOption({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 70,
|
||||
height: 70,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueBright,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 30,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColorsPetugas.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImageUploader() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@ -696,7 +749,7 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
||||
children: [
|
||||
// Add button
|
||||
GestureDetector(
|
||||
onTap: () => controller.addSampleImage(),
|
||||
onTap: _showImageSourceOptions,
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
@ -732,69 +785,107 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
||||
),
|
||||
|
||||
// Image previews
|
||||
...controller.selectedImages.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
return Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
...List<Widget>.generate(
|
||||
controller.selectedImages.length,
|
||||
(index) => Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
),
|
||||
child: Obx(
|
||||
() {
|
||||
// Check if we have a network URL for this index
|
||||
if (index < controller.networkImageUrls.length &&
|
||||
controller.networkImageUrls[index].isNotEmpty) {
|
||||
// Display network image
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
controller.networkImageUrls[index],
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return const Center(
|
||||
child: Icon(Icons.error_outline, color: Colors.red),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Display local file
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: FutureBuilder<File>(
|
||||
future: File(controller.selectedImages[index].path).exists().then((exists) {
|
||||
if (exists) {
|
||||
return File(controller.selectedImages[index].path);
|
||||
} else {
|
||||
return File(controller.selectedImages[index].path);
|
||||
}
|
||||
}),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data != null) {
|
||||
return Image.file(
|
||||
snapshot.data!,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
color: Colors.grey[200],
|
||||
child: const Icon(Icons.broken_image, color: Colors.grey),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return Container(
|
||||
color: Colors.grey[200],
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: GestureDetector(
|
||||
onTap: () => controller.removeImage(index),
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.image,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 40,
|
||||
),
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black26,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
size: 16,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: GestureDetector(
|
||||
onTap: () => controller.removeImage(index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 3,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
color: AppColorsPetugas.error,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
).toList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -850,7 +941,9 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.save),
|
||||
label: Text(isSubmitting ? 'Menyimpan...' : 'Simpan Aset'),
|
||||
label: Obx(() => Text(
|
||||
isSubmitting ? 'Menyimpan...' : (controller.isEditing.value ? 'Simpan Perubahan' : 'Simpan Aset'),
|
||||
)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
foregroundColor: Colors.white,
|
||||
|
@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../controllers/petugas_tambah_paket_controller.dart';
|
||||
import 'dart:io';
|
||||
|
||||
class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
const PetugasTambahPaketView({Key? key}) : super(key: key);
|
||||
@ -12,9 +13,11 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Tambah Paket',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
title: Obx(
|
||||
() => Text(
|
||||
controller.isEditing.value ? 'Edit Paket' : 'Tambah Paket',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
backgroundColor: AppColorsPetugas.navyBlue,
|
||||
elevation: 0,
|
||||
@ -24,7 +27,7 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [_buildHeaderSection(), _buildFormSection(context)],
|
||||
children: [_buildFormSection(context)],
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -32,64 +35,6 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderSection() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColorsPetugas.navyBlue, AppColorsPetugas.blueGrotto],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.category,
|
||||
color: Colors.white,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Informasi Paket Baru',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Isi data dengan lengkap untuk menambahkan paket',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFormSection(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
@ -132,22 +77,22 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Category Section
|
||||
_buildSectionHeader(icon: Icons.category, title: 'Kategori & Status'),
|
||||
_buildSectionHeader(icon: Icons.category, title: 'Status'),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Category and Status as cards
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildCategorySelect(
|
||||
title: 'Kategori',
|
||||
options: controller.categoryOptions,
|
||||
selectedOption: controller.selectedCategory,
|
||||
onChanged: controller.setCategory,
|
||||
icon: Icons.category,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Expanded(
|
||||
// child: _buildCategorySelect(
|
||||
// title: 'Kategori',
|
||||
// options: controller.categoryOptions,
|
||||
// selectedOption: controller.selectedCategory,
|
||||
// onChanged: controller.setCategory,
|
||||
// icon: Icons.category,
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildCategorySelect(
|
||||
title: 'Status',
|
||||
@ -161,24 +106,6 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Price Section
|
||||
_buildSectionHeader(
|
||||
icon: Icons.monetization_on,
|
||||
title: 'Harga Paket',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
label: 'Harga Paket',
|
||||
hint: 'Masukkan harga paket',
|
||||
controller: controller.priceController,
|
||||
isRequired: true,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
prefixText: 'Rp ',
|
||||
prefixIcon: Icons.payments,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Package Items Section
|
||||
_buildSectionHeader(
|
||||
icon: Icons.inventory_2,
|
||||
@ -186,6 +113,40 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildPackageItems(),
|
||||
const SizedBox(height: 24),
|
||||
_buildSectionHeader(
|
||||
icon: Icons.schedule,
|
||||
title: 'Opsi Waktu & Harga Sewa',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTimeOptionsCards(),
|
||||
const SizedBox(height: 16),
|
||||
Obx(
|
||||
() => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (controller.timeOptions['Per Jam']!.value)
|
||||
_buildPriceCard(
|
||||
title: 'Harga Per Jam',
|
||||
icon: Icons.timer,
|
||||
priceController: controller.pricePerHourController,
|
||||
maxController: controller.maxHourController,
|
||||
maxLabel: 'Maksimal Jam',
|
||||
),
|
||||
if (controller.timeOptions['Per Jam']!.value &&
|
||||
controller.timeOptions['Per Hari']!.value)
|
||||
const SizedBox(height: 16),
|
||||
if (controller.timeOptions['Per Hari']!.value)
|
||||
_buildPriceCard(
|
||||
title: 'Harga Per Hari',
|
||||
icon: Icons.calendar_today,
|
||||
priceController: controller.pricePerDayController,
|
||||
maxController: controller.maxDayController,
|
||||
maxLabel: 'Maksimal Hari',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
@ -310,7 +271,7 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Asset dropdown
|
||||
DropdownButtonFormField<int>(
|
||||
DropdownButtonFormField<String>(
|
||||
value: controller.selectedAsset.value,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Pilih Aset',
|
||||
@ -319,8 +280,8 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
hint: const Text('Pilih Aset'),
|
||||
items:
|
||||
controller.availableAssets.map((asset) {
|
||||
return DropdownMenuItem<int>(
|
||||
value: asset['id'] as int,
|
||||
return DropdownMenuItem<String>(
|
||||
value: asset['id'].toString(),
|
||||
child: Text(
|
||||
'${asset['nama']} (Stok: ${asset['stok']})',
|
||||
),
|
||||
@ -422,7 +383,7 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Asset dropdown
|
||||
DropdownButtonFormField<int>(
|
||||
DropdownButtonFormField<String>(
|
||||
value: controller.selectedAsset.value,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Pilih Aset',
|
||||
@ -431,8 +392,8 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
hint: const Text('Pilih Aset'),
|
||||
items:
|
||||
controller.availableAssets.map((asset) {
|
||||
return DropdownMenuItem<int>(
|
||||
value: asset['id'] as int,
|
||||
return DropdownMenuItem<String>(
|
||||
value: asset['id'].toString(),
|
||||
child: Text(
|
||||
'${asset['nama']} (Stok: ${asset['stok']})',
|
||||
),
|
||||
@ -757,7 +718,7 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
children: [
|
||||
// Add button
|
||||
GestureDetector(
|
||||
onTap: () => controller.addSampleImage(),
|
||||
onTap: _showImageSourceOptions,
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
@ -791,69 +752,82 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Image previews
|
||||
...controller.selectedImages.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
return Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
...List<Widget>.generate(controller.selectedImages.length, (
|
||||
index,
|
||||
) {
|
||||
final img = controller.selectedImages[index];
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
color: AppColorsPetugas.babyBlueLight,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.image,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
),
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: GestureDetector(
|
||||
onTap: () => controller.removeImage(index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 3,
|
||||
offset: const Offset(0, 1),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child:
|
||||
(img is String && img.startsWith('http'))
|
||||
? Image.network(
|
||||
img,
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) =>
|
||||
const Center(
|
||||
child: Icon(
|
||||
Icons.broken_image,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
)
|
||||
: (img is String)
|
||||
? Container(
|
||||
color: Colors.grey[200],
|
||||
child: const Icon(
|
||||
Icons.broken_image,
|
||||
color: Colors.grey,
|
||||
),
|
||||
)
|
||||
: Image.file(
|
||||
File(img.path),
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) =>
|
||||
const Center(
|
||||
child: Icon(
|
||||
Icons.broken_image,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
color: AppColorsPetugas.error,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: InkWell(
|
||||
onTap: () => controller.removeImage(index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
size: 18,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
@ -864,6 +838,104 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showImageSourceOptions() {
|
||||
Get.bottomSheet(
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Pilih Sumber Gambar',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildImageSourceOption(
|
||||
icon: Icons.camera_alt,
|
||||
label: 'Kamera',
|
||||
onTap: () {
|
||||
Get.back();
|
||||
controller.pickImageFromCamera();
|
||||
},
|
||||
),
|
||||
_buildImageSourceOption(
|
||||
icon: Icons.photo_library,
|
||||
label: 'Galeri',
|
||||
onTap: () {
|
||||
Get.back();
|
||||
controller.pickImageFromGallery();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImageSourceOption({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.babyBlueBright,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(icon, color: AppColorsPetugas.blueGrotto, size: 28),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomBar() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
@ -899,26 +971,37 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
final isSubmitting = controller.isSubmitting.value;
|
||||
return ElevatedButton.icon(
|
||||
onPressed:
|
||||
isValid && !isSubmitting ? controller.savePaket : null,
|
||||
controller.isFormChanged.value && !isSubmitting
|
||||
? controller.savePaket
|
||||
: null,
|
||||
icon:
|
||||
isSubmitting
|
||||
? SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.save),
|
||||
label: Text(isSubmitting ? 'Menyimpan...' : 'Simpan Paket'),
|
||||
label: Text(
|
||||
isSubmitting
|
||||
? 'Menyimpan...'
|
||||
: (controller.isEditing.value
|
||||
? 'Simpan Paket'
|
||||
: 'Tambah Paket'),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
textStyle: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
disabledBackgroundColor: AppColorsPetugas.textLight,
|
||||
),
|
||||
@ -929,4 +1012,226 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTimeOptionsCards() {
|
||||
return 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:
|
||||
controller.timeOptions.entries.map((entry) {
|
||||
final option = entry.key;
|
||||
final isSelected = entry.value;
|
||||
return Obx(
|
||||
() => Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => controller.toggleTimeOption(option),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isSelected.value
|
||||
? AppColorsPetugas.blueGrotto.withOpacity(
|
||||
0.1,
|
||||
)
|
||||
: Colors.grey.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
option == 'Per Jam'
|
||||
? Icons.hourglass_bottom
|
||||
: Icons.calendar_today,
|
||||
color:
|
||||
isSelected.value
|
||||
? AppColorsPetugas.blueGrotto
|
||||
: Colors.grey,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
option,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color:
|
||||
isSelected.value
|
||||
? AppColorsPetugas.navyBlue
|
||||
: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
option == 'Per Jam'
|
||||
? 'Sewa paket dengan basis perhitungan per jam'
|
||||
: 'Sewa paket dengan basis perhitungan per hari',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Checkbox(
|
||||
value: isSelected.value,
|
||||
onChanged:
|
||||
(_) => controller.toggleTimeOption(option),
|
||||
activeColor: AppColorsPetugas.blueGrotto,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPriceCard({
|
||||
required String title,
|
||||
required IconData icon,
|
||||
required TextEditingController priceController,
|
||||
required TextEditingController maxController,
|
||||
required String maxLabel,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColorsPetugas.babyBlue.withOpacity(0.5)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, size: 20, color: AppColorsPetugas.blueGrotto),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColorsPetugas.navyBlue,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Harga Sewa',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: priceController,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Masukkan harga',
|
||||
hintStyle: TextStyle(color: AppColorsPetugas.textLight),
|
||||
prefixText: 'Rp ',
|
||||
filled: true,
|
||||
fillColor: AppColorsPetugas.babyBlueBright,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
maxLabel,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColorsPetugas.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: maxController,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Opsional',
|
||||
hintStyle: TextStyle(color: AppColorsPetugas.textLight),
|
||||
filled: true,
|
||||
fillColor: AppColorsPetugas.babyBlueBright,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user