fitur petugas

This commit is contained in:
Andreas Malvino
2025-06-22 09:25:58 +07:00
parent c4dd4fdfa2
commit 8284c93aa5
48 changed files with 8688 additions and 3436 deletions

View File

@ -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(

View File

@ -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,

View File

@ -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',

View File

@ -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,

View File

@ -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,

View File

@ -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,
),
),
),
],
),
),
],
),
],
),
);
}
}