diff --git a/.metadata b/.metadata index d77a4e0..01965b7 100644 --- a/.metadata +++ b/.metadata @@ -18,21 +18,6 @@ migration: - platform: android create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf - - platform: ios - create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf - base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf - - platform: linux - create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf - base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf - - platform: macos - create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf - base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf - - platform: web - create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf - base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf - - platform: windows - create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf - base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf # User provided section diff --git a/lib/core/router.dart b/lib/core/router.dart index e5f3411..b2a1558 100644 --- a/lib/core/router.dart +++ b/lib/core/router.dart @@ -1,11 +1,25 @@ import 'package:rijig_mobile/core/utils/exportimportview.dart'; +import 'package:rijig_mobile/features/chat/presentation/screen/chatroom_screen.dart'; final router = GoRouter( routes: [ GoRoute(path: '/', builder: (context, state) => SplashScreen()), - GoRoute(path: '/trashview', builder: (context, state) => TestRequestPickScreen()), - GoRoute(path: '/ordersumary', builder: (context, state) => OrderSummaryScreen()), - GoRoute(path: '/pinsecureinput', builder: (context, state) => SecurityCodeScreen()), + // GoRoute( + // path: '/', + // builder: (context, state) => UploadKtpScreen(), + // ), + GoRoute( + path: '/trashview', + builder: (context, state) => TestRequestPickScreen(), + ), + GoRoute( + path: '/ordersumary', + builder: (context, state) => OrderSummaryScreen(), + ), + GoRoute( + path: '/pinsecureinput', + builder: (context, state) => SecurityCodeScreen(), + ), GoRoute( path: '/cmapview', builder: (context, state) => CollectorRouteMapScreen(), @@ -66,12 +80,42 @@ final router = GoRouter( // Rute untuk halaman-halaman utama GoRoute(path: '/home', builder: (context, state) => HomeScreen()), + GoRoute(path: '/chatlist', builder: (context, state) => ChatListScreen()), + // Router config + GoRoute( + path: '/chatroom/:contactId', + builder: (context, state) { + final contactName = state.uri.queryParameters['name'] ?? 'Unknown'; + final contactImage = state.uri.queryParameters['image'] ?? ''; + final isOnline = state.uri.queryParameters['online'] == 'true'; + + return ChatRoomScreen( + contactName: contactName, + contactImage: contactImage, + isOnline: isOnline, + ); + }, + ), + GoRoute( path: '/dataperforma', builder: (context, state) => DatavisualizedScreen(), ), GoRoute(path: '/activity', builder: (context, state) => ActivityScreen()), GoRoute(path: '/profil', builder: (context, state) => ProfilScreen()), + GoRoute(path: '/akunprofil', builder: (context, state) => AccountScreen()), + GoRoute(path: '/address', builder: (context, state) => AddressScreen()), + GoRoute( + path: '/addaddress', + builder: (context, state) => AddAddressScreen(), + ), + GoRoute( + path: '/editaddress', + builder: (context, state) { + dynamic address = state.extra; + return EditAddressScreen(address: address); + }, + ), GoRoute( path: '/aboutdetail', diff --git a/lib/core/utils/exportimportview.dart b/lib/core/utils/exportimportview.dart index 235f901..9ee7b0d 100644 --- a/lib/core/utils/exportimportview.dart +++ b/lib/core/utils/exportimportview.dart @@ -3,8 +3,12 @@ export 'package:go_router/go_router.dart'; export 'package:rijig_mobile/core/utils/navigation.dart'; export 'package:rijig_mobile/features/activity/presentation/screen/activity_screen.dart'; export 'package:rijig_mobile/features/home/presentation/screen/home_screen.dart'; +export 'package:rijig_mobile/features/home/notification/presentation/screen/notification_screen.dart'; export 'package:rijig_mobile/features/home/datavisualized/presentation/screen/datavisualized_screen.dart'; export 'package:rijig_mobile/features/profil/presentation/screen/profil_screen.dart'; +export 'package:rijig_mobile/features/profil/address/presentation/screen/address_screen.dart'; +export 'package:rijig_mobile/features/profil/address/presentation/screen/add_address_screen.dart'; +export 'package:rijig_mobile/features/profil/address/presentation/screen/edit_address_screen.dart'; export 'package:rijig_mobile/features/auth/presentation/screen/inputpin_screen.dart'; export 'package:rijig_mobile/features/auth/presentation/screen/login_screen.dart'; export 'package:rijig_mobile/features/auth/presentation/screen/otp_screen.dart'; @@ -22,6 +26,8 @@ export 'package:rijig_mobile/features/auth/presentation/screen/collector/cotp_sc export 'package:rijig_mobile/features/auth/presentation/screen/collector/clogin_screen.dart'; export 'package:rijig_mobile/features/home/presentation/screen/collector/pickup_history_screen.dart'; export 'package:rijig_mobile/features/pickup/presentation/screen/pickup_map_screen.dart'; +export 'package:rijig_mobile/features/chat/presentation/screen/chatlist_screen.dart'; +export 'package:rijig_mobile/features/profil/account/presentation/screen/account_screen.dart'; diff --git a/lib/features/activity/presentation/screen/activity_screen.dart b/lib/features/activity/presentation/screen/activity_screen.dart index 4ba52fd..bfd3544 100644 --- a/lib/features/activity/presentation/screen/activity_screen.dart +++ b/lib/features/activity/presentation/screen/activity_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:rijig_mobile/core/utils/guide.dart'; import 'package:rijig_mobile/widget/tabbar_custom.dart'; import 'package:rijig_mobile/widget/unhope_handler.dart'; +import 'package:timelines_plus/timelines_plus.dart'; class ActivityScreen extends StatefulWidget { const ActivityScreen({super.key}); @@ -11,6 +12,98 @@ class ActivityScreen extends StatefulWidget { } class _ActivityScreenState extends State { + // Data contoh untuk timeline + final List processActivities = [ + ActivityItem( + title: 'Pesanan Dikonfirmasi', + description: 'Pesanan Anda telah dikonfirmasi dan sedang diproses', + time: '10:30', + date: '15 Juni 2024', + status: ActivityStatus.completed, + icon: Icons.check_circle, + ), + ActivityItem( + title: 'Sedang Disiapkan', + description: 'Tim kami sedang menyiapkan pesanan Anda', + time: '11:15', + date: '15 Juni 2024', + status: ActivityStatus.inProgress, + icon: Icons.timer, + ), + ActivityItem( + title: 'Siap Dikirim', + description: 'Pesanan siap untuk dikirim ke alamat tujuan', + time: '', + date: '', + status: ActivityStatus.pending, + icon: Icons.local_shipping, + ), + ActivityItem( + title: 'Dalam Perjalanan', + description: 'Pesanan sedang dalam perjalanan menuju alamat Anda', + time: '', + date: '', + status: ActivityStatus.pending, + icon: Icons.directions_car, + ), + ActivityItem( + title: 'Pesanan Sampai', + description: 'Pesanan telah sampai di alamat tujuan', + time: '', + date: '', + status: ActivityStatus.pending, + icon: Icons.home, + ), + ]; + + // Data contoh untuk pesanan selesai + final List completedOrders = [ + CompletedOrder( + orderId: '#12340', + title: 'Paket Makanan Premium', + description: '2x Nasi Gudeg, 1x Es Teh Manis', + totalAmount: 'Rp 85.000', + completedDate: '12 Juni 2024', + completedTime: '14:30', + rating: 5, + customerNote: 'Makanan enak sekali, pengiriman cepat!', + ), + CompletedOrder( + orderId: '#12339', + title: 'Paket Minuman Segar', + description: '3x Jus Jeruk, 2x Smoothie Mangga', + totalAmount: 'Rp 65.000', + completedDate: '10 Juni 2024', + completedTime: '16:45', + rating: 4, + customerNote: 'Minuman segar, kemasan bagus', + ), + CompletedOrder( + orderId: '#12338', + title: 'Paket Snack Keluarga', + description: '1x Risoles, 2x Pastel, 1x Kopi', + totalAmount: 'Rp 45.000', + completedDate: '8 Juni 2024', + completedTime: '10:15', + rating: 5, + customerNote: '', + ), + ]; + + // Data contoh untuk pesanan dibatalkan + final List cancelledOrders = [ + CancelledOrder( + orderId: '#12337', + title: 'Paket Lunch Box', + description: '1x Nasi Ayam Geprek, 1x Es Jeruk', + totalAmount: 'Rp 35.000', + cancelledDate: '7 Juni 2024', + cancelledTime: '13:20', + cancelReason: 'Dibatalkan oleh pelanggan', + refundStatus: 'Dikembalikan', + ), + ]; + @override Widget build(BuildContext context) { return DefaultTabController( @@ -42,7 +135,7 @@ class _ActivityScreenState extends State { unselectedLabelColor: Colors.black54, tabs: [ TabItem(title: 'Proses', count: 6), - TabItem(title: 'Gak Eroh', count: 3), + TabItem(title: 'Selesai', count: 3), TabItem(title: 'Dibatalkan', count: 1), ], ), @@ -52,12 +145,845 @@ class _ActivityScreenState extends State { ), body: TabBarView( children: [ - Center(child: InfoStateWidget(type: InfoStateType.emptyData)), - Center(child: InfoStateWidget(type: InfoStateType.emptyData)), - Center(child: InfoStateWidget(type: InfoStateType.emptyData)), + _buildProcessTab(), + _buildCompletedTab(), + _buildCancelledTab(), ], ), ), ); } + + Widget _buildCompletedTab() { + return Container( + color: Colors.grey.shade50, + child: completedOrders.isEmpty + ? Center(child: InfoStateWidget(type: InfoStateType.emptyData)) + : ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: completedOrders.length, + itemBuilder: (context, index) { + final order = completedOrders[index]; + return _buildCompletedOrderCard(order); + }, + ), + ); + } + + Widget _buildCompletedOrderCard(CompletedOrder order) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha:0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header dengan status + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + order.orderId, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.grey.shade600, + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.green.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.check_circle, + size: 14, + color: Colors.green.shade700, + ), + const SizedBox(width: 4), + Text( + 'Selesai', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.green.shade700, + ), + ), + ], + ), + ), + ], + ), + + const SizedBox(height: 12), + + // Konten pesanan + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.shopping_bag, + color: Colors.green.shade600, + size: 24, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + order.title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + order.description, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 8), + Text( + 'Selesai pada ${order.completedDate} • ${order.completedTime}', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade500, + ), + ), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + order.totalAmount, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.green, + ), + ), + const SizedBox(height: 4), + _buildRatingStars(order.rating), + ], + ), + ], + ), + + // Customer note jika ada + if (order.customerNote.isNotEmpty) ...[ + const SizedBox(height: 12), + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue.shade100), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.comment, + size: 16, + color: Colors.blue.shade600, + ), + const SizedBox(width: 6), + Text( + 'Catatan Pelanggan:', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.blue.shade700, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + order.customerNote, + style: TextStyle( + fontSize: 13, + color: Colors.blue.shade700, + fontStyle: FontStyle.italic, + ), + ), + ], + ), + ), + ], + + const SizedBox(height: 12), + + // Action buttons + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () { + // Action untuk lihat detail + }, + icon: Icon( + Icons.visibility, + size: 16, + color: primaryColor, + ), + label: Text( + 'Lihat Detail', + style: TextStyle(color: primaryColor), + ), + style: OutlinedButton.styleFrom( + side: BorderSide(color: primaryColor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + onPressed: () { + // Action untuk pesan lagi + }, + icon: const Icon( + Icons.refresh, + size: 16, + color: Colors.white, + ), + label: const Text( + 'Pesan Lagi', + style: TextStyle(color: Colors.white), + ), + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildCancelledTab() { + return Container( + color: Colors.grey.shade50, + child: cancelledOrders.isEmpty + ? Center(child: InfoStateWidget(type: InfoStateType.emptyData)) + : ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: cancelledOrders.length, + itemBuilder: (context, index) { + final order = cancelledOrders[index]; + return _buildCancelledOrderCard(order); + }, + ), + ); + } + + Widget _buildCancelledOrderCard(CancelledOrder order) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.red.shade100), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha:0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header dengan status + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + order.orderId, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.grey.shade600, + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.red.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.cancel, + size: 14, + color: Colors.red.shade700, + ), + const SizedBox(width: 4), + Text( + 'Dibatalkan', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.red.shade700, + ), + ), + ], + ), + ), + ], + ), + + const SizedBox(height: 12), + + // Konten pesanan + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.shopping_bag_outlined, + color: Colors.red.shade600, + size: 24, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + order.title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.grey.shade700, + ), + ), + const SizedBox(height: 4), + Text( + order.description, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 8), + Text( + 'Dibatalkan pada ${order.cancelledDate} • ${order.cancelledTime}', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade500, + ), + ), + ], + ), + ), + Text( + order.totalAmount, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.grey.shade600, + decoration: TextDecoration.lineThrough, + ), + ), + ], + ), + + const SizedBox(height: 12), + + // Info pembatalan + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.orange.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.orange.shade100), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.info_outline, + size: 16, + color: Colors.orange.shade700, + ), + const SizedBox(width: 6), + Text( + 'Alasan Pembatalan:', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.orange.shade700, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + order.cancelReason, + style: TextStyle( + fontSize: 13, + color: Colors.orange.shade700, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.account_balance_wallet, + size: 16, + color: Colors.green.shade600, + ), + const SizedBox(width: 6), + Text( + 'Status Refund: ${order.refundStatus}', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.green.shade700, + ), + ), + ], + ), + ], + ), + ), + + const SizedBox(height: 12), + + // Action button + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: () { + // Action untuk pesan lagi + }, + icon: Icon( + Icons.refresh, + size: 16, + color: primaryColor, + ), + label: Text( + 'Pesan Lagi', + style: TextStyle(color: primaryColor), + ), + style: OutlinedButton.styleFrom( + side: BorderSide(color: primaryColor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildRatingStars(int rating) { + return Row( + mainAxisSize: MainAxisSize.min, + children: List.generate(5, (index) { + return Icon( + index < rating ? Icons.star : Icons.star_border, + size: 14, + color: Colors.orange, + ); + }), + ); + } + + Widget _buildProcessTab() { + return Container( + color: Colors.grey.shade50, + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header Card + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha:0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: primaryColor.withValues(alpha:0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.shopping_bag, + color: primaryColor, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Pesanan #12345', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + const SizedBox(height: 4), + Text( + 'Total: Rp 150.000', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.orange.shade100, + borderRadius: BorderRadius.circular(16), + ), + child: Text( + 'Dalam Proses', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.orange.shade700, + ), + ), + ), + ], + ), + ], + ), + ), + + const SizedBox(height: 24), + + // Timeline Section + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha:0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Status Pesanan', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + const SizedBox(height: 20), + _buildTimeline(), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildTimeline() { + return Timeline.tileBuilder( + theme: TimelineThemeData( + nodePosition: 0, + color: Colors.grey.shade300, + indicatorTheme: const IndicatorThemeData( + position: 0, + size: 20.0, + ), + connectorTheme: const ConnectorThemeData( + thickness: 2.0, + ), + ), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + builder: TimelineTileBuilder.connected( + connectionDirection: ConnectionDirection.before, + itemCount: processActivities.length, + contentsBuilder: (context, index) { + return _buildTimelineContent(processActivities[index]); + }, + indicatorBuilder: (context, index) { + return _buildTimelineIndicator(processActivities[index]); + }, + connectorBuilder: (context, index, type) { + return SolidLineConnector( + color: index < processActivities.length - 1 && + processActivities[index].status == ActivityStatus.completed + ? primaryColor + : Colors.grey.shade300, + ); + }, + ), + ); + } + + Widget _buildTimelineIndicator(ActivityItem item) { + Color indicatorColor; + Widget indicatorChild; + + switch (item.status) { + case ActivityStatus.completed: + indicatorColor = primaryColor; + indicatorChild = Icon( + Icons.check, + color: Colors.white, + size: 12, + ); + break; + case ActivityStatus.inProgress: + indicatorColor = Colors.orange; + indicatorChild = Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + ); + break; + case ActivityStatus.pending: + indicatorColor = Colors.grey.shade300; + indicatorChild = Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: Colors.grey.shade400, + shape: BoxShape.circle, + ), + ); + break; + } + + return DotIndicator( + size: 20, + color: indicatorColor, + child: indicatorChild, + ); + } + + Widget _buildTimelineContent(ActivityItem item) { + return Padding( + padding: const EdgeInsets.only(left: 16, bottom: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + item.icon, + size: 20, + color: item.status == ActivityStatus.completed + ? primaryColor + : item.status == ActivityStatus.inProgress + ? Colors.orange + : Colors.grey.shade400, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + item.title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: item.status == ActivityStatus.pending + ? Colors.grey.shade500 + : Colors.grey.shade800, + ), + ), + ), + ], + ), + const SizedBox(height: 6), + Padding( + padding: const EdgeInsets.only(left: 28), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.description, + style: TextStyle( + fontSize: 14, + color: item.status == ActivityStatus.pending + ? Colors.grey.shade400 + : Colors.grey.shade600, + ), + ), + if (item.time.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + '${item.time} • ${item.date}', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade500, + fontWeight: FontWeight.w500, + ), + ), + ], + ], + ), + ), + ], + ), + ); + } } + +// Model untuk item aktivitas +class ActivityItem { + final String title; + final String description; + final String time; + final String date; + final ActivityStatus status; + final IconData icon; + + ActivityItem({ + required this.title, + required this.description, + required this.time, + required this.date, + required this.status, + required this.icon, + }); +} + +// Model untuk pesanan selesai +class CompletedOrder { + final String orderId; + final String title; + final String description; + final String totalAmount; + final String completedDate; + final String completedTime; + final int rating; + final String customerNote; + + CompletedOrder({ + required this.orderId, + required this.title, + required this.description, + required this.totalAmount, + required this.completedDate, + required this.completedTime, + required this.rating, + required this.customerNote, + }); +} + +// Model untuk pesanan dibatalkan +class CancelledOrder { + final String orderId; + final String title; + final String description; + final String totalAmount; + final String cancelledDate; + final String cancelledTime; + final String cancelReason; + final String refundStatus; + + CancelledOrder({ + required this.orderId, + required this.title, + required this.description, + required this.totalAmount, + required this.cancelledDate, + required this.cancelledTime, + required this.cancelReason, + required this.refundStatus, + }); +} + +// Enum untuk status aktivitas +enum ActivityStatus { + completed, + inProgress, + pending, +} \ No newline at end of file diff --git a/lib/features/auth/presentation/screen/collector/clogin_screen.dart b/lib/features/auth/presentation/screen/collector/clogin_screen.dart index 763c3f9..f0c5c07 100644 --- a/lib/features/auth/presentation/screen/collector/clogin_screen.dart +++ b/lib/features/auth/presentation/screen/collector/clogin_screen.dart @@ -85,18 +85,22 @@ class CloginScreenState extends State { horizontal: double.infinity, vertical: 50, onTap: () { - // if (cPhoneController.text.isNotEmpty) { - // debugPrint("send otp dipencet"); - // await viewModel.loginOrRegister( - // cPhoneController.text, - // ); - // if (viewModel.loginResponse != null) { - // router.go( - // "/verif-otp", - // extra: cPhoneController.text, - // ); - // } - // } + if (cPhoneController.text.isNotEmpty) { + debugPrint("send otp dipencet"); + router.go( + "/cverif-otp", + extra: cPhoneController.text, + ); + // await viewModel.loginOrRegister( + // cPhoneController.text, + // ); + // if (viewModel.loginResponse != null) { + // router.go( + // "/verif-otp", + // extra: cPhoneController.text, + // ); + // } + } }, // loadingTrue: viewModel.isLoading, usingRow: false, diff --git a/lib/features/auth/presentation/screen/collector/controller/ktp_validator_controller.dart b/lib/features/auth/presentation/screen/collector/controller/ktp_validator_controller.dart new file mode 100644 index 0000000..e64c993 --- /dev/null +++ b/lib/features/auth/presentation/screen/collector/controller/ktp_validator_controller.dart @@ -0,0 +1,716 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart'; +import 'package:image/image.dart' as img; +import 'package:image_picker/image_picker.dart'; +import 'package:nik_validator/nik_validator.dart'; + +enum KtpValidationState { initial, imageSelected, processing, success, error } + +enum ProcessingStatus { idle, preprocessing, extracting, validating, completed } + +class ImageProcessingData { + final Uint8List imageBytes; + final String imagePath; + + ImageProcessingData(this.imageBytes, this.imagePath); +} + +class ProcessedImageResult { + final String processedPath; + final bool success; + final String? error; + + ProcessedImageResult({ + required this.processedPath, + required this.success, + this.error, + }); +} + +class KtpData { + String identificationNumber; + String fullName; + String bloodType; + String hamlet; + String village; + String neighbourhood; + String religion; + String maritalStatus; + String job; + String citizenship; + String validUntil; + String placeOfBirth; + String dateOfBirth; + String gender; + String province; + String district; + String subDistrict; + String postalCode; + + KtpData({ + this.identificationNumber = '', + this.fullName = '', + this.bloodType = '', + this.hamlet = '', + this.village = '', + this.neighbourhood = '', + this.religion = '', + this.maritalStatus = '', + this.job = '', + this.citizenship = '', + this.validUntil = '', + this.placeOfBirth = '', + this.dateOfBirth = '', + this.gender = '', + this.province = '', + this.district = '', + this.subDistrict = '', + this.postalCode = '', + }); + + void clear() { + identificationNumber = ''; + fullName = ''; + bloodType = ''; + hamlet = ''; + village = ''; + neighbourhood = ''; + religion = ''; + maritalStatus = ''; + job = ''; + citizenship = ''; + validUntil = ''; + placeOfBirth = ''; + dateOfBirth = ''; + gender = ''; + province = ''; + district = ''; + subDistrict = ''; + postalCode = ''; + } + + bool get hasData => identificationNumber.isNotEmpty; + + Map toJson() { + return { + 'identificationNumber': identificationNumber, + 'fullName': fullName, + 'bloodType': bloodType, + 'hamlet': hamlet, + 'village': village, + 'neighbourhood': neighbourhood, + 'religion': religion, + 'maritalStatus': maritalStatus, + 'job': job, + 'citizenship': citizenship, + 'validUntil': validUntil, + 'placeOfBirth': placeOfBirth, + 'dateOfBirth': dateOfBirth, + 'gender': gender, + 'province': province, + 'district': district, + 'subDistrict': subDistrict, + 'postalCode': postalCode, + }; + } +} + +class KtpValidatorController extends ChangeNotifier { + static const int _maxImageSize = 2048; + static const int _imageQuality = 85; + + final ImagePicker _picker = ImagePicker(); + final KtpData _ktpData = KtpData(); + final Map _controllers = {}; + + File? _selectedImage; + File? _processedImage; + KtpValidationState _state = KtpValidationState.initial; + ProcessingStatus _processingStatus = ProcessingStatus.idle; + String _errorMessage = ''; + String _successMessage = ''; + TextRecognizer? _textRecognizer; + + // Getters tetap sama + KtpValidationState get state => _state; + ProcessingStatus get processingStatus => _processingStatus; + String get errorMessage => _errorMessage; + String get successMessage => _successMessage; + File? get selectedImage => _selectedImage; + KtpData get ktpData => _ktpData; + Map get controllers => _controllers; + bool get isProcessing => _state == KtpValidationState.processing; + bool get hasData => _ktpData.hasData; + bool get canProcessImage => _selectedImage != null && !isProcessing; + + void initialize() { + _initializeControllers(); + _initializeTextRecognizer(); + } + + void _initializeControllers() { + final fields = [ + 'nik', + 'name', + 'placeOfBirth', + 'dateOfBirth', + 'gender', + 'bloodType', + 'hamlet', + 'village', + 'neighbourhood', + 'religion', + 'maritalStatus', + 'job', + 'citizenship', + 'validUntil', + 'province', + 'district', + 'subDistrict', + 'postalCode', + ]; + + for (String field in fields) { + _controllers[field] = TextEditingController(); + } + } + + void _initializeTextRecognizer() { + _textRecognizer = TextRecognizer(script: TextRecognitionScript.latin); + } + + Future pickImageFromCamera() async { + await _pickImage(ImageSource.camera); + } + + Future pickImageFromGallery() async { + await _pickImage(ImageSource.gallery); + } + + Future _pickImage(ImageSource source) async { + try { + final XFile? image = await _picker.pickImage( + source: source, + imageQuality: _imageQuality, + maxWidth: _maxImageSize.toDouble(), + maxHeight: _maxImageSize.toDouble(), + ); + + if (image != null) { + _cleanupTempFiles(); + _selectedImage = File(image.path); + _processedImage = null; + _ktpData.clear(); + _clearControllers(); + + _updateState(KtpValidationState.imageSelected); + _clearMessages(); + } + } catch (e) { + _handleError('Error saat memilih gambar: $e'); + } + } + + Future processImage() async { + if (_selectedImage == null || _textRecognizer == null) return; + + _updateState(KtpValidationState.processing); + _updateProcessingStatus(ProcessingStatus.preprocessing); + _ktpData.clear(); + _clearControllers(); + + try { + // Improved image processing + final ProcessedImageResult result = await _processImageOptimized( + _selectedImage!, + ); + + if (!result.success) { + throw Exception(result.error ?? 'Image processing failed'); + } + + _processedImage = File(result.processedPath); + _updateProcessingStatus(ProcessingStatus.extracting); + + // Enhanced OCR with multiple attempts + final String extractedNIK = await _performMultipleOCRAttempts( + _processedImage!, + ); + + if (extractedNIK.isEmpty) { + throw Exception( + 'NIK tidak ditemukan. Pastikan foto KTP jelas dan tidak buram.', + ); + } + + _updateProcessingStatus(ProcessingStatus.validating); + + await _parseNIK(extractedNIK); + + _updateProcessingStatus(ProcessingStatus.completed); + _updateControllers(); + _updateState(KtpValidationState.success); + _setSuccessMessage('NIK berhasil terdeteksi: $extractedNIK'); + } catch (e) { + _handleError('Error saat memproses gambar: $e'); + } + } + + Future _performMultipleOCRAttempts(File processedImage) async { + try { + // Attempt 1: Original processed image + String nik = await _performEnhancedOCR(processedImage); + if (nik.isNotEmpty) { + debugPrint('NIK found in attempt 1: $nik'); + return nik; + } + + // Attempt 2: Try with different image processing + final File alternativeProcessed = await _createAlternativeProcessedImage( + processedImage, + ); + nik = await _performEnhancedOCR(alternativeProcessed); + if (nik.isNotEmpty) { + debugPrint('NIK found in attempt 2: $nik'); + return nik; + } + + // Attempt 3: Try with original image (no processing) + nik = await _performEnhancedOCR(_selectedImage!); + if (nik.isNotEmpty) { + debugPrint('NIK found in attempt 3 (original): $nik'); + return nik; + } + + debugPrint('NIK not found in any attempt'); + return ''; + } catch (e) { + debugPrint('Multiple OCR attempts error: $e'); + return ''; + } + } + + Future _createAlternativeProcessedImage(File originalProcessed) async { + try { + final Uint8List imageBytes = await originalProcessed.readAsBytes(); + final img.Image? image = img.decodeImage(imageBytes); + + if (image == null) return originalProcessed; + + img.Image processed = image; + + // Different processing approach + processed = img.adjustColor(processed, contrast: 1.6, brightness: 1.2); + processed = img.gaussianBlur(processed, radius: 1); + processed = img.adjustColor(processed, contrast: 1.3); + + // Apply threshold for better text recognition + processed = _applyThreshold(processed, 128); + + final String altProcessedPath = originalProcessed.path.replaceAll( + '_processed.jpg', + '_alt_processed.jpg', + ); + final File altProcessedFile = File(altProcessedPath); + await altProcessedFile.writeAsBytes( + img.encodeJpg(processed, quality: 95), + ); + + return altProcessedFile; + } catch (e) { + debugPrint('Alternative processing error: $e'); + return originalProcessed; + } + } + + img.Image _applyThreshold(img.Image image, int threshold) { + for (int y = 0; y < image.height; y++) { + for (int x = 0; x < image.width; x++) { + final pixel = image.getPixel(x, y); + final gray = img.getLuminance(pixel); + final newPixel = + gray > threshold + ? img.ColorRgb8(255, 255, 255) + : img.ColorRgb8(0, 0, 0); + image.setPixel(x, y, newPixel); + } + } + return image; + } + + Future _processImageOptimized(File imageFile) async { + try { + final Uint8List imageBytes = await imageFile.readAsBytes(); + return await compute( + _processImageInIsolate, + ImageProcessingData(imageBytes, imageFile.path), + ); + } catch (e) { + return ProcessedImageResult( + processedPath: imageFile.path, + success: false, + error: e.toString(), + ); + } + } + + Future _performEnhancedOCR(File imageFile) async { + try { + final InputImage inputImage = InputImage.fromFile(imageFile); + final RecognizedText recognizedText = await _textRecognizer!.processImage( + inputImage, + ); + + // Debug: Print all detected text + debugPrint('OCR Raw Text: ${recognizedText.text}'); + + // Print each text block for debugging + for (TextBlock block in recognizedText.blocks) { + debugPrint('Text Block: ${block.text}'); + for (TextLine line in block.lines) { + debugPrint('Text Line: ${line.text}'); + } + } + + // Try multiple extraction methods + String nik = _extractNIKEnhanced(recognizedText.text); + if (nik.isNotEmpty) return nik; + + nik = _extractNIKFromBlocks(recognizedText.blocks); + if (nik.isNotEmpty) return nik; + + nik = _extractNIKFallback(recognizedText.text); + return nik; + } catch (e) { + debugPrint('OCR Error: $e'); + return ''; + } + } + + // IMPROVED: Enhanced NIK extraction with better patterns + String _extractNIKEnhanced(String ocrText) { + final List nikPatterns = [ + // Standard 16 digits + RegExp(r'\b(\d{16})\b'), + // With spaces or separators + RegExp(r'(\d{2}[\s\-_]?\d{2}[\s\-_]?\d{2}[\s\-_]?\d{6}[\s\-_]?\d{4})'), + RegExp(r'(\d{4}[\s\-_]?\d{4}[\s\-_]?\d{4}[\s\-_]?\d{4})'), + // More flexible patterns + RegExp(r'(\d{2}\s*\d{2}\s*\d{2}\s*\d{6}\s*\d{4})'), + RegExp(r'(\d{6}\s*\d{6}\s*\d{4})'), + // Looking for NIK label followed by numbers + RegExp(r'(?:NIK|No\.?\s*KTP)[\s:]*(\d{16})', caseSensitive: false), + RegExp( + r'(?:NIK|No\.?\s*KTP)[\s:]*(\d{2}[\s\-_]?\d{2}[\s\-_]?\d{2}[\s\-_]?\d{6}[\s\-_]?\d{4})', + caseSensitive: false, + ), + ]; + + // Clean text for better matching + String cleanText = + ocrText + .replaceAll(RegExp(r'[^\d\s\-_:]'), ' ') + .replaceAll(RegExp(r'\s+'), ' ') + .trim(); + + debugPrint('Clean text for NIK extraction: $cleanText'); + + for (RegExp pattern in nikPatterns) { + final Match? match = pattern.firstMatch(ocrText); + if (match != null) { + String candidate = match.group(1) ?? match.group(0)!; + candidate = candidate.replaceAll(RegExp(r'[\s\-_]+'), ''); + + debugPrint('Pattern matched candidate: $candidate'); + + if (candidate.length == 16 && _isValidNIKFormat(candidate)) { + debugPrint('Valid NIK found: $candidate'); + return candidate; + } + } + } + + return ''; + } + + String _extractNIKFromBlocks(List blocks) { + for (TextBlock block in blocks) { + for (TextLine line in block.lines) { + String lineText = line.text; + debugPrint('Checking line: $lineText'); + + // Look for lines that might contain NIK + if (lineText.toLowerCase().contains('nik') || + lineText.toLowerCase().contains('ktp') || + RegExp(r'\d{12,}').hasMatch(lineText)) { + String nik = _extractNIKEnhanced(lineText); + if (nik.isNotEmpty) { + debugPrint('NIK found in line: $lineText -> $nik'); + return nik; + } + } + } + } + return ''; + } + + String _extractNIKFallback(String ocrText) { + final RegExp numberPattern = RegExp(r'\d+'); + final Iterable matches = numberPattern.allMatches(ocrText); + + for (Match match in matches) { + String candidate = match.group(0)!; + + if (candidate.length == 16 && _isValidNIKFormat(candidate)) { + return candidate; + } + + if (candidate.length > 16) { + for (int i = 0; i <= candidate.length - 16; i++) { + String subCandidate = candidate.substring(i, i + 16); + if (_isValidNIKFormat(subCandidate)) { + return subCandidate; + } + } + } + } + + return ''; + } + + bool _isValidNIKFormat(String nik) { + if (nik.length != 16) return false; + + int provinceCode = int.tryParse(nik.substring(0, 2)) ?? 0; + if (provinceCode < 11 || provinceCode > 94) return false; + + int regencyCode = int.tryParse(nik.substring(2, 4)) ?? 0; + if (regencyCode < 1 || regencyCode > 99) return false; + + int districtCode = int.tryParse(nik.substring(4, 6)) ?? 0; + if (districtCode < 1 || districtCode > 99) return false; + + String birthDate = nik.substring(6, 12); + int day = int.tryParse(birthDate.substring(0, 2)) ?? 0; + int month = int.tryParse(birthDate.substring(2, 4)) ?? 0; + + if (day > 40) day -= 40; + + if (day < 1 || day > 31) return false; + if (month < 1 || month > 12) return false; + + return true; + } + + Future _parseNIK(String nik) async { + try { + final NIKModel result = await NIKValidator.instance.parse(nik: nik); + + if (result.valid == true) { + _ktpData.identificationNumber = result.nik ?? ''; + _ktpData.gender = result.gender ?? ''; + _ktpData.dateOfBirth = result.bornDate ?? ''; + _ktpData.province = result.province ?? ''; + _ktpData.district = result.city ?? ''; + _ktpData.subDistrict = result.subdistrict ?? ''; + _ktpData.postalCode = result.postalCode ?? ''; + } else { + throw Exception('NIK tidak valid: $nik'); + } + } catch (e) { + throw Exception('Error saat memvalidasi NIK: $e'); + } + } + + void updateControllerValue(String key, String value) { + if (_controllers.containsKey(key)) { + _controllers[key]?.text = value; + } + } + + void _updateControllers() { + _controllers['nik']?.text = _ktpData.identificationNumber; + _controllers['name']?.text = _ktpData.fullName; + _controllers['placeOfBirth']?.text = _ktpData.placeOfBirth; + _controllers['dateOfBirth']?.text = _ktpData.dateOfBirth; + _controllers['gender']?.text = _ktpData.gender; + _controllers['bloodType']?.text = _ktpData.bloodType; + _controllers['hamlet']?.text = _ktpData.hamlet; + _controllers['village']?.text = _ktpData.village; + _controllers['neighbourhood']?.text = _ktpData.neighbourhood; + _controllers['religion']?.text = _ktpData.religion; + _controllers['maritalStatus']?.text = _ktpData.maritalStatus; + _controllers['job']?.text = _ktpData.job; + _controllers['citizenship']?.text = _ktpData.citizenship; + _controllers['validUntil']?.text = _ktpData.validUntil; + _controllers['province']?.text = _ktpData.province; + _controllers['district']?.text = _ktpData.district; + _controllers['subDistrict']?.text = _ktpData.subDistrict; + _controllers['postalCode']?.text = _ktpData.postalCode; + } + + void _clearControllers() { + for (var controller in _controllers.values) { + controller.clear(); + } + } + + bool validateForm() { + final nikValue = _controllers['nik']?.text ?? ''; + final nameValue = _controllers['name']?.text ?? ''; + + if (nikValue.length != 16) { + _handleError('NIK harus 16 digit'); + return false; + } + + if (nameValue.isEmpty) { + _handleError('Nama lengkap harus diisi'); + return false; + } + + return true; + } + + Future saveData() async { + if (!validateForm()) return false; + + try { + _ktpData.identificationNumber = _controllers['nik']?.text ?? ''; + _ktpData.fullName = _controllers['name']?.text ?? ''; + _ktpData.placeOfBirth = _controllers['placeOfBirth']?.text ?? ''; + _ktpData.dateOfBirth = _controllers['dateOfBirth']?.text ?? ''; + _ktpData.gender = _controllers['gender']?.text ?? ''; + _ktpData.bloodType = _controllers['bloodType']?.text ?? ''; + _ktpData.neighbourhood = _controllers['neighbourhood']?.text ?? ''; + _ktpData.village = _controllers['village']?.text ?? ''; + _ktpData.religion = _controllers['religion']?.text ?? ''; + _ktpData.maritalStatus = _controllers['maritalStatus']?.text ?? ''; + _ktpData.job = _controllers['job']?.text ?? ''; + _ktpData.citizenship = _controllers['citizenship']?.text ?? ''; + _ktpData.validUntil = _controllers['validUntil']?.text ?? ''; + + await Future.delayed(const Duration(milliseconds: 500)); + + _setSuccessMessage('Data KTP berhasil disimpan!'); + debugPrint('KTP Data saved: ${_ktpData.toJson()}'); + + return true; + } catch (e) { + _handleError('Error saat menyimpan data: $e'); + return false; + } + } + + void _updateState(KtpValidationState newState) { + _state = newState; + notifyListeners(); + } + + void _updateProcessingStatus(ProcessingStatus status) { + _processingStatus = status; + notifyListeners(); + } + + void _handleError(String message) { + _errorMessage = message; + _successMessage = ''; + _updateState(KtpValidationState.error); + debugPrint('KTP Validation Error: $message'); + } + + void _setSuccessMessage(String message) { + _successMessage = message; + _errorMessage = ''; + } + + void _clearMessages() { + _errorMessage = ''; + _successMessage = ''; + } + + void clearError() { + _errorMessage = ''; + if (_state == KtpValidationState.error) { + _updateState( + _selectedImage != null + ? KtpValidationState.imageSelected + : KtpValidationState.initial, + ); + } + } + + void _cleanupTempFiles() { + try { + _processedImage?.deleteSync(); + } catch (e) { + debugPrint('Error cleaning temp files: $e'); + } + } + + @override + void dispose() { + _textRecognizer?.close(); + for (var controller in _controllers.values) { + controller.dispose(); + } + _cleanupTempFiles(); + super.dispose(); + } +} + +Future _processImageInIsolate( + ImageProcessingData data, +) async { + try { + img.Image? image = img.decodeImage(data.imageBytes); + if (image == null) { + return ProcessedImageResult( + processedPath: data.imagePath, + success: false, + error: 'Failed to decode image', + ); + } + + img.Image processed = image; + + processed = img.grayscale(processed); + + processed = img.adjustColor(processed, contrast: 1.4, brightness: 1.1); + + processed = img.convolution( + processed, + filter: [0, -1, 0, -1, 5, -1, 0, -1, 0], + ); + + if (processed.width < 1200) { + processed = img.copyResize(processed, width: 1200); + } else if (processed.width > 1600) { + processed = img.copyResize(processed, width: 1600); + } + + processed = img.gaussianBlur(processed, radius: 1); + + processed = img.adjustColor(processed, contrast: 1.2); + + processed = img.normalize(processed, min: 0, max: 255); + + final String processedPath = data.imagePath.replaceAll( + '.jpg', + '_processed.jpg', + ); + final File processedFile = File(processedPath); + await processedFile.writeAsBytes(img.encodeJpg(processed, quality: 95)); + + return ProcessedImageResult(processedPath: processedPath, success: true); + } catch (e) { + return ProcessedImageResult( + processedPath: data.imagePath, + success: false, + error: e.toString(), + ); + } +} diff --git a/lib/features/auth/presentation/screen/collector/identity_validation_screen.dart b/lib/features/auth/presentation/screen/collector/identity_validation_screen.dart index 6c0e9c2..bcbdc3f 100644 --- a/lib/features/auth/presentation/screen/collector/identity_validation_screen.dart +++ b/lib/features/auth/presentation/screen/collector/identity_validation_screen.dart @@ -1,178 +1,684 @@ import 'dart:io'; + import 'package:flutter/material.dart'; -import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:image/image.dart' as img; -import 'package:path_provider/path_provider.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:provider/provider.dart'; import 'package:rijig_mobile/core/router.dart'; import 'package:rijig_mobile/core/utils/guide.dart'; import 'package:rijig_mobile/widget/buttoncard.dart'; +import 'package:rijig_mobile/widget/formfiled.dart'; +import 'package:rijig_mobile/features/auth/presentation/screen/collector/controller/ktp_validator_controller.dart'; -class UploadKtpScreen extends StatefulWidget { - const UploadKtpScreen({super.key}); +class IdentityValidationScreen extends StatefulWidget { + const IdentityValidationScreen({super.key}); @override - State createState() => _UploadKtpScreenState(); + State createState() => + _IdentityValidationScreenState(); } -class _UploadKtpScreenState extends State { - File? _ktpImage; - bool isLoading = false; - - final Map controllers = { - "NIK": TextEditingController(), - "Nama": TextEditingController(), - "Tempat/Tgl Lahir": TextEditingController(), - "Alamat": TextEditingController(), - "RT/RW": TextEditingController(), - "Kel/Desa": TextEditingController(), - "Kecamatan": TextEditingController(), - "Agama": TextEditingController(), - "Status Perkawinan": TextEditingController(), - "Pekerjaan": TextEditingController(), - "Kewarganegaraan": TextEditingController(), - "Berlaku Hingga": TextEditingController(), - }; - - final Map> labelSynonyms = { - "NIK": ["nik"], - "Nama": ["nama", "nama lengkap"], - "Tempat/Tgl Lahir": ["tempat/tgl lahir", "tempat lahir", "tgl lahir"], - "Alamat": ["alamat"], - "RT/RW": ["rt/rw", "rtrw", "rt rw"], - "Kel/Desa": ["kelurahan", "desa", "kel/desa"], - "Kecamatan": ["kecamatan"], - "Agama": ["agama"], - "Status Perkawinan": ["status", "status perkawinan", "perkawinan"], - "Pekerjaan": ["pekerjaan"], - "Kewarganegaraan": ["kewarganegaraan", "warga negara"], - "Berlaku Hingga": ["berlaku", "berlaku hingga"], - }; - - Future _pickImage() async { - final pickedFile = await ImagePicker().pickImage( - source: ImageSource.camera, - ); - if (pickedFile != null) { - setState(() { - isLoading = true; - _ktpImage = File(pickedFile.path); - }); - await _processImageWithEnhancements(File(pickedFile.path)); - setState(() => isLoading = false); - } - } - - Future _processImageWithEnhancements(File imageFile) async { - final rawBytes = await imageFile.readAsBytes(); - final originalImage = img.decodeImage(rawBytes); - if (originalImage == null) return; - - // Enhance image: grayscale + auto rotate + thresholding - var processedImage = img.grayscale(originalImage); - if (processedImage.width > processedImage.height) { - processedImage = img.copyRotate(processedImage, angle: -90); - } - // processedImage = img.threshold(processedImage, threshold: 128); - - final tempDir = await getTemporaryDirectory(); - final enhancedPath = '${tempDir.path}/enhanced_ktp.jpg'; - final enhancedFile = File(enhancedPath) - ..writeAsBytesSync(img.encodeJpg(processedImage)); - - final inputImage = InputImage.fromFile(enhancedFile); - final textRecognizer = TextRecognizer(script: TextRecognitionScript.latin); - final RecognizedText recognizedText = await textRecognizer.processImage( - inputImage, - ); - await textRecognizer.close(); - - final lines = recognizedText.text.split('\n'); - debugPrint("[OCR Result]\n${recognizedText.text}"); - - setState(() { - for (var key in controllers.keys) { - final value = _extractMultilineValue(key, lines); - controllers[key]?.text = value; - } - }); - } - - String _extractMultilineValue(String key, List lines) { - final List keywords = labelSynonyms[key] ?? [key]; - for (int i = 0; i < lines.length; i++) { - final line = lines[i].toLowerCase(); - for (final kw in keywords) { - if (line.contains(kw.toLowerCase())) { - if (lines[i].contains(":")) { - return lines[i].split(":").last.trim(); - } else if (i + 1 < lines.length) { - return lines[i + 1].trim(); - } - } - } - } - return ''; - } +class _IdentityValidationScreenState extends State + with AutomaticKeepAliveClientMixin { + final GlobalKey _formKey = GlobalKey(); + late KtpValidatorController _controller; @override - void dispose() { - for (final controller in controllers.values) { - controller.dispose(); - } - super.dispose(); + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + _controller = context.read(); + _controller.initialize(); } @override Widget build(BuildContext context) { + super.build(context); + return Scaffold( backgroundColor: whiteColor, - appBar: AppBar( - title: const Text("Upload KTP"), - backgroundColor: primaryColor, - foregroundColor: whiteColor, + appBar: _buildAppBar(), + body: _buildBody(), + ); + } + + PreferredSizeWidget _buildAppBar() { + return AppBar( + title: Text( + 'Validasi Identitas KTP', + style: GoogleFonts.dmSans( + fontSize: 18.sp, + fontWeight: bold, + color: whiteColor, + ), ), - body: SafeArea( - child: SingleChildScrollView( - padding: const EdgeInsets.all(20), + backgroundColor: primaryColor, + elevation: 0, + centerTitle: true, + leading: IconButton( + icon: Icon(Icons.arrow_back_ios, color: whiteColor), + onPressed: () => router.pop(), + ), + ); + } + + Widget _buildBody() { + return Consumer( + builder: (context, controller, child) { + return SingleChildScrollView( + padding: EdgeInsets.all(20.w), child: Column( - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _ktpImage != null - ? Image.file(_ktpImage!, height: 200) - : Container( - height: 200, - color: Colors.grey.shade300, - child: const Center(child: Text("Belum ada gambar KTP")), - ), - const SizedBox(height: 20), - isLoading - ? const CircularProgressIndicator() - : CardButtonOne( - textButton: "Upload Foto KTP", - fontSized: 16, - colorText: whiteColor, - color: primaryColor, - borderRadius: 10, - horizontal: double.infinity, - vertical: 50, - onTap: _pickImage, - usingRow: false, - ), - const SizedBox(height: 30), - for (var key in controllers.keys) - Padding( - padding: const EdgeInsets.only(bottom: 12), - child: TextField( - decoration: InputDecoration(labelText: key), - controller: controllers[key], - ), - ), - TextButton(onPressed: ()=> router.go("/berandapengepul"), child: Text("ke home collector")) + _buildImageUploadSection(controller), + SizedBox(height: 24.h), + if (controller.hasData) _buildFormSection(controller), + _buildMessages(controller), ], ), + ); + }, + ); + } + + Widget _buildImageUploadSection(KtpValidatorController controller) { + return Container( + padding: EdgeInsets.all(20.w), + decoration: BoxDecoration( + color: whiteColor, + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + _buildUploadHeader(), + SizedBox(height: 20.h), + if (controller.selectedImage != null) + _buildImagePreview(controller.selectedImage!), + SizedBox(height: 20.h), + _buildImageSourceButtons(controller), + SizedBox(height: 20.h), + _buildProcessButton(controller), + if (controller.isProcessing) _buildProcessingIndicator(controller), + ], + ), + ); + } + + Widget _buildUploadHeader() { + return Row( + children: [ + Container( + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: primaryColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12.r), + ), + child: Icon(Icons.camera_alt, color: primaryColor, size: 24.w), + ), + SizedBox(width: 16.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Upload Foto KTP', + style: GoogleFonts.dmSans( + fontSize: 16.sp, + fontWeight: bold, + color: blackNavyColor, + ), + ), + SizedBox(height: 4.h), + Text( + 'Pastikan foto jelas dan tidak buram', + style: GoogleFonts.dmSans(fontSize: 12.sp, color: greyColor), + ), + ], + ), + ), + ], + ); + } + + Widget _buildImagePreview(File image) { + return Container( + height: 180.h, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.r), + border: Border.all(color: greyColor.withValues(alpha: 0.3)), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12.r), + child: Image.file(image, fit: BoxFit.cover), + ), + ); + } + + Widget _buildImageSourceButtons(KtpValidatorController controller) { + return Row( + children: [ + Expanded( + child: CardButtonOne( + textButton: 'Kamera', + fontSized: 14, + colorText: primaryColor, + borderRadius: 12, + horizontal: double.infinity, + vertical: 48.h, + color: whiteColor, + borderAll: Border.all(color: primaryColor, width: 1), + onTap: controller.pickImageFromCamera, + usingRow: true, + child: Icon(Icons.camera_alt, color: primaryColor, size: 18.w), + ), + ), + SizedBox(width: 16.w), + Expanded( + child: CardButtonOne( + textButton: 'Galeri', + fontSized: 14, + colorText: primaryColor, + borderRadius: 12, + horizontal: double.infinity, + vertical: 48.h, + color: whiteColor, + borderAll: Border.all(color: primaryColor, width: 1), + onTap: controller.pickImageFromGallery, + usingRow: true, + child: Icon(Icons.photo_library, color: primaryColor, size: 18.w), + ), + ), + ], + ); + } + + Widget _buildProcessButton(KtpValidatorController controller) { + return CardButtonOne( + textButton: controller.isProcessing ? 'Memproses...' : 'SCAN KTP', + fontSized: 16, + colorText: whiteColor, + borderRadius: 12, + horizontal: double.infinity, + vertical: 52.h, + color: controller.canProcessImage ? primaryColor : greyColor, + loadingTrue: controller.isProcessing, + onTap: controller.canProcessImage ? controller.processImage : () {}, + ); + } + + Widget _buildProcessingIndicator(KtpValidatorController controller) { + String statusText = ''; + double progress = 0.0; + + switch (controller.processingStatus) { + case ProcessingStatus.preprocessing: + statusText = 'Memproses gambar...'; + progress = 0.25; + break; + case ProcessingStatus.extracting: + statusText = 'Membaca teks...'; + progress = 0.5; + break; + case ProcessingStatus.validating: + statusText = 'Validasi NIK...'; + progress = 0.75; + break; + case ProcessingStatus.completed: + statusText = 'Selesai!'; + progress = 1.0; + break; + default: + statusText = 'Memulai...'; + progress = 0.1; + } + + return Container( + margin: EdgeInsets.only(top: 16.h), + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: primaryColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12.r), + ), + child: Column( + children: [ + Text( + statusText, + style: GoogleFonts.dmSans( + fontSize: 14.sp, + fontWeight: medium, + color: primaryColor, + ), + ), + SizedBox(height: 8.h), + LinearProgressIndicator( + value: progress, + backgroundColor: greyColor.withValues(alpha: 0.3), + valueColor: AlwaysStoppedAnimation(primaryColor), + ), + ], + ), + ); + } + + Widget _buildFormSection(KtpValidatorController controller) { + return Container( + padding: EdgeInsets.all(20.w), + decoration: BoxDecoration( + color: whiteColor, + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildFormHeader(), + SizedBox(height: 24.h), + ..._buildFormFields(controller), + SizedBox(height: 24.h), + _buildSaveButton(controller), + ], ), ), ); } + + Widget _buildFormHeader() { + return Row( + children: [ + Container( + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: Colors.green.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12.r), + ), + child: Icon(Icons.edit_document, color: Colors.green, size: 24.w), + ), + SizedBox(width: 16.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Data KTP Terdeteksi', + style: GoogleFonts.dmSans( + fontSize: 18.sp, + fontWeight: bold, + color: blackNavyColor, + ), + ), + SizedBox(height: 4.h), + Text( + 'Silakan periksa dan edit data jika diperlukan', + style: GoogleFonts.dmSans(fontSize: 12.sp, color: greyColor), + ), + ], + ), + ), + ], + ); + } + + List _buildFormFields(KtpValidatorController controller) { + return [ + _buildSectionTitle('Data Utama'), + SizedBox(height: 12.h), + + FormFieldOne( + hintText: 'NIK', + controllers: controller.controllers['nik'], + isRequired: true, + keyboardType: TextInputType.number, + maxLength: 16, + onTap: () {}, + validator: (value) { + if (value == null || value.length != 16) { + return 'NIK harus 16 digit'; + } + return null; + }, + ), + + FormFieldOne( + hintText: 'Nama Lengkap', + controllers: controller.controllers['name'], + isRequired: true, + onTap: () {}, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Nama lengkap harus diisi'; + } + return null; + }, + ), + + SizedBox(height: 20.h), + _buildSectionTitle('Informasi Personal'), + SizedBox(height: 12.h), + + Row( + children: [ + Expanded( + child: FormFieldOne( + hintText: 'Tempat Lahir', + controllers: controller.controllers['placeOfBirth'], + isRequired: false, + onTap: () {}, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: FormFieldOne( + hintText: 'Tanggal Lahir', + controllers: controller.controllers['dateOfBirth'], + isRequired: false, + onTap: () {}, + placeholder: 'DD-MM-YYYY', + ), + ), + ], + ), + + Row( + children: [ + Expanded( + child: FormFieldOne( + hintText: 'Jenis Kelamin', + controllers: controller.controllers['gender'], + isRequired: false, + onTap: () {}, + placeholder: 'LAKI-LAKI / PEREMPUAN', + ), + ), + SizedBox(width: 12.w), + Expanded( + child: FormFieldOne( + hintText: 'Golongan Darah', + controllers: controller.controllers['bloodType'], + isRequired: false, + onTap: () {}, + placeholder: 'A, B, AB, O', + ), + ), + ], + ), + + SizedBox(height: 20.h), + _buildSectionTitle('Alamat'), + SizedBox(height: 12.h), + + FormFieldOne( + hintText: 'RT/RW', + controllers: controller.controllers['neighbourhood'], + isRequired: false, + onTap: () {}, + placeholder: 'Contoh: 001/002', + ), + + Row( + children: [ + Expanded( + child: FormFieldOne( + hintText: 'Desa/Kelurahan', + controllers: controller.controllers['village'], + isRequired: false, + onTap: () {}, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: FormFieldOne( + hintText: 'Kecamatan', + controllers: controller.controllers['subDistrict'], + isRequired: false, + enabled: false, + inputColor: greyColor.withValues(alpha: 0.1), + onTap: () {}, + ), + ), + ], + ), + + SizedBox(height: 20.h), + _buildSectionTitle('Informasi Lainnya'), + SizedBox(height: 12.h), + + FormFieldOne( + hintText: 'Agama', + controllers: controller.controllers['religion'], + isRequired: false, + onTap: () {}, + ), + + FormFieldOne( + hintText: 'Status Perkawinan', + controllers: controller.controllers['maritalStatus'], + isRequired: false, + onTap: () {}, + placeholder: 'BELUM KAWIN / KAWIN / CERAI', + ), + + FormFieldOne( + hintText: 'Pekerjaan', + controllers: controller.controllers['job'], + isRequired: false, + onTap: () {}, + ), + + Row( + children: [ + Expanded( + child: FormFieldOne( + hintText: 'Kewarganegaraan', + controllers: controller.controllers['citizenship'], + isRequired: false, + onTap: () {}, + placeholder: 'WNI / WNA', + ), + ), + SizedBox(width: 12.w), + Expanded( + child: FormFieldOne( + hintText: 'Berlaku Hingga', + controllers: controller.controllers['validUntil'], + isRequired: false, + onTap: () {}, + placeholder: 'SEUMUR HIDUP', + ), + ), + ], + ), + + SizedBox(height: 20.h), + _buildSectionTitle('Data dari NIK Validator'), + SizedBox(height: 12.h), + + FormFieldOne( + hintText: 'Provinsi', + controllers: controller.controllers['province'], + isRequired: false, + enabled: false, + inputColor: greyColor.withValues(alpha: 0.1), + onTap: () {}, + ), + + Row( + children: [ + Expanded( + child: FormFieldOne( + hintText: 'Kabupaten/Kota', + controllers: controller.controllers['district'], + isRequired: false, + enabled: false, + inputColor: greyColor.withValues(alpha: 0.1), + onTap: () {}, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: FormFieldOne( + hintText: 'Kode Pos', + controllers: controller.controllers['postalCode'], + isRequired: false, + enabled: false, + inputColor: greyColor.withValues(alpha: 0.1), + onTap: () {}, + ), + ), + ], + ), + ]; + } + + Widget _buildSectionTitle(String title) { + return Text( + title, + style: GoogleFonts.dmSans( + fontSize: 16.sp, + fontWeight: bold, + color: blackNavyColor, + ), + ); + } + + Widget _buildSaveButton(KtpValidatorController controller) { + return CardButtonOne( + textButton: 'SIMPAN DATA KTP', + fontSized: 16, + colorText: whiteColor, + borderRadius: 12, + horizontal: double.infinity, + vertical: 52.h, + color: primaryColor, + onTap: () => _handleSaveData(controller), + ); + } + + Widget _buildMessages(KtpValidatorController controller) { + if (controller.errorMessage.isNotEmpty) { + return Container( + margin: EdgeInsets.only(top: 16.h), + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: redColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12.r), + border: Border.all(color: redColor.withValues(alpha: 0.3)), + ), + child: Row( + children: [ + Icon(Icons.error_outline, color: redColor, size: 20.w), + SizedBox(width: 12.w), + Expanded( + child: Text( + controller.errorMessage, + style: GoogleFonts.dmSans( + fontSize: 14.sp, + color: redColor, + fontWeight: medium, + ), + ), + ), + IconButton( + onPressed: controller.clearError, + icon: Icon(Icons.close, color: redColor, size: 20.w), + ), + ], + ), + ); + } + + if (controller.successMessage.isNotEmpty) { + return Container( + margin: EdgeInsets.only(top: 16.h), + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: Colors.green.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12.r), + border: Border.all(color: Colors.green.withValues(alpha: 0.3)), + ), + child: Row( + children: [ + Icon(Icons.check_circle_outline, color: Colors.green, size: 20.w), + SizedBox(width: 12.w), + Expanded( + child: Text( + controller.successMessage, + style: GoogleFonts.dmSans( + fontSize: 14.sp, + color: Colors.green, + fontWeight: medium, + ), + ), + ), + ], + ), + ); + } + + return const SizedBox.shrink(); + } + + Future _handleSaveData(KtpValidatorController controller) async { + if (_formKey.currentState!.validate()) { + final success = await controller.saveData(); + + if (success && mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + Icon(Icons.check_circle, color: whiteColor), + SizedBox(width: 12.w), + Expanded( + child: Text( + 'Data KTP berhasil disimpan!', + style: GoogleFonts.dmSans( + fontSize: 14.sp, + fontWeight: medium, + color: whiteColor, + ), + ), + ), + ], + ), + backgroundColor: Colors.green, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + duration: const Duration(seconds: 3), + ), + ); + } + } + } +} + +class UploadKtpScreen extends StatelessWidget { + const UploadKtpScreen({super.key}); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => KtpValidatorController(), + child: const IdentityValidationScreen(), + ); + } } diff --git a/lib/features/cart/presentation/screens/cart_test_screen.dart b/lib/features/cart/presentation/screens/cart_test_screen.dart index fb82d00..22d8a1d 100644 --- a/lib/features/cart/presentation/screens/cart_test_screen.dart +++ b/lib/features/cart/presentation/screens/cart_test_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:gap/gap.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:rijig_mobile/core/router.dart'; import 'package:rijig_mobile/features/cart/presentation/viewmodel/trashcart_vmod.dart'; import 'package:rijig_mobile/features/cart/model/trashcart_model.dart'; @@ -868,6 +869,7 @@ class _OrderSummaryScreenState extends State : hasItems ? () { _showSnackbar('Lanjut ke proses selanjutnya'); + router.push('/pickupmethod'); } : () => Navigator.of(context).pop(), style: ElevatedButton.styleFrom( diff --git a/lib/features/chat/presentation/model/chatlist_model.dart b/lib/features/chat/presentation/model/chatlist_model.dart new file mode 100644 index 0000000..b7d290c --- /dev/null +++ b/lib/features/chat/presentation/model/chatlist_model.dart @@ -0,0 +1,19 @@ +class ChatItem { + final String id; + final String name; + final String profileImage; + final String lastMessage; + final DateTime lastMessageTime; + final int unreadCount; + final bool isOnline; + + ChatItem({ + required this.id, + required this.name, + required this.profileImage, + required this.lastMessage, + required this.lastMessageTime, + this.unreadCount = 0, + this.isOnline = false, + }); +} \ No newline at end of file diff --git a/lib/features/chat/presentation/model/chatroom_model.dart b/lib/features/chat/presentation/model/chatroom_model.dart new file mode 100644 index 0000000..67c8cc3 --- /dev/null +++ b/lib/features/chat/presentation/model/chatroom_model.dart @@ -0,0 +1,21 @@ +import 'package:rijig_mobile/features/chat/presentation/screen/chatroom_screen.dart'; + +class Message { + final String id; + final String content; + final DateTime timestamp; + final bool isFromMe; + final MessageType type; + final MessageStatus status; + final String? mediaUrl; + + Message({ + required this.id, + required this.content, + required this.timestamp, + required this.isFromMe, + this.type = MessageType.text, + this.status = MessageStatus.sent, + this.mediaUrl, + }); +} diff --git a/lib/features/chat/presentation/screen/chatlist_screen.dart b/lib/features/chat/presentation/screen/chatlist_screen.dart new file mode 100644 index 0000000..43516dc --- /dev/null +++ b/lib/features/chat/presentation/screen/chatlist_screen.dart @@ -0,0 +1,805 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:rijig_mobile/core/router.dart'; +import 'package:rijig_mobile/core/utils/guide.dart'; +import 'package:rijig_mobile/features/chat/presentation/model/chatlist_model.dart'; + +class ChatListScreen extends StatefulWidget { + const ChatListScreen({super.key}); + + @override + State createState() => _ChatListScreenState(); +} + +class _ChatListScreenState extends State { + final TextEditingController _searchController = TextEditingController(); + List _allChats = []; + List _filteredChats = []; + bool _isSearching = false; + + // Selection mode states + bool _isSelectionMode = false; + Set _selectedChatIds = {}; + + @override + void initState() { + super.initState(); + _initializeDummyData(); + _filteredChats = _allChats; + _searchController.addListener(_onSearchChanged); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + void _initializeDummyData() { + final now = DateTime.now(); + + _allChats = [ + ChatItem( + id: '1', + name: 'Sarah Johnson', + profileImage: + 'https://images.unsplash.com/photo-1494790108755-2616b612b793?w=150', + lastMessage: 'Halo! Bagaimana dengan projek yang kemarin?', + lastMessageTime: now.subtract(const Duration(minutes: 5)), + unreadCount: 3, + isOnline: true, + ), + ChatItem( + id: '2', + name: 'Ahmad Pratama', + profileImage: + 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150', + lastMessage: 'Terima kasih untuk bantuannya!', + lastMessageTime: now.subtract(const Duration(minutes: 15)), + unreadCount: 1, + isOnline: true, + ), + ChatItem( + id: '3', + name: 'Maria Garcia', + profileImage: + 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150', + lastMessage: 'Sudah sampai rumah dengan selamat', + lastMessageTime: now.subtract(const Duration(hours: 1)), + unreadCount: 0, + isOnline: false, + ), + ChatItem( + id: '4', + name: 'David Chen', + profileImage: + 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150', + lastMessage: 'Baik, nanti saya kirim file nya', + lastMessageTime: now.subtract(const Duration(hours: 2)), + unreadCount: 2, + isOnline: true, + ), + ChatItem( + id: '5', + name: 'Jessica Lee', + profileImage: + 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=150', + lastMessage: 'Kapan kita bisa ketemu lagi?', + lastMessageTime: now.subtract(const Duration(hours: 4)), + unreadCount: 0, + isOnline: true, + ), + ChatItem( + id: '6', + name: 'Michael Rodriguez', + profileImage: + 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=150', + lastMessage: 'Jangan lupa meeting besok pagi', + lastMessageTime: now.subtract(const Duration(hours: 8)), + unreadCount: 4, + isOnline: false, + ), + ChatItem( + id: '7', + name: 'Lisa Wang', + profileImage: + 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=150', + lastMessage: 'Foto-fotonya bagus banget!', + lastMessageTime: now.subtract(const Duration(days: 1)), + unreadCount: 0, + isOnline: false, + ), + ChatItem( + id: '8', + name: 'Ryan Thompson', + profileImage: + 'https://images.unsplash.com/photo-1519244703995-f4e0f30006d5?w=150', + lastMessage: 'Oke siap, sampai jumpa!', + lastMessageTime: now.subtract(const Duration(days: 2)), + unreadCount: 1, + isOnline: false, + ), + ]; + } + + void _onSearchChanged() { + final query = _searchController.text.toLowerCase(); + setState(() { + _isSearching = query.isNotEmpty; + if (query.isEmpty) { + _filteredChats = _allChats; + } else { + _filteredChats = + _allChats.where((chat) { + return chat.name.toLowerCase().contains(query) || + chat.lastMessage.toLowerCase().contains(query); + }).toList(); + } + }); + } + + void _onChatTap(ChatItem chat) { + if (_isSelectionMode) { + // Toggle selection in selection mode + setState(() { + if (_selectedChatIds.contains(chat.id)) { + _selectedChatIds.remove(chat.id); + } else { + _selectedChatIds.add(chat.id); + } + + // Exit selection mode if no items selected + if (_selectedChatIds.isEmpty) { + _isSelectionMode = false; + } + }); + } else { + // Normal navigation + // Mark as read (remove unread count) + setState(() { + final index = _allChats.indexWhere((c) => c.id == chat.id); + if (index != -1) { + _allChats[index] = ChatItem( + id: chat.id, + name: chat.name, + profileImage: chat.profileImage, + lastMessage: chat.lastMessage, + lastMessageTime: chat.lastMessageTime, + unreadCount: 0, // Mark as read + isOnline: chat.isOnline, + ); + _filteredChats = _allChats; + } + }); + + // Navigate to chat room with query parameters + final encodedName = Uri.encodeComponent(chat.name); + final encodedImage = Uri.encodeComponent(chat.profileImage); + final onlineStatus = chat.isOnline.toString(); + + router.push( + '/chatroom/${chat.id}?name=$encodedName&image=$encodedImage&online=$onlineStatus', + ); + } + } + + void _onChatLongPress(ChatItem chat) { + if (!_isSelectionMode) { + setState(() { + _isSelectionMode = true; + _selectedChatIds.add(chat.id); + }); + } + } + + void _exitSelectionMode() { + setState(() { + _isSelectionMode = false; + _selectedChatIds.clear(); + }); + } + + void _selectAllChats() { + setState(() { + _selectedChatIds = _filteredChats.map((chat) => chat.id).toSet(); + }); + } + + void _markSelectedAsRead() { + final selectedCount = _selectedChatIds.length; + + setState(() { + for (int i = 0; i < _allChats.length; i++) { + if (_selectedChatIds.contains(_allChats[i].id)) { + _allChats[i] = ChatItem( + id: _allChats[i].id, + name: _allChats[i].name, + profileImage: _allChats[i].profileImage, + lastMessage: _allChats[i].lastMessage, + lastMessageTime: _allChats[i].lastMessageTime, + unreadCount: 0, // Mark as read + isOnline: _allChats[i].isOnline, + ); + } + } + _filteredChats = _allChats; + _exitSelectionMode(); + }); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('$selectedCount chat ditandai sudah dibaca'), + backgroundColor: Colors.green, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + ); + } + + void _deleteSelectedChats() { + final selectedCount = _selectedChatIds.length; + + showDialog( + context: context, + builder: + (context) => AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + title: Row( + children: const [ + Icon(Icons.warning, color: Colors.red, size: 24), + SizedBox(width: 8), + Text('Hapus Chat'), + ], + ), + content: Text( + 'Apakah Anda yakin ingin menghapus $selectedCount chat? Tindakan ini tidak dapat dibatalkan.', + ), + actions: [ + TextButton( + onPressed: () => context.pop(), + child: Text( + 'Batal', + style: TextStyle(color: Colors.grey.shade600), + ), + ), + TextButton( + onPressed: () { + setState(() { + _allChats.removeWhere( + (chat) => _selectedChatIds.contains(chat.id), + ); + _filteredChats = _allChats; + _exitSelectionMode(); + }); + + context.pop(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('$selectedCount chat berhasil dihapus'), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ); + }, + child: const Text('Hapus', style: TextStyle(color: Colors.red)), + ), + ], + ), + ); + } + + void _markAllAsRead() { + setState(() { + for (int i = 0; i < _allChats.length; i++) { + _allChats[i] = ChatItem( + id: _allChats[i].id, + name: _allChats[i].name, + profileImage: _allChats[i].profileImage, + lastMessage: _allChats[i].lastMessage, + lastMessageTime: _allChats[i].lastMessageTime, + unreadCount: 0, // Mark as read + isOnline: _allChats[i].isOnline, + ); + } + _filteredChats = _allChats; + }); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Semua chat ditandai sudah dibaca'), + backgroundColor: Colors.green, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + ); + } + + void _deleteAllChats() { + showDialog( + context: context, + builder: + (context) => AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + title: Row( + children: const [ + Icon(Icons.warning, color: Colors.red, size: 24), + SizedBox(width: 8), + Text('Hapus Semua Chat'), + ], + ), + content: const Text( + 'Apakah Anda yakin ingin menghapus semua chat? Tindakan ini tidak dapat dibatalkan.', + ), + actions: [ + TextButton( + onPressed: () => context.pop(), + child: Text( + 'Batal', + style: TextStyle(color: Colors.grey.shade600), + ), + ), + TextButton( + onPressed: () { + setState(() { + _allChats.clear(); + _filteredChats.clear(); + }); + + context.pop(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Semua chat berhasil dihapus'), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ); + }, + child: const Text( + 'Hapus Semua', + style: TextStyle(color: Colors.red), + ), + ), + ], + ), + ); + } + + String _formatTime(DateTime time) { + final now = DateTime.now(); + final difference = now.difference(time); + + if (difference.inMinutes < 1) { + return 'Baru saja'; + } else if (difference.inMinutes < 60) { + return '${difference.inMinutes}m'; + } else if (difference.inHours < 24) { + return '${difference.inHours}j'; + } else if (difference.inDays == 1) { + return 'Kemarin'; + } else if (difference.inDays < 7) { + return '${difference.inDays} hari'; + } else { + return '${time.day}/${time.month}/${time.year}'; + } + } + + @override + Widget build(BuildContext context) { + final totalUnread = _allChats.fold( + 0, + (sum, chat) => sum + chat.unreadCount, + ); + + return Scaffold( + backgroundColor: Colors.grey.shade50, + appBar: AppBar( + backgroundColor: whiteColor, + elevation: 0, + leading: + _isSelectionMode + ? IconButton( + icon: const Icon(Icons.close), + onPressed: _exitSelectionMode, + ) + : IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.pop(), + ), + title: + _isSelectionMode + ? Text( + '${_selectedChatIds.length} dipilih', + style: Tulisan.subheading(), + ) + : Column( + children: [ + Text('Chat', style: Tulisan.subheading()), + if (totalUnread > 0) + Text( + '$totalUnread pesan belum dibaca', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + fontWeight: FontWeight.normal, + ), + ), + ], + ), + centerTitle: true, + actions: + _isSelectionMode + ? [ + // Select All Button + IconButton( + icon: Icon( + _selectedChatIds.length == _filteredChats.length + ? Icons.deselect + : Icons.select_all, + ), + onPressed: () { + if (_selectedChatIds.length == _filteredChats.length) { + _exitSelectionMode(); + } else { + _selectAllChats(); + } + }, + tooltip: + _selectedChatIds.length == _filteredChats.length + ? 'Batal pilih semua' + : 'Pilih semua', + ), + // Mark as Read Button + IconButton( + icon: const Icon(Icons.mark_email_read), + onPressed: + _selectedChatIds.isNotEmpty + ? _markSelectedAsRead + : null, + tooltip: 'Tandai sudah dibaca', + ), + // Delete Button + IconButton( + icon: const Icon(Icons.delete, color: Colors.red), + onPressed: + _selectedChatIds.isNotEmpty + ? _deleteSelectedChats + : null, + tooltip: 'Hapus', + ), + ] + : [ + PopupMenuButton( + icon: const Icon(Icons.more_vert), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + onSelected: (value) { + switch (value) { + case 'mark_all_read': + _markAllAsRead(); + break; + case 'delete_all': + _deleteAllChats(); + break; + } + }, + itemBuilder: + (BuildContext context) => [ + PopupMenuItem( + value: 'mark_all_read', + child: Row( + children: const [ + Icon( + Icons.mark_email_read, + size: 18, + color: Colors.green, + ), + SizedBox(width: 8), + Text('Baca Semua'), + ], + ), + ), + PopupMenuItem( + value: 'delete_all', + child: Row( + children: const [ + Icon( + Icons.delete_sweep, + size: 18, + color: Colors.red, + ), + SizedBox(width: 8), + Text( + 'Hapus Semua Chat', + style: TextStyle(color: Colors.red), + ), + ], + ), + ), + ], + ), + ], + ), + body: Column( + children: [ + // Search Bar + Container( + padding: const EdgeInsets.all(16), + color: whiteColor, + child: TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: 'Cari chat atau pesan...', + prefixIcon: Icon(Icons.search, color: Colors.grey.shade500), + suffixIcon: + _isSearching + ? IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + _searchController.clear(); + }, + ) + : null, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(25), + borderSide: BorderSide.none, + ), + filled: true, + fillColor: Colors.grey.shade100, + contentPadding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 12, + ), + ), + ), + ), + + // Chat List + Expanded( + child: + _filteredChats.isEmpty + ? _buildEmptyState() + : ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: _filteredChats.length, + itemBuilder: (context, index) { + return _buildChatItem(_filteredChats[index]); + }, + ), + ), + ], + ), + ); + } + + Widget _buildEmptyState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.chat_bubble_outline, + size: 80, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + Text( + _isSearching ? 'Tidak ada chat ditemukan' : 'Belum ada chat', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 8), + Text( + _isSearching ? 'Coba kata kunci lain' : 'Belum ada percakapan', + style: TextStyle(fontSize: 14, color: Colors.grey.shade500), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + Widget _buildChatItem(ChatItem chat) { + final isSelected = _selectedChatIds.contains(chat.id); + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 2), + decoration: BoxDecoration( + color: isSelected ? primaryColor.withValues(alpha: 0.1) : whiteColor, + borderRadius: BorderRadius.circular(12), + border: isSelected ? Border.all(color: primaryColor, width: 2) : null, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.04), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _onChatTap(chat), + onLongPress: () => _onChatLongPress(chat), + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + // Selection Checkbox (only show in selection mode) + if (_isSelectionMode) ...[ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: isSelected ? primaryColor : Colors.grey.shade400, + width: 2, + ), + color: isSelected ? primaryColor : Colors.transparent, + ), + child: + isSelected + ? const Icon( + Icons.check, + size: 16, + color: Colors.white, + ) + : null, + ), + const SizedBox(width: 16), + ], + + // Profile Picture with Online Status + Stack( + children: [ + CircleAvatar( + radius: 28, + backgroundColor: Colors.grey.shade200, + backgroundImage: NetworkImage(chat.profileImage), + onBackgroundImageError: (_, __) {}, + child: Text( + chat.name.substring(0, 1).toUpperCase(), + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.grey.shade600, + ), + ), + ), + if (chat.isOnline) + Positioned( + bottom: 2, + right: 2, + child: Container( + width: 14, + height: 14, + decoration: BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + border: Border.all(color: whiteColor, width: 2), + ), + ), + ), + ], + ), + + const SizedBox(width: 16), + + // Chat Content + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Name and Time + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + chat.name, + style: TextStyle( + fontSize: 16, + fontWeight: + chat.unreadCount > 0 + ? FontWeight.bold + : FontWeight.w600, + color: Colors.black87, + ), + overflow: TextOverflow.ellipsis, + ), + ), + Text( + _formatTime(chat.lastMessageTime), + style: TextStyle( + fontSize: 12, + fontWeight: + chat.unreadCount > 0 + ? FontWeight.w600 + : FontWeight.normal, + color: + chat.unreadCount > 0 + ? primaryColor + : Colors.grey.shade500, + ), + ), + ], + ), + + const SizedBox(height: 4), + + // Last Message and Unread Count + Row( + children: [ + Expanded( + child: Text( + chat.lastMessage, + style: TextStyle( + fontSize: 14, + fontWeight: + chat.unreadCount > 0 + ? FontWeight.w500 + : FontWeight.normal, + color: + chat.unreadCount > 0 + ? Colors.black87 + : Colors.grey.shade600, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (chat.unreadCount > 0) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: primaryColor, + borderRadius: BorderRadius.circular(12), + ), + constraints: const BoxConstraints(minWidth: 20), + child: Text( + chat.unreadCount > 99 + ? '99+' + : '${chat.unreadCount}', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + ], + ], + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/chat/presentation/screen/chatroom_screen.dart b/lib/features/chat/presentation/screen/chatroom_screen.dart new file mode 100644 index 0000000..0802003 --- /dev/null +++ b/lib/features/chat/presentation/screen/chatroom_screen.dart @@ -0,0 +1,692 @@ +import 'package:flutter/material.dart'; +import 'package:rijig_mobile/core/utils/guide.dart'; +import 'package:rijig_mobile/features/chat/presentation/model/chatroom_model.dart'; +import 'package:rijig_mobile/widget/custom_bottom_sheet.dart'; + +// Model untuk Message +/* class Message { + final String id; + final String content; + final DateTime timestamp; + final bool isFromMe; + final MessageType type; + final MessageStatus status; + final String? mediaUrl; + + Message({ + required this.id, + required this.content, + required this.timestamp, + required this.isFromMe, + this.type = MessageType.text, + this.status = MessageStatus.sent, + this.mediaUrl, + }); +} */ + +enum MessageType { text, image, video } + +enum MessageStatus { sending, sent, delivered, read } + +class ChatRoomScreen extends StatefulWidget { + final String contactName; + final String contactImage; + final bool isOnline; + + const ChatRoomScreen({ + super.key, + required this.contactName, + required this.contactImage, + this.isOnline = false, + }); + + @override + State createState() => _ChatRoomScreenState(); +} + +class _ChatRoomScreenState extends State { + final TextEditingController _messageController = TextEditingController(); + final ScrollController _scrollController = ScrollController(); + final FocusNode _messageFocus = FocusNode(); + + List _messages = []; + bool _isTyping = false; + bool _isSending = false; + + @override + void initState() { + super.initState(); + _initializeDummyMessages(); + _messageController.addListener(_onTypingChanged); + } + + @override + void dispose() { + _messageController.dispose(); + _scrollController.dispose(); + _messageFocus.dispose(); + super.dispose(); + } + + void _initializeDummyMessages() { + final now = DateTime.now(); + + _messages = [ + Message( + id: '1', + content: 'Halo! Bagaimana kabarnya?', + timestamp: now.subtract(const Duration(hours: 2)), + isFromMe: false, + status: MessageStatus.read, + ), + Message( + id: '2', + content: 'Halo juga! Baik kok, sedang sibuk proyekan', + timestamp: now.subtract(const Duration(hours: 2, minutes: -5)), + isFromMe: true, + status: MessageStatus.read, + ), + Message( + id: '3', + content: 'Wah asyik, projek apa emangnya?', + timestamp: now.subtract(const Duration(hours: 1, minutes: 50)), + isFromMe: false, + status: MessageStatus.read, + ), + Message( + id: '4', + content: 'Projek mobile app untuk client, lumayan challenging sih', + timestamp: now.subtract(const Duration(hours: 1, minutes: 45)), + isFromMe: true, + status: MessageStatus.read, + ), + Message( + id: '5', + content: + 'https://images.unsplash.com/photo-1551650975-87deedd944c3?w=400', + timestamp: now.subtract(const Duration(hours: 1, minutes: 30)), + isFromMe: false, + type: MessageType.image, + status: MessageStatus.read, + mediaUrl: + 'https://images.unsplash.com/photo-1551650975-87deedd944c3?w=400', + ), + Message( + id: '6', + content: 'Keren banget! UI nya bagus', + timestamp: now.subtract(const Duration(hours: 1, minutes: 25)), + isFromMe: true, + status: MessageStatus.read, + ), + Message( + id: '7', + content: 'Terima kasih! Btw kapan kita bisa ketemu lagi?', + timestamp: now.subtract(const Duration(minutes: 30)), + isFromMe: false, + status: MessageStatus.delivered, + ), + Message( + id: '8', + content: 'Gimana weekend ini? Kita bisa lunch bareng', + timestamp: now.subtract(const Duration(minutes: 25)), + isFromMe: true, + status: MessageStatus.delivered, + ), + Message( + id: '9', + content: 'Boleh banget! Jam berapa dan dimana?', + timestamp: now.subtract(const Duration(minutes: 5)), + isFromMe: false, + status: MessageStatus.sent, + ), + ]; + } + + void _onTypingChanged() { + final isCurrentlyTyping = _messageController.text.isNotEmpty; + if (isCurrentlyTyping != _isTyping) { + setState(() { + _isTyping = isCurrentlyTyping; + }); + } + } + + void _sendMessage() { + final content = _messageController.text.trim(); + if (content.isEmpty || _isSending) return; + + setState(() { + _isSending = true; + }); + + final newMessage = Message( + id: DateTime.now().millisecondsSinceEpoch.toString(), + content: content, + timestamp: DateTime.now(), + isFromMe: true, + status: MessageStatus.sending, + ); + + setState(() { + _messages.add(newMessage); + _messageController.clear(); + }); + + // Scroll to bottom + _scrollToBottom(); + + // Simulate sending + Future.delayed(const Duration(seconds: 1), () { + setState(() { + _isSending = false; + // Update message status to sent + final index = _messages.indexWhere((m) => m.id == newMessage.id); + if (index != -1) { + _messages[index] = Message( + id: newMessage.id, + content: newMessage.content, + timestamp: newMessage.timestamp, + isFromMe: newMessage.isFromMe, + status: MessageStatus.sent, + ); + } + }); + + // Simulate delivery after 2 seconds + Future.delayed(const Duration(seconds: 2), () { + setState(() { + final index = _messages.indexWhere((m) => m.id == newMessage.id); + if (index != -1) { + _messages[index] = Message( + id: newMessage.id, + content: newMessage.content, + timestamp: newMessage.timestamp, + isFromMe: newMessage.isFromMe, + status: MessageStatus.delivered, + ); + } + }); + }); + }); + } + + void _scrollToBottom() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + }); + } + + void _showAttachmentOptions() { + CustomBottomSheet.show( + context: context, + title: 'Kirim Media', + content: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildAttachmentOption( + icon: Icons.camera_alt, + label: 'Kamera', + color: Colors.blue, + onTap: () { + Navigator.pop(context); + _takePhoto(); + }, + ), + _buildAttachmentOption( + icon: Icons.photo_library, + label: 'Galeri', + color: Colors.green, + onTap: () { + Navigator.pop(context); + _pickFromGallery(); + }, + ), + _buildAttachmentOption( + icon: Icons.videocam, + label: 'Video', + color: Colors.red, + onTap: () { + Navigator.pop(context); + _pickVideo(); + }, + ), + ], + ), + button1: Container(), // Empty since we have custom buttons + ); + } + + Widget _buildAttachmentOption({ + required IconData icon, + required String label, + required Color color, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 60, + height: 60, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(30), + border: Border.all(color: color.withValues(alpha: 0.3)), + ), + child: Icon(icon, color: color, size: 30), + ), + const SizedBox(height: 8), + Text( + label, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.grey.shade700, + ), + ), + ], + ), + ); + } + + void _takePhoto() { + debugPrint('Take photo from camera'); + // TODO: Implement camera functionality + // Example: ImagePicker.pickImage(source: ImageSource.camera) + } + + void _pickFromGallery() { + debugPrint('Pick image from gallery'); + // TODO: Implement gallery picker + // Example: ImagePicker.pickImage(source: ImageSource.gallery) + } + + void _pickVideo() { + debugPrint('Pick video from gallery'); + // TODO: Implement video picker + // Example: ImagePicker.pickVideo(source: ImageSource.gallery) + } + + String _formatMessageTime(DateTime time) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final messageDate = DateTime(time.year, time.month, time.day); + + if (messageDate == today) { + return '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; + } else if (messageDate == today.subtract(const Duration(days: 1))) { + return 'Kemarin ${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; + } else { + return '${time.day}/${time.month} ${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; + } + } + + Widget _buildMessageStatusIcon(MessageStatus status) { + switch (status) { + case MessageStatus.sending: + return const SizedBox( + width: 12, + height: 12, + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.grey), + ); + case MessageStatus.sent: + return Icon(Icons.check, size: 14, color: Colors.grey.shade500); + case MessageStatus.delivered: + return Icon(Icons.done_all, size: 14, color: Colors.grey.shade500); + case MessageStatus.read: + return Icon(Icons.done_all, size: 14, color: primaryColor); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey.shade50, + appBar: AppBar( + backgroundColor: whiteColor, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.of(context).pop(), + ), + title: Row( + children: [ + Stack( + children: [ + CircleAvatar( + radius: 20, + backgroundColor: Colors.grey.shade200, + backgroundImage: NetworkImage(widget.contactImage), + onBackgroundImageError: (_, __) {}, + child: Text( + widget.contactName.substring(0, 1).toUpperCase(), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.grey, + ), + ), + ), + if (widget.isOnline) + Positioned( + bottom: 0, + right: 0, + child: Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + border: Border.all(color: whiteColor, width: 2), + ), + ), + ), + ], + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.contactName, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + Text( + widget.isOnline ? 'Online' : 'Terakhir dilihat baru saja', + style: TextStyle(fontSize: 12, color: Colors.grey.shade600), + ), + ], + ), + ), + ], + ), + actions: [ + IconButton( + icon: const Icon(Icons.more_vert), + onPressed: () { + debugPrint('Show chat options'); + }, + ), + ], + ), + body: Column( + children: [ + // Messages List + Expanded( + child: ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.all(16), + itemCount: _messages.length, + itemBuilder: (context, index) { + final message = _messages[index]; + final showTimestamp = + index == 0 || + _messages[index - 1].timestamp + .difference(message.timestamp) + .inMinutes + .abs() > + 5; + + return Column( + children: [ + if (showTimestamp) + Container( + margin: const EdgeInsets.symmetric(vertical: 16), + child: Text( + _formatMessageTime(message.timestamp), + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade500, + fontWeight: FontWeight.w500, + ), + ), + ), + _buildMessageBubble(message), + ], + ); + }, + ), + ), + + // Message Input Area + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: whiteColor, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, -2), + ), + ], + ), + child: SafeArea( + child: Row( + children: [ + // Attachment Button + GestureDetector( + onTap: _showAttachmentOptions, + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(20), + ), + child: Icon( + Icons.attach_file, + color: Colors.grey.shade600, + size: 20, + ), + ), + ), + + const SizedBox(width: 12), + + // Text Input + Expanded( + child: Container( + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(25), + ), + child: TextField( + controller: _messageController, + focusNode: _messageFocus, + maxLines: null, + textCapitalization: TextCapitalization.sentences, + decoration: InputDecoration( + hintText: 'Ketik pesan...', + hintStyle: TextStyle( + color: Colors.grey.shade500, + fontSize: 14, + ), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 12, + ), + ), + onSubmitted: (_) => _sendMessage(), + ), + ), + ), + + const SizedBox(width: 12), + + // Send Button + GestureDetector( + onTap: _isTyping && !_isSending ? _sendMessage : null, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 40, + height: 40, + decoration: BoxDecoration( + color: _isTyping ? primaryColor : Colors.grey.shade300, + borderRadius: BorderRadius.circular(20), + ), + child: + _isSending + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : Icon( + Icons.send, + color: + _isTyping + ? Colors.white + : Colors.grey.shade500, + size: 18, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildMessageBubble(Message message) { + final isMe = message.isFromMe; + + return Container( + margin: const EdgeInsets.symmetric(vertical: 2), + child: Row( + mainAxisAlignment: + isMe ? MainAxisAlignment.end : MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (!isMe) ...[ + CircleAvatar( + radius: 12, + backgroundColor: Colors.grey.shade200, + backgroundImage: NetworkImage(widget.contactImage), + onBackgroundImageError: (_, __) {}, + child: Text( + widget.contactName.substring(0, 1).toUpperCase(), + style: const TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: Colors.grey, + ), + ), + ), + const SizedBox(width: 8), + ], + + Flexible( + child: Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.7, + ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: isMe ? primaryColor : whiteColor, + borderRadius: BorderRadius.only( + topLeft: const Radius.circular(18), + topRight: const Radius.circular(18), + bottomLeft: Radius.circular(isMe ? 18 : 4), + bottomRight: Radius.circular(isMe ? 4 : 18), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (message.type == MessageType.image && + message.mediaUrl != null) + Container( + margin: const EdgeInsets.only(bottom: 8), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + message.mediaUrl!, + width: double.infinity, + height: 200, + fit: BoxFit.cover, + errorBuilder: + (_, __, ___) => Container( + width: double.infinity, + height: 200, + color: Colors.grey.shade200, + child: const Icon(Icons.broken_image), + ), + ), + ), + ), + + if (message.content.isNotEmpty) + Text( + message.content, + style: TextStyle( + fontSize: 14, + color: isMe ? Colors.white : Colors.black87, + height: 1.3, + ), + ), + + if (isMe) ...[ + const SizedBox(height: 4), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _formatMessageTime(message.timestamp), + style: TextStyle( + fontSize: 10, + color: Colors.white.withValues(alpha: 0.7), + ), + ), + const SizedBox(width: 4), + _buildMessageStatusIcon(message.status), + ], + ), + ] else ...[ + const SizedBox(height: 4), + Text( + _formatMessageTime(message.timestamp), + style: TextStyle( + fontSize: 10, + color: Colors.grey.shade500, + ), + ), + ], + ], + ), + ), + ), + + if (isMe) const SizedBox(width: 8), + ], + ), + ); + } +} diff --git a/lib/features/home/notification/presentation/screen/notification_screen.dart b/lib/features/home/notification/presentation/screen/notification_screen.dart new file mode 100644 index 0000000..fc5e6d0 --- /dev/null +++ b/lib/features/home/notification/presentation/screen/notification_screen.dart @@ -0,0 +1,592 @@ +import 'package:flutter/material.dart'; +import 'package:rijig_mobile/core/utils/guide.dart'; + +class NotificationScreen extends StatefulWidget { + const NotificationScreen({super.key}); + + @override + State createState() => _NotificationScreenState(); +} + +class _NotificationScreenState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + + List unreadNotifications = [ + NotificationItem( + id: '1', + title: 'Pesanan Dikonfirmasi', + message: 'Pesanan #12345 telah dikonfirmasi dan sedang diproses', + time: DateTime.now().subtract(const Duration(minutes: 15)), + type: NotificationType.order, + isRead: false, + ), + NotificationItem( + id: '2', + title: 'Promo Spesial!', + message: 'Dapatkan diskon 50% untuk pembelian minimal Rp 100.000', + time: DateTime.now().subtract(const Duration(hours: 2)), + type: NotificationType.promotion, + isRead: false, + ), + NotificationItem( + id: '3', + title: 'Pengiriman Dalam Perjalanan', + message: 'Pesanan #12344 sedang dalam perjalanan menuju alamat Anda', + time: DateTime.now().subtract(const Duration(hours: 4)), + type: NotificationType.delivery, + isRead: false, + ), + NotificationItem( + id: '4', + title: 'Update Sistem', + message: 'Aplikasi telah diperbarui dengan fitur-fitur terbaru', + time: DateTime.now().subtract(const Duration(days: 1)), + type: NotificationType.system, + isRead: false, + ), + NotificationItem( + id: '5', + title: 'Pembayaran Berhasil', + message: 'Pembayaran untuk pesanan #12343 telah berhasil diproses', + time: DateTime.now().subtract(const Duration(days: 1, hours: 3)), + type: NotificationType.payment, + isRead: false, + ), + ]; + + List readNotifications = [ + NotificationItem( + id: '6', + title: 'Pesanan Selesai', + message: + 'Pesanan #12342 telah selesai. Berikan rating untuk pelayanan kami!', + time: DateTime.now().subtract(const Duration(days: 2)), + type: NotificationType.order, + isRead: true, + ), + NotificationItem( + id: '7', + title: 'Cashback Berhasil', + message: 'Cashback sebesar Rp 10.000 telah masuk ke saldo Anda', + time: DateTime.now().subtract(const Duration(days: 3)), + type: NotificationType.payment, + isRead: true, + ), + NotificationItem( + id: '8', + title: 'Selamat Datang!', + message: 'Terima kasih telah bergabung dengan aplikasi kami', + time: DateTime.now().subtract(const Duration(days: 7)), + type: NotificationType.system, + isRead: true, + ), + ]; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + void _markAllAsRead() { + setState(() { + for (var notification in unreadNotifications) { + notification.isRead = true; + readNotifications.insert(0, notification); + } + unreadNotifications.clear(); + }); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Semua notifikasi telah ditandai sebagai dibaca'), + backgroundColor: Colors.green, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + ); + } + + void _markAsRead(NotificationItem notification) { + setState(() { + notification.isRead = true; + unreadNotifications.remove(notification); + readNotifications.insert(0, notification); + }); + } + + void _deleteNotification(NotificationItem notification, bool isFromUnread) { + setState(() { + if (isFromUnread) { + unreadNotifications.remove(notification); + } else { + readNotifications.remove(notification); + } + }); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Notifikasi telah dihapus'), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: whiteColor, + appBar: AppBar( + title: Text('Notifikasi', style: Tulisan.subheading()), + centerTitle: true, + actions: [ + if (unreadNotifications.isNotEmpty) + TextButton( + onPressed: _markAllAsRead, + child: Text( + 'Baca Semua', + style: TextStyle( + color: primaryColor, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + bottom: PreferredSize( + preferredSize: const Size.fromHeight(40), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(10)), + child: Container( + height: 40, + margin: const EdgeInsets.symmetric(horizontal: 20), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(10)), + color: Colors.grey.shade100, + ), + child: TabBar( + controller: _tabController, + indicatorSize: TabBarIndicatorSize.tab, + dividerColor: Colors.transparent, + indicator: BoxDecoration( + color: primaryColor, + borderRadius: const BorderRadius.all(Radius.circular(10)), + ), + labelColor: Colors.white, + unselectedLabelColor: Colors.black54, + tabs: [ + Tab( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Belum Dibaca'), + if (unreadNotifications.isNotEmpty) ...[ + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(10), + ), + child: Text( + '${unreadNotifications.length}', + style: const TextStyle( + fontSize: 12, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ], + ), + ), + Tab( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Dibaca'), + if (readNotifications.isNotEmpty) ...[ + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.grey.shade400, + borderRadius: BorderRadius.circular(10), + ), + child: Text( + '${readNotifications.length}', + style: const TextStyle( + fontSize: 12, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ], + ), + ), + ], + ), + ), + ), + ), + ), + body: TabBarView( + controller: _tabController, + children: [_buildUnreadTab(), _buildReadTab()], + ), + ); + } + + Widget _buildUnreadTab() { + if (unreadNotifications.isEmpty) { + return _buildEmptyState( + icon: Icons.notifications_off, + title: 'Tidak Ada Notifikasi Baru', + subtitle: 'Semua notifikasi sudah dibaca', + ); + } + + return Container( + color: Colors.grey.shade50, + child: ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: unreadNotifications.length, + itemBuilder: (context, index) { + final notification = unreadNotifications[index]; + return _buildNotificationCard(notification, true); + }, + ), + ); + } + + Widget _buildReadTab() { + if (readNotifications.isEmpty) { + return _buildEmptyState( + icon: Icons.mark_email_read, + title: 'Belum Ada Notifikasi Dibaca', + subtitle: 'Notifikasi yang sudah dibaca akan muncul di sini', + ); + } + + return Container( + color: Colors.grey.shade50, + child: ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: readNotifications.length, + itemBuilder: (context, index) { + final notification = readNotifications[index]; + return _buildNotificationCard(notification, false); + }, + ), + ); + } + + Widget _buildNotificationCard(NotificationItem notification, bool isUnread) { + final notificationIcon = _getNotificationIcon(notification.type); + final notificationColor = _getNotificationColor(notification.type); + + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: isUnread ? Colors.white : Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + border: + isUnread + ? Border.all( + color: primaryColor.withValues(alpha: 0.2), + width: 1, + ) + : Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Dismissible( + key: Key(notification.id), + direction: DismissDirection.endToStart, + background: Container( + alignment: Alignment.centerRight, + padding: const EdgeInsets.symmetric(horizontal: 20), + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(12), + ), + child: const Icon(Icons.delete, color: Colors.white, size: 24), + ), + onDismissed: (direction) { + _deleteNotification(notification, isUnread); + }, + child: InkWell( + onTap: isUnread ? () => _markAsRead(notification) : null, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: notificationColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + notificationIcon, + color: notificationColor, + size: 24, + ), + ), + + const SizedBox(width: 12), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + notification.title, + style: TextStyle( + fontSize: 16, + fontWeight: + isUnread + ? FontWeight.bold + : FontWeight.w600, + color: + isUnread + ? Colors.black + : Colors.grey.shade700, + ), + ), + ), + if (isUnread) + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: primaryColor, + shape: BoxShape.circle, + ), + ), + ], + ), + + const SizedBox(height: 6), + + Text( + notification.message, + style: TextStyle( + fontSize: 14, + color: + isUnread + ? Colors.grey.shade700 + : Colors.grey.shade600, + height: 1.3, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + + const SizedBox(height: 8), + + Row( + children: [ + Icon( + Icons.access_time, + size: 14, + color: Colors.grey.shade500, + ), + const SizedBox(width: 4), + Text( + _formatTime(notification.time), + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade500, + ), + ), + const Spacer(), + _buildNotificationTypeChip(notification.type), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildNotificationTypeChip(NotificationType type) { + String label; + Color color; + + switch (type) { + case NotificationType.order: + label = 'Pesanan'; + color = Colors.blue; + break; + case NotificationType.delivery: + label = 'Pengiriman'; + color = Colors.green; + break; + case NotificationType.payment: + label = 'Pembayaran'; + color = Colors.orange; + break; + case NotificationType.promotion: + label = 'Promo'; + color = Colors.purple; + break; + case NotificationType.system: + label = 'Sistem'; + color = Colors.grey; + break; + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + label, + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w600, + color: color, + ), + ), + ); + } + + Widget _buildEmptyState({ + required IconData icon, + required String title, + required String subtitle, + }) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: Colors.grey.shade100, + shape: BoxShape.circle, + ), + child: Icon(icon, size: 40, color: Colors.grey.shade400), + ), + const SizedBox(height: 20), + Text( + title, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, + ), + ), + const SizedBox(height: 8), + Text( + subtitle, + style: TextStyle(fontSize: 14, color: Colors.grey.shade500), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + IconData _getNotificationIcon(NotificationType type) { + switch (type) { + case NotificationType.order: + return Icons.shopping_bag; + case NotificationType.delivery: + return Icons.local_shipping; + case NotificationType.payment: + return Icons.payment; + case NotificationType.promotion: + return Icons.local_offer; + case NotificationType.system: + return Icons.settings; + } + } + + Color _getNotificationColor(NotificationType type) { + switch (type) { + case NotificationType.order: + return Colors.blue; + case NotificationType.delivery: + return Colors.green; + case NotificationType.payment: + return Colors.orange; + case NotificationType.promotion: + return Colors.purple; + case NotificationType.system: + return Colors.grey; + } + } + + String _formatTime(DateTime time) { + final now = DateTime.now(); + final difference = now.difference(time); + + if (difference.inMinutes < 60) { + return '${difference.inMinutes} menit lalu'; + } else if (difference.inHours < 24) { + return '${difference.inHours} jam lalu'; + } else if (difference.inDays < 7) { + return '${difference.inDays} hari lalu'; + } else { + return '${time.day}/${time.month}/${time.year}'; + } + } +} + +class NotificationItem { + final String id; + final String title; + final String message; + final DateTime time; + final NotificationType type; + bool isRead; + + NotificationItem({ + required this.id, + required this.title, + required this.message, + required this.time, + required this.type, + required this.isRead, + }); +} + +enum NotificationType { order, delivery, payment, promotion, system } diff --git a/lib/features/home/presentation/screen/home_screen.dart b/lib/features/home/presentation/screen/home_screen.dart index 412a8f6..335077d 100644 --- a/lib/features/home/presentation/screen/home_screen.dart +++ b/lib/features/home/presentation/screen/home_screen.dart @@ -65,7 +65,8 @@ class _HomeScreenState extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ IconButton( - onPressed: () => router.push('/trashview'), + // onPressed: () => router.push('/trashview'), + onPressed: () => router.push('/notifikasi'), icon: Icon( Iconsax.notification_copy, color: primaryColor, @@ -74,6 +75,8 @@ class _HomeScreenState extends State { IconButton( onPressed: () { debugPrint('message icon tapped'); + // router.push('/cmapview'); + router.push('/chatlist'); }, icon: Icon(Iconsax.message_copy, color: primaryColor), ), diff --git a/lib/features/profil/account/presentation/screen/account_screen.dart b/lib/features/profil/account/presentation/screen/account_screen.dart new file mode 100644 index 0000000..b5962b8 --- /dev/null +++ b/lib/features/profil/account/presentation/screen/account_screen.dart @@ -0,0 +1,739 @@ +import 'package:flutter/material.dart'; +import 'package:rijig_mobile/core/utils/guide.dart'; +import 'package:rijig_mobile/widget/buttoncard.dart'; +import 'package:rijig_mobile/widget/formfiled.dart'; +import 'package:rijig_mobile/widget/custom_bottom_sheet.dart'; + +class AccountScreen extends StatefulWidget { + const AccountScreen({super.key}); + + @override + State createState() => _AccountScreenState(); +} + +class _AccountScreenState extends State + with TickerProviderStateMixin { + final _nameController = TextEditingController(); + final _phoneController = TextEditingController(); + + String _selectedGender = ''; + DateTime? _selectedDate; + + // Expanded states for each card + bool _isNameExpanded = false; + bool _isPhoneExpanded = false; + bool _isGenderExpanded = false; + bool _isBirthDateExpanded = false; + + // Loading states + bool _isNameLoading = false; + bool _isPhoneLoading = false; + bool _isGenderLoading = false; + bool _isBirthDateLoading = false; + + // Change detection states + bool _hasNameChanged = false; + bool _hasPhoneChanged = false; + bool _hasGenderChanged = false; + bool _hasBirthDateChanged = false; + + // Original values for comparison + String _originalName = 'John Doe'; + String _originalPhone = '+62 812 3456 7890'; + String _originalGender = 'Laki-laki'; + DateTime? _originalBirthDate = DateTime(1990, 1, 15); + + // Current display values + String _userName = 'John Doe'; + String _userPhone = '+62 812 3456 7890'; + String _userGender = 'Laki-laki'; + String _userBirthDate = '15 Januari 1990'; + + @override + void initState() { + super.initState(); + _initializeData(); + _addChangeListeners(); + } + + void _initializeData() { + _nameController.text = _originalName; + _phoneController.text = _originalPhone; + _selectedGender = _originalGender; + _selectedDate = _originalBirthDate; + } + + void _addChangeListeners() { + _nameController.addListener(() { + setState(() { + _hasNameChanged = _nameController.text != _originalName; + }); + }); + + _phoneController.addListener(() { + setState(() { + _hasPhoneChanged = _phoneController.text != _originalPhone; + }); + }); + } + + @override + void dispose() { + _nameController.dispose(); + _phoneController.dispose(); + super.dispose(); + } + + void _selectProfilePhoto() { + CustomBottomSheet.show( + context: context, + title: 'Pilih Foto Profil', + content: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildPhotoOption( + icon: Icons.camera_alt, + label: 'Kamera', + onTap: () { + Navigator.pop(context); + debugPrint('Open Camera'); + }, + ), + _buildPhotoOption( + icon: Icons.photo_library, + label: 'Galeri', + onTap: () { + Navigator.pop(context); + debugPrint('Open Gallery'); + }, + ), + ], + ), + button1: + Container(), // Empty container since we have custom buttons above + ); + } + + Widget _buildPhotoOption({ + required IconData icon, + required String label, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Column( + children: [ + Container( + width: 60, + height: 60, + decoration: BoxDecoration( + color: primaryColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(30), + ), + child: Icon(icon, color: primaryColor, size: 30), + ), + const SizedBox(height: 8), + Text( + label, + style: TextStyle(fontSize: 14, color: Colors.grey.shade700), + ), + ], + ), + ); + } + + void _saveField(String field) { + setState(() { + switch (field) { + case 'name': + _isNameLoading = true; + break; + case 'gender': + _isGenderLoading = true; + break; + case 'birthDate': + _isBirthDateLoading = true; + break; + } + }); + + // Simulate API call + Future.delayed(const Duration(seconds: 1), () { + setState(() { + // Update the display values and reset loading/change states + switch (field) { + case 'name': + _userName = _nameController.text; + _originalName = _nameController.text; + _hasNameChanged = false; + _isNameExpanded = false; + _isNameLoading = false; + break; + case 'gender': + _userGender = _selectedGender; + _originalGender = _selectedGender; + _hasGenderChanged = false; + _isGenderExpanded = false; + _isGenderLoading = false; + break; + case 'birthDate': + if (_selectedDate != null) { + _userBirthDate = + '${_selectedDate!.day} ${_getMonthName(_selectedDate!.month)} ${_selectedDate!.year}'; + _originalBirthDate = _selectedDate; + } + _hasBirthDateChanged = false; + _isBirthDateExpanded = false; + _isBirthDateLoading = false; + break; + } + }); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: const [ + Icon(Icons.check_circle, color: Colors.white), + SizedBox(width: 8), + Text('Data berhasil disimpan'), + ], + ), + backgroundColor: Colors.green, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + ); + }); + } + + void _sendOTP() { + setState(() { + _isPhoneLoading = true; + }); + + // Simulate OTP sending + Future.delayed(const Duration(seconds: 2), () { + setState(() { + _isPhoneLoading = false; + // Update phone after successful OTP verification + _userPhone = _phoneController.text; + _originalPhone = _phoneController.text; + _hasPhoneChanged = false; + _isPhoneExpanded = false; + }); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Kode OTP telah dikirim ke nomor Anda'), + backgroundColor: primaryColor, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + ); + + debugPrint('Send OTP to: ${_phoneController.text}'); + // TODO: Navigate to OTP verification screen + // Navigator.pushNamed(context, '/otp-verification'); + }); + } + + String _getMonthName(int month) { + const months = [ + '', + 'Januari', + 'Februari', + 'Maret', + 'April', + 'Mei', + 'Juni', + 'Juli', + 'Agustus', + 'September', + 'Oktober', + 'November', + 'Desember', + ]; + return months[month]; + } + + Future _selectDate() async { + final DateTime? picked = await showDatePicker( + context: context, + initialDate: _selectedDate ?? DateTime(1990), + firstDate: DateTime(1950), + lastDate: DateTime.now(), + builder: (context, child) { + return Theme( + data: Theme.of(context).copyWith( + colorScheme: ColorScheme.light( + primary: primaryColor, + onPrimary: Colors.white, + surface: Colors.white, + onSurface: Colors.black, + ), + ), + child: child!, + ); + }, + ); + + if (picked != null && picked != _selectedDate) { + setState(() { + _selectedDate = picked; + _hasBirthDateChanged = _selectedDate != _originalBirthDate; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey.shade50, + appBar: AppBar( + backgroundColor: whiteColor, + elevation: 0, + title: Text('Akun Saya', style: Tulisan.subheading()), + centerTitle: true, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.of(context).pop(), + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + // Profile Photo Section + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Stack( + children: [ + CircleAvatar( + radius: 50, + backgroundColor: Colors.grey.shade200, + backgroundImage: const NetworkImage( + 'https://via.placeholder.com/150', + ), + child: const Icon( + Icons.person, + size: 50, + color: Colors.grey, + ), + ), + Positioned( + bottom: 0, + right: 0, + child: GestureDetector( + onTap: _selectProfilePhoto, + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: primaryColor, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + ), + child: const Icon( + Icons.edit, + color: Colors.white, + size: 16, + ), + ), + ), + ), + ], + ), + const SizedBox(height: 16), + Text( + _userName, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + _userPhone, + style: TextStyle(fontSize: 14, color: Colors.grey.shade600), + ), + ], + ), + ), + + const SizedBox(height: 20), + + // Account Information Cards + _buildExpandableCard( + title: 'Nama Lengkap', + value: _userName, + icon: Icons.person, + isExpanded: _isNameExpanded, + hasChanges: _hasNameChanged, + onTap: () { + setState(() { + _isNameExpanded = !_isNameExpanded; + _isPhoneExpanded = false; + _isGenderExpanded = false; + _isBirthDateExpanded = false; + }); + }, + child: _buildNameForm(), + ), + + const SizedBox(height: 12), + + _buildExpandableCard( + title: 'Nomor Telepon', + value: _userPhone, + icon: Icons.phone, + isExpanded: _isPhoneExpanded, + hasChanges: _hasPhoneChanged, + onTap: () { + setState(() { + _isPhoneExpanded = !_isPhoneExpanded; + _isNameExpanded = false; + _isGenderExpanded = false; + _isBirthDateExpanded = false; + }); + }, + child: _buildPhoneForm(), + ), + + const SizedBox(height: 12), + + _buildExpandableCard( + title: 'Jenis Kelamin', + value: _userGender, + icon: Icons.wc, + isExpanded: _isGenderExpanded, + hasChanges: _hasGenderChanged, + onTap: () { + setState(() { + _isGenderExpanded = !_isGenderExpanded; + _isNameExpanded = false; + _isPhoneExpanded = false; + _isBirthDateExpanded = false; + }); + }, + child: _buildGenderForm(), + ), + + const SizedBox(height: 12), + + _buildExpandableCard( + title: 'Tanggal Lahir', + value: _userBirthDate, + icon: Icons.cake, + isExpanded: _isBirthDateExpanded, + hasChanges: _hasBirthDateChanged, + onTap: () { + setState(() { + _isBirthDateExpanded = !_isBirthDateExpanded; + _isNameExpanded = false; + _isPhoneExpanded = false; + _isGenderExpanded = false; + }); + }, + child: _buildBirthDateForm(), + ), + + const SizedBox(height: 32), + ], + ), + ), + ); + } + + Widget _buildExpandableCard({ + required String title, + required String value, + required IconData icon, + required bool isExpanded, + required bool hasChanges, + required VoidCallback onTap, + required Widget child, + }) { + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: primaryColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Icon(icon, color: primaryColor, size: 20), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + title, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + ), + ), + if (hasChanges) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.orange.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + 'Berubah', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w600, + color: Colors.orange.shade700, + ), + ), + ), + ], + ], + ), + const SizedBox(height: 4), + Text( + value, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + AnimatedRotation( + turns: isExpanded ? 0.5 : 0, + duration: const Duration(milliseconds: 300), + child: Icon( + Icons.keyboard_arrow_down, + color: Colors.grey.shade400, + ), + ), + ], + ), + ), + ), + AnimatedCrossFade( + firstChild: const SizedBox.shrink(), + secondChild: Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: child, + ), + crossFadeState: + isExpanded + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + duration: const Duration(milliseconds: 300), + ), + ], + ), + ); + } + + Widget _buildNameForm() { + return Column( + children: [ + const Divider(height: 1), + const SizedBox(height: 16), + FormFieldOne( + controllers: _nameController, + hintText: 'Nama Lengkap', + placeholder: 'Masukkan nama lengkap', + isRequired: true, + onTap: () {}, + ), + const SizedBox(height: 16), + CardButtonOne( + textButton: 'Simpan', + fontSized: 16, + colorText: Colors.white, + borderRadius: 8, + horizontal: double.infinity, + vertical: 45, + color: _hasNameChanged ? primaryColor : Colors.grey.shade400, + loadingTrue: _isNameLoading, + onTap: _hasNameChanged ? () => _saveField('name') : () {}, + ), + ], + ); + } + + Widget _buildPhoneForm() { + return Column( + children: [ + const Divider(height: 1), + const SizedBox(height: 16), + FormFieldOne( + controllers: _phoneController, + hintText: 'Nomor Telepon', + placeholder: 'Masukkan nomor telepon', + keyboardType: TextInputType.phone, + isRequired: true, + onTap: () {}, + ), + const SizedBox(height: 16), + CardButtonOne( + textButton: 'Kirim OTP', + fontSized: 16, + colorText: Colors.white, + borderRadius: 8, + horizontal: double.infinity, + vertical: 45, + color: _hasPhoneChanged ? Colors.orange : Colors.grey.shade400, + loadingTrue: _isPhoneLoading, + onTap: _hasPhoneChanged ? _sendOTP : () {}, + ), + ], + ); + } + + Widget _buildGenderForm() { + return Column( + children: [ + const Divider(height: 1), + const SizedBox(height: 16), + Column( + children: [ + RadioListTile( + title: const Text('Laki-laki'), + value: 'Laki-laki', + groupValue: _selectedGender, + activeColor: primaryColor, + onChanged: (value) { + setState(() { + _selectedGender = value!; + _hasGenderChanged = _selectedGender != _originalGender; + }); + }, + ), + RadioListTile( + title: const Text('Perempuan'), + value: 'Perempuan', + groupValue: _selectedGender, + activeColor: primaryColor, + onChanged: (value) { + setState(() { + _selectedGender = value!; + _hasGenderChanged = _selectedGender != _originalGender; + }); + }, + ), + ], + ), + const SizedBox(height: 16), + CardButtonOne( + textButton: 'Simpan', + fontSized: 16, + colorText: Colors.white, + borderRadius: 8, + horizontal: double.infinity, + vertical: 45, + color: _hasGenderChanged ? primaryColor : Colors.grey.shade400, + loadingTrue: _isGenderLoading, + onTap: _hasGenderChanged ? () => _saveField('gender') : () {}, + ), + ], + ); + } + + Widget _buildBirthDateForm() { + return Column( + children: [ + const Divider(height: 1), + const SizedBox(height: 16), + GestureDetector( + onTap: _selectDate, + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(8), + color: Colors.white, + ), + child: Row( + children: [ + Icon( + Icons.calendar_today, + color: Colors.grey.shade600, + size: 20, + ), + const SizedBox(width: 12), + Text( + _selectedDate != null + ? '${_selectedDate!.day} ${_getMonthName(_selectedDate!.month)} ${_selectedDate!.year}' + : 'Pilih tanggal lahir', + style: TextStyle( + fontSize: 16, + color: + _selectedDate != null + ? Colors.black + : Colors.grey.shade600, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + CardButtonOne( + textButton: 'Simpan', + fontSized: 16, + colorText: Colors.white, + borderRadius: 8, + horizontal: double.infinity, + vertical: 45, + color: _hasBirthDateChanged ? primaryColor : Colors.grey.shade400, + loadingTrue: _isBirthDateLoading, + onTap: _hasBirthDateChanged ? () => _saveField('birthDate') : () {}, + ), + ], + ); + } +} diff --git a/lib/features/profil/address/model/address_model.dart b/lib/features/profil/address/model/address_model.dart new file mode 100644 index 0000000..3d341ae --- /dev/null +++ b/lib/features/profil/address/model/address_model.dart @@ -0,0 +1,17 @@ +class AddressItem { + final String id; + final String label; + final String recipientName; + final String phoneNumber; + final String fullAddress; + final bool isDefault; + + AddressItem({ + required this.id, + required this.label, + required this.recipientName, + required this.phoneNumber, + required this.fullAddress, + required this.isDefault, + }); +} \ No newline at end of file diff --git a/lib/features/profil/address/presentation/screen/add_address_screen.dart b/lib/features/profil/address/presentation/screen/add_address_screen.dart new file mode 100644 index 0000000..37315ff --- /dev/null +++ b/lib/features/profil/address/presentation/screen/add_address_screen.dart @@ -0,0 +1,676 @@ +import 'package:flutter/material.dart'; +import 'package:rijig_mobile/core/router.dart'; +import 'package:rijig_mobile/core/utils/guide.dart'; + +class AddAddressScreen extends StatefulWidget { + const AddAddressScreen({super.key}); + + @override + State createState() => _AddAddressScreenState(); +} + +class _AddAddressScreenState extends State { + final _formKey = GlobalKey(); + final _scrollController = ScrollController(); + + final _labelController = TextEditingController(); + final _recipientNameController = TextEditingController(); + final _phoneController = TextEditingController(); + final _addressController = TextEditingController(); + final _detailController = TextEditingController(); + final _noteController = TextEditingController(); + + final _labelFocus = FocusNode(); + final _recipientNameFocus = FocusNode(); + final _phoneFocus = FocusNode(); + final _addressFocus = FocusNode(); + final _detailFocus = FocusNode(); + final _noteFocus = FocusNode(); + + bool _isDefault = false; + bool _isLoading = false; + String? _selectedProvince; + String? _selectedCity; + String? _selectedDistrict; + + final List _provinces = [ + 'DKI Jakarta', + 'Jawa Barat', + 'Jawa Tengah', + 'Jawa Timur', + 'Banten', + ]; + + final Map> _cities = { + 'DKI Jakarta': [ + 'Jakarta Pusat', + 'Jakarta Utara', + 'Jakarta Selatan', + 'Jakarta Timur', + 'Jakarta Barat', + ], + 'Jawa Barat': ['Bandung', 'Bekasi', 'Bogor', 'Depok', 'Cimahi'], + 'Jawa Tengah': ['Semarang', 'Solo', 'Yogyakarta', 'Magelang', 'Salatiga'], + 'Jawa Timur': ['Surabaya', 'Malang', 'Kediri', 'Blitar', 'Madiun'], + 'Banten': ['Tangerang', 'Tangerang Selatan', 'Serang', 'Cilegon', 'Lebak'], + }; + + final Map> _districts = { + 'Jakarta Pusat': [ + 'Menteng', + 'Gambir', + 'Tanah Abang', + 'Senen', + 'Cempaka Putih', + ], + 'Jakarta Selatan': [ + 'Kebayoran Baru', + 'Kemang', + 'Kuningan', + 'Senayan', + 'Pondok Indah', + ], + 'Bandung': ['Coblong', 'Sukasari', 'Cidadap', 'Dago', 'Antapani'], + 'Surabaya': ['Gubeng', 'Wonokromo', 'Tegalsari', 'Genteng', 'Bubutan'], + }; + + @override + void dispose() { + _labelController.dispose(); + _recipientNameController.dispose(); + _phoneController.dispose(); + _addressController.dispose(); + _detailController.dispose(); + _noteController.dispose(); + _labelFocus.dispose(); + _recipientNameFocus.dispose(); + _phoneFocus.dispose(); + _addressFocus.dispose(); + _detailFocus.dispose(); + _noteFocus.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + void _onProvinceChanged(String? value) { + setState(() { + _selectedProvince = value; + _selectedCity = null; + _selectedDistrict = null; + }); + } + + void _onCityChanged(String? value) { + setState(() { + _selectedCity = value; + _selectedDistrict = null; + }); + } + + void _onDistrictChanged(String? value) { + setState(() { + _selectedDistrict = value; + }); + } + + Future _saveAddress() async { + if (!_formKey.currentState!.validate()) { + _scrollController.animateTo( + 0, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + return; + } + + setState(() { + _isLoading = true; + }); + + await Future.delayed(const Duration(seconds: 2)); + + setState(() { + _isLoading = false; + }); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + const Icon(Icons.check_circle, color: Colors.white), + const SizedBox(width: 8), + const Text('Alamat berhasil disimpan'), + ], + ), + backgroundColor: Colors.green, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + ); + + router.pop(_buildFullAddress); + } + } + + String _buildFullAddress() { + final parts = [ + _addressController.text, + _detailController.text, + _selectedDistrict, + _selectedCity, + _selectedProvince, + ]; + return parts.where((part) => part != null && part.isNotEmpty).join(', '); + } + + void _useCurrentLocation() { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Menggunakan lokasi saat ini...'), + backgroundColor: primaryColor, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey.shade50, + appBar: AppBar( + backgroundColor: whiteColor, + elevation: 0, + title: Text('Tambah Alamat', style: Tulisan.subheading()), + centerTitle: true, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + + onPressed: () => router.pop(), + ), + ), + body: Form( + key: _formKey, + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + controller: _scrollController, + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Row( + children: [ + Icon( + Icons.location_on, + color: primaryColor, + size: 24, + ), + const SizedBox(width: 8), + Text( + 'Lokasi Alamat', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + ], + ), + const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: _useCurrentLocation, + icon: Icon( + Icons.my_location, + size: 18, + color: primaryColor, + ), + label: Text( + 'Gunakan Lokasi Saat Ini', + style: TextStyle(color: primaryColor), + ), + style: OutlinedButton.styleFrom( + side: BorderSide(color: primaryColor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric( + vertical: 12, + ), + ), + ), + ), + ], + ), + ), + + const SizedBox(height: 16), + + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Informasi Alamat', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + const SizedBox(height: 16), + + _buildTextField( + controller: _labelController, + focusNode: _labelFocus, + label: 'Label Alamat', + hint: 'Contoh: Rumah, Kantor, Kost', + icon: Icons.label, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Label alamat tidak boleh kosong'; + } + return null; + }, + onFieldSubmitted: (value) { + FocusScope.of( + context, + ).requestFocus(_recipientNameFocus); + }, + ), + + const SizedBox(height: 16), + + _buildTextField( + controller: _recipientNameController, + focusNode: _recipientNameFocus, + label: 'Nama Penerima', + hint: 'Masukkan nama lengkap penerima', + icon: Icons.person, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Nama penerima tidak boleh kosong'; + } + return null; + }, + onFieldSubmitted: (value) { + FocusScope.of(context).requestFocus(_phoneFocus); + }, + ), + + const SizedBox(height: 16), + + _buildTextField( + controller: _phoneController, + focusNode: _phoneFocus, + label: 'Nomor Telepon', + hint: 'Contoh: 08123456789', + icon: Icons.phone, + keyboardType: TextInputType.phone, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Nomor telepon tidak boleh kosong'; + } + if (value.length < 10) { + return 'Nomor telepon minimal 10 digit'; + } + return null; + }, + onFieldSubmitted: (value) { + FocusScope.of( + context, + ).requestFocus(_addressFocus); + }, + ), + + const SizedBox(height: 16), + + _buildDropdown( + label: 'Provinsi', + value: _selectedProvince, + items: _provinces, + onChanged: _onProvinceChanged, + hint: 'Pilih Provinsi', + icon: Icons.location_city, + ), + + const SizedBox(height: 16), + + _buildDropdown( + label: 'Kota/Kabupaten', + value: _selectedCity, + items: + _selectedProvince != null + ? _cities[_selectedProvince!] ?? [] + : [], + onChanged: _onCityChanged, + hint: 'Pilih Kota/Kabupaten', + icon: Icons.business, + enabled: _selectedProvince != null, + ), + + const SizedBox(height: 16), + + _buildDropdown( + label: 'Kecamatan', + value: _selectedDistrict, + items: + _selectedCity != null + ? _districts[_selectedCity!] ?? [] + : [], + onChanged: _onDistrictChanged, + hint: 'Pilih Kecamatan', + icon: Icons.map, + enabled: _selectedCity != null, + ), + + const SizedBox(height: 16), + + _buildTextField( + controller: _addressController, + focusNode: _addressFocus, + label: 'Alamat Lengkap', + hint: 'Jalan, RT/RW, Kelurahan', + icon: Icons.home, + maxLines: 3, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Alamat lengkap tidak boleh kosong'; + } + return null; + }, + onFieldSubmitted: (value) { + FocusScope.of(context).requestFocus(_detailFocus); + }, + ), + + const SizedBox(height: 16), + + _buildTextField( + controller: _detailController, + focusNode: _detailFocus, + label: 'Detail Alamat (Opsional)', + hint: 'Patokan, warna rumah, nomor rumah, dll', + icon: Icons.info, + maxLines: 2, + onFieldSubmitted: (value) { + FocusScope.of(context).requestFocus(_noteFocus); + }, + ), + + const SizedBox(height: 16), + + _buildTextField( + controller: _noteController, + focusNode: _noteFocus, + label: 'Catatan untuk Kurir (Opsional)', + hint: 'Instruksi khusus untuk kurir', + icon: Icons.note, + maxLines: 2, + ), + + const SizedBox(height: 20), + + Row( + children: [ + Checkbox( + value: _isDefault, + onChanged: (bool? value) { + setState(() { + _isDefault = value ?? false; + }); + }, + activeColor: primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + Expanded( + child: Text( + 'Jadikan sebagai alamat utama', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + ), + ), + ), + ], + ), + ], + ), + ), + + const SizedBox(height: 80), + ], + ), + ), + ), + + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 10, + offset: const Offset(0, -2), + ), + ], + ), + child: SafeArea( + child: SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: _isLoading ? null : _saveAddress, + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + child: + _isLoading + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ), + const SizedBox(width: 12), + const Text( + 'Menyimpan...', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ], + ) + : const Text( + 'Simpan Alamat', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildTextField({ + required TextEditingController controller, + required FocusNode focusNode, + required String label, + required String hint, + required IconData icon, + int maxLines = 1, + TextInputType? keyboardType, + String? Function(String?)? validator, + void Function(String)? onFieldSubmitted, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, + ), + ), + const SizedBox(height: 8), + TextFormField( + controller: controller, + focusNode: focusNode, + maxLines: maxLines, + keyboardType: keyboardType, + validator: validator, + onFieldSubmitted: onFieldSubmitted, + decoration: InputDecoration( + hintText: hint, + prefixIcon: Icon(icon, color: Colors.grey.shade500), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: primaryColor, width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: Colors.red), + ), + filled: true, + fillColor: Colors.grey.shade50, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + ), + ], + ); + } + + Widget _buildDropdown({ + required String label, + required String? value, + required List items, + required void Function(String?) onChanged, + required String hint, + required IconData icon, + bool enabled = true, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, + ), + ), + const SizedBox(height: 8), + DropdownButtonFormField( + value: value, + items: + items.map((String item) { + return DropdownMenuItem(value: item, child: Text(item)); + }).toList(), + onChanged: enabled ? onChanged : null, + validator: (value) { + if (enabled && (value == null || value.isEmpty)) { + return '$label tidak boleh kosong'; + } + return null; + }, + decoration: InputDecoration( + hintText: hint, + prefixIcon: Icon( + icon, + color: enabled ? Colors.grey.shade500 : Colors.grey.shade400, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: primaryColor, width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: Colors.red), + ), + filled: true, + fillColor: enabled ? Colors.grey.shade50 : Colors.grey.shade100, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + ), + ], + ); + } +} diff --git a/lib/features/profil/address/presentation/screen/address_screen.dart b/lib/features/profil/address/presentation/screen/address_screen.dart new file mode 100644 index 0000000..fe80a42 --- /dev/null +++ b/lib/features/profil/address/presentation/screen/address_screen.dart @@ -0,0 +1,562 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:rijig_mobile/core/router.dart'; +import 'package:rijig_mobile/core/utils/guide.dart'; +import 'package:rijig_mobile/features/profil/address/model/address_model.dart'; + +class AddressScreen extends StatefulWidget { + const AddressScreen({super.key}); + + @override + State createState() => _AddressScreenState(); +} + +class _AddressScreenState extends State { + int? selectedAddressIndex; + + final List addresses = [ + AddressItem( + id: '1', + label: 'Rumah', + recipientName: 'John Doe', + phoneNumber: '+62 812-3456-7890', + fullAddress: + 'Jl. Merdeka No. 123, RT 05/RW 02, Kelurahan Suka Maju, Kecamatan Menteng, Jakarta Pusat, DKI Jakarta 10310', + isDefault: true, + ), + AddressItem( + id: '2', + label: 'Kantor', + recipientName: 'John Doe', + phoneNumber: '+62 812-3456-7890', + fullAddress: + 'Gedung Cyber 2 Tower, Lantai 15, Jl. HR Rasuna Said, Kuningan, Jakarta Selatan, DKI Jakarta 12950', + isDefault: false, + ), + AddressItem( + id: '3', + label: 'Rumah Orang Tua', + recipientName: 'Jane Doe', + phoneNumber: '+62 821-9876-5432', + fullAddress: + 'Komplek Permata Hijau Blok A No. 45, Jl. Kemang Raya, Kemang, Jakarta Selatan, DKI Jakarta 12560', + isDefault: false, + ), + ]; + + void _selectAddress(int index) { + setState(() { + if (selectedAddressIndex == index) { + selectedAddressIndex = null; + } else { + selectedAddressIndex = index; + } + }); + } + + void _confirmAddressSelection() { + if (selectedAddressIndex != null) { + final selectedAddress = addresses[selectedAddressIndex!]; + + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + title: Row( + children: [ + Icon(Icons.check_circle, color: Colors.green, size: 24), + const SizedBox(width: 8), + const Text('Alamat Dipilih'), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Alamat pengiriman berhasil dipilih:', + style: TextStyle(color: Colors.grey.shade600), + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.green.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + selectedAddress.label, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + const SizedBox(height: 4), + Text( + selectedAddress.recipientName, + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 13, + ), + ), + const SizedBox(height: 2), + Text( + selectedAddress.fullAddress, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade700, + ), + ), + ], + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => router.pop(), + + child: Text('OK', style: TextStyle(color: primaryColor)), + ), + ], + ); + }, + ); + } + } + + void _addNewAddress() { + router.push('/addaddress'); + } + + void _editAddress(AddressItem address) { + router.push('/editaddress', extra: address); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Edit alamat: ${address.label}'), + backgroundColor: Colors.orange, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + ); + } + + void _deleteAddress(AddressItem address, int index) { + setState(() { + addresses.removeAt(index); + if (selectedAddressIndex == index) { + selectedAddressIndex = null; + } else if (selectedAddressIndex != null && + selectedAddressIndex! > index) { + selectedAddressIndex = selectedAddressIndex! - 1; + } + }); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Alamat "${address.label}" telah dihapus'), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey.shade50, + appBar: AppBar( + backgroundColor: whiteColor, + elevation: 0, + title: Text('Pilih Alamat', style: Tulisan.subheading()), + centerTitle: true, + actions: [ + TextButton( + onPressed: _addNewAddress, + child: Text( + 'Tambah Alamat', + style: TextStyle( + color: primaryColor, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + ), + ], + ), + body: Column( + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + color: Colors.white, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.location_on, color: primaryColor, size: 20), + const SizedBox(width: 8), + Text( + 'Alamat Pengiriman', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + 'Pilih alamat untuk pengiriman pesanan Anda', + style: TextStyle(fontSize: 13, color: Colors.grey.shade600), + ), + ], + ), + ), + + const SizedBox(height: 8), + + Expanded( + child: + addresses.isEmpty + ? _buildEmptyState() + : ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: addresses.length, + itemBuilder: (context, index) { + final address = addresses[index]; + final isSelected = selectedAddressIndex == index; + + return Slidable( + key: Key(address.id), + endActionPane: ActionPane( + motion: const ScrollMotion(), + children: [ + SlidableAction( + onPressed: + (context) => _deleteAddress(address, index), + backgroundColor: Colors.red, + foregroundColor: Colors.white, + icon: Icons.delete, + label: 'Hapus', + borderRadius: BorderRadius.circular(12), + ), + ], + ), + child: _buildAddressCard(address, index, isSelected), + ); + }, + ), + ), + + if (addresses.isNotEmpty) + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 10, + offset: const Offset(0, -2), + ), + ], + ), + child: SafeArea( + child: SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: + selectedAddressIndex != null + ? _confirmAddressSelection + : null, + style: ElevatedButton.styleFrom( + backgroundColor: + selectedAddressIndex != null + ? primaryColor + : Colors.grey.shade300, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + child: Text( + selectedAddressIndex != null + ? 'Pilih Alamat' + : 'Pilih salah satu alamat', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: + selectedAddressIndex != null + ? Colors.white + : Colors.grey.shade600, + ), + ), + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildAddressCard(AddressItem address, int index, bool isSelected) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: + isSelected + ? Border.all(color: primaryColor, width: 2) + : Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _selectAddress(index), + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: _getLabelColor( + address.label, + ).withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + address.label, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: _getLabelColor(address.label), + ), + ), + ), + + if (address.isDefault) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.green.shade100, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + 'Utama', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w600, + color: Colors.green.shade700, + ), + ), + ), + ], + + const Spacer(), + + if (isSelected) + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: primaryColor, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.check, + color: Colors.white, + size: 16, + ), + ) + else + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + shape: BoxShape.circle, + ), + ), + ], + ), + + const SizedBox(height: 12), + + Row( + children: [ + Icon(Icons.person, size: 16, color: Colors.grey.shade600), + const SizedBox(width: 6), + Text( + address.recipientName, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 12), + Icon(Icons.phone, size: 16, color: Colors.grey.shade600), + const SizedBox(width: 6), + Text( + address.phoneNumber, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + ), + ), + ], + ), + + const SizedBox(height: 8), + + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.location_on, + size: 16, + color: Colors.grey.shade600, + ), + const SizedBox(width: 6), + Expanded( + child: Text( + address.fullAddress, + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade700, + height: 1.4, + ), + ), + ), + ], + ), + + if (isSelected) ...[ + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: () => _editAddress(address), + icon: Icon(Icons.edit, size: 16, color: primaryColor), + label: Text( + 'Edit Alamat', + style: TextStyle(color: primaryColor), + ), + style: OutlinedButton.styleFrom( + side: BorderSide(color: primaryColor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric(vertical: 12), + ), + ), + ), + ], + ], + ), + ), + ), + ), + ); + } + + Widget _buildEmptyState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: Colors.grey.shade100, + shape: BoxShape.circle, + ), + child: Icon( + Icons.location_off, + size: 40, + color: Colors.grey.shade400, + ), + ), + const SizedBox(height: 20), + Text( + 'Belum Ada Alamat', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, + ), + ), + const SizedBox(height: 8), + Text( + 'Tambahkan alamat untuk memudahkan pengiriman', + style: TextStyle(fontSize: 14, color: Colors.grey.shade500), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + ElevatedButton.icon( + onPressed: _addNewAddress, + icon: const Icon(Icons.add, color: Colors.white), + label: const Text( + 'Tambah Alamat', + style: TextStyle(color: Colors.white), + ), + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + ), + ), + ], + ), + ); + } + + Color _getLabelColor(String label) { + switch (label.toLowerCase()) { + case 'rumah': + return Colors.blue; + case 'kantor': + return Colors.orange; + case 'rumah orang tua': + return Colors.purple; + default: + return Colors.grey; + } + } +} diff --git a/lib/features/profil/address/presentation/screen/edit_address_screen.dart b/lib/features/profil/address/presentation/screen/edit_address_screen.dart new file mode 100644 index 0000000..e87c19b --- /dev/null +++ b/lib/features/profil/address/presentation/screen/edit_address_screen.dart @@ -0,0 +1,889 @@ +import 'package:flutter/material.dart'; +import 'package:rijig_mobile/core/router.dart'; +import 'package:rijig_mobile/core/utils/guide.dart'; +import 'package:go_router/go_router.dart'; +import 'package:rijig_mobile/features/profil/address/model/address_model.dart'; + +class EditAddressScreen extends StatefulWidget { + final AddressItem address; + + const EditAddressScreen({super.key, required this.address}); + + @override + State createState() => _EditAddressScreenState(); +} + +class _EditAddressScreenState extends State { + final _formKey = GlobalKey(); + final _scrollController = ScrollController(); + + final _labelController = TextEditingController(); + final _recipientNameController = TextEditingController(); + final _phoneController = TextEditingController(); + final _addressController = TextEditingController(); + final _detailController = TextEditingController(); + final _noteController = TextEditingController(); + + final _labelFocus = FocusNode(); + final _recipientNameFocus = FocusNode(); + final _phoneFocus = FocusNode(); + final _addressFocus = FocusNode(); + final _detailFocus = FocusNode(); + final _noteFocus = FocusNode(); + + bool _isDefault = false; + bool _isLoading = false; + bool _hasChanges = false; + String? _selectedProvince; + String? _selectedCity; + String? _selectedDistrict; + + final List _provinces = [ + 'DKI Jakarta', + 'Jawa Barat', + 'Jawa Tengah', + 'Jawa Timur', + 'Banten', + ]; + + final Map> _cities = { + 'DKI Jakarta': [ + 'Jakarta Pusat', + 'Jakarta Utara', + 'Jakarta Selatan', + 'Jakarta Timur', + 'Jakarta Barat', + ], + 'Jawa Barat': ['Bandung', 'Bekasi', 'Bogor', 'Depok', 'Cimahi'], + 'Jawa Tengah': ['Semarang', 'Solo', 'Yogyakarta', 'Magelang', 'Salatiga'], + 'Jawa Timur': ['Surabaya', 'Malang', 'Kediri', 'Blitar', 'Madiun'], + 'Banten': ['Tangerang', 'Tangerang Selatan', 'Serang', 'Cilegon', 'Lebak'], + }; + + final Map> _districts = { + 'Jakarta Pusat': [ + 'Menteng', + 'Gambir', + 'Tanah Abang', + 'Senen', + 'Cempaka Putih', + ], + 'Jakarta Selatan': [ + 'Kebayoran Baru', + 'Kemang', + 'Kuningan', + 'Senayan', + 'Pondok Indah', + ], + 'Bandung': ['Coblong', 'Sukasari', 'Cidadap', 'Dago', 'Antapani'], + 'Surabaya': ['Gubeng', 'Wonokromo', 'Tegalsari', 'Genteng', 'Bubutan'], + }; + + @override + void initState() { + super.initState(); + _initializeForm(); + _addChangeListeners(); + } + + void _initializeForm() { + _labelController.text = widget.address.label; + _recipientNameController.text = widget.address.recipientName; + _phoneController.text = widget.address.phoneNumber; + _isDefault = widget.address.isDefault; + + _parseFullAddress(widget.address.fullAddress); + } + + void _parseFullAddress(String fullAddress) { + final parts = fullAddress.split(', '); + + if (parts.isNotEmpty) { + _addressController.text = parts[0]; + } + + if (fullAddress.contains('Jakarta')) { + _selectedProvince = 'DKI Jakarta'; + if (fullAddress.contains('Jakarta Pusat')) { + _selectedCity = 'Jakarta Pusat'; + _selectedDistrict = 'Menteng'; + } + } + + if (parts.length > 3) { + _detailController.text = parts[1]; + } + } + + void _addChangeListeners() { + _labelController.addListener(_onFormChanged); + _recipientNameController.addListener(_onFormChanged); + _phoneController.addListener(_onFormChanged); + _addressController.addListener(_onFormChanged); + _detailController.addListener(_onFormChanged); + _noteController.addListener(_onFormChanged); + } + + void _onFormChanged() { + if (!_hasChanges) { + setState(() { + _hasChanges = true; + }); + } + } + + @override + void dispose() { + _labelController.dispose(); + _recipientNameController.dispose(); + _phoneController.dispose(); + _addressController.dispose(); + _detailController.dispose(); + _noteController.dispose(); + _labelFocus.dispose(); + _recipientNameFocus.dispose(); + _phoneFocus.dispose(); + _addressFocus.dispose(); + _detailFocus.dispose(); + _noteFocus.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + void _onProvinceChanged(String? value) { + setState(() { + _selectedProvince = value; + _selectedCity = null; + _selectedDistrict = null; + _hasChanges = true; + }); + } + + void _onCityChanged(String? value) { + setState(() { + _selectedCity = value; + _selectedDistrict = null; + _hasChanges = true; + }); + } + + void _onDistrictChanged(String? value) { + setState(() { + _selectedDistrict = value; + _hasChanges = true; + }); + } + + Future _showUnsavedChangesDialog() async { + if (!_hasChanges) return true; + + return await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + title: Row( + children: [ + Icon(Icons.warning, color: Colors.orange, size: 24), + const SizedBox(width: 8), + const Text('Perubahan Belum Disimpan'), + ], + ), + content: const Text( + 'Anda memiliki perubahan yang belum disimpan. Apakah Anda yakin ingin keluar?', + ), + actions: [ + TextButton( + onPressed: () => context.pop(false), + child: Text( + 'Batal', + style: TextStyle(color: Colors.grey.shade600), + ), + ), + TextButton( + onPressed: () => context.pop(true), + child: const Text( + 'Keluar', + style: TextStyle(color: Colors.red), + ), + ), + ], + ); + }, + ) ?? + false; + } + + Future _updateAddress() async { + if (!_formKey.currentState!.validate()) { + _scrollController.animateTo( + 0, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + return; + } + + setState(() { + _isLoading = true; + }); + + await Future.delayed(const Duration(seconds: 2)); + + setState(() { + _isLoading = false; + }); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + const Icon(Icons.check_circle, color: Colors.white), + const SizedBox(width: 8), + const Text('Alamat berhasil diperbarui'), + ], + ), + backgroundColor: Colors.green, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + ); + + context.pop({ + 'id': widget.address.id, + 'label': _labelController.text, + 'recipientName': _recipientNameController.text, + 'phone': _phoneController.text, + 'fullAddress': _buildFullAddress(), + 'isDefault': _isDefault, + }); + } + } + + String _buildFullAddress() { + final parts = [ + _addressController.text, + _detailController.text, + _selectedDistrict, + _selectedCity, + _selectedProvince, + ]; + return parts.where((part) => part != null && part.isNotEmpty).join(', '); + } + + void _useCurrentLocation() { + setState(() { + _hasChanges = true; + }); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Menggunakan lokasi saat ini...'), + backgroundColor: primaryColor, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + ); + } + + void _deleteAddress() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + title: Row( + children: [ + Icon(Icons.warning, color: Colors.red, size: 24), + const SizedBox(width: 8), + const Text('Hapus Alamat'), + ], + ), + content: Text( + 'Apakah Anda yakin ingin menghapus alamat "${widget.address.label}"?', + ), + actions: [ + TextButton( + onPressed: () => context.pop(), + child: Text( + 'Batal', + style: TextStyle(color: Colors.grey.shade600), + ), + ), + TextButton( + onPressed: () { + context.pop(); + context.pop({'action': 'delete', 'id': widget.address.id}); + }, + child: const Text('Hapus', style: TextStyle(color: Colors.red)), + ), + ], + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey.shade50, + appBar: AppBar( + backgroundColor: whiteColor, + elevation: 0, + title: Text('Edit Alamat', style: Tulisan.subheading()), + centerTitle: true, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () async { + if (await _showUnsavedChangesDialog()) { + router.pop(); + } + }, + ), + actions: [ + PopupMenuButton( + icon: Icon(Icons.more_vert, color: Colors.grey.shade700), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + onSelected: (value) { + if (value == 'delete') { + _deleteAddress(); + } + }, + itemBuilder: + (BuildContext context) => [ + PopupMenuItem( + value: 'delete', + child: Row( + children: [ + const Icon(Icons.delete, size: 18, color: Colors.red), + const SizedBox(width: 8), + const Text( + 'Hapus Alamat', + style: TextStyle(color: Colors.red), + ), + ], + ), + ), + ], + ), + ], + ), + body: Form( + key: _formKey, + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + controller: _scrollController, + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Row( + children: [ + Icon( + Icons.edit_location, + color: primaryColor, + size: 24, + ), + const SizedBox(width: 8), + Text( + 'Edit Lokasi Alamat', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + ], + ), + const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: _useCurrentLocation, + icon: Icon( + Icons.my_location, + size: 18, + color: primaryColor, + ), + label: Text( + 'Gunakan Lokasi Saat Ini', + style: TextStyle(color: primaryColor), + ), + style: OutlinedButton.styleFrom( + side: BorderSide(color: primaryColor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric( + vertical: 12, + ), + ), + ), + ), + ], + ), + ), + + const SizedBox(height: 16), + + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'Informasi Alamat', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + const Spacer(), + if (_hasChanges) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: Colors.orange.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + 'Ada Perubahan', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w600, + color: Colors.orange.shade700, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + + _buildTextField( + controller: _labelController, + focusNode: _labelFocus, + label: 'Label Alamat', + hint: 'Contoh: Rumah, Kantor, Kost', + icon: Icons.label, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Label alamat tidak boleh kosong'; + } + return null; + }, + onFieldSubmitted: (value) { + FocusScope.of( + context, + ).requestFocus(_recipientNameFocus); + }, + ), + + const SizedBox(height: 16), + + _buildTextField( + controller: _recipientNameController, + focusNode: _recipientNameFocus, + label: 'Nama Penerima', + hint: 'Masukkan nama lengkap penerima', + icon: Icons.person, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Nama penerima tidak boleh kosong'; + } + return null; + }, + onFieldSubmitted: (value) { + FocusScope.of(context).requestFocus(_phoneFocus); + }, + ), + + const SizedBox(height: 16), + + _buildTextField( + controller: _phoneController, + focusNode: _phoneFocus, + label: 'Nomor Telepon', + hint: 'Contoh: 08123456789', + icon: Icons.phone, + keyboardType: TextInputType.phone, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Nomor telepon tidak boleh kosong'; + } + if (value.length < 10) { + return 'Nomor telepon minimal 10 digit'; + } + return null; + }, + onFieldSubmitted: (value) { + FocusScope.of( + context, + ).requestFocus(_addressFocus); + }, + ), + + const SizedBox(height: 16), + + _buildDropdown( + label: 'Provinsi', + value: _selectedProvince, + items: _provinces, + onChanged: _onProvinceChanged, + hint: 'Pilih Provinsi', + icon: Icons.location_city, + ), + + const SizedBox(height: 16), + + _buildDropdown( + label: 'Kota/Kabupaten', + value: _selectedCity, + items: + _selectedProvince != null + ? _cities[_selectedProvince!] ?? [] + : [], + onChanged: _onCityChanged, + hint: 'Pilih Kota/Kabupaten', + icon: Icons.business, + enabled: _selectedProvince != null, + ), + + const SizedBox(height: 16), + + _buildDropdown( + label: 'Kecamatan', + value: _selectedDistrict, + items: + _selectedCity != null + ? _districts[_selectedCity!] ?? [] + : [], + onChanged: _onDistrictChanged, + hint: 'Pilih Kecamatan', + icon: Icons.map, + enabled: _selectedCity != null, + ), + + const SizedBox(height: 16), + + _buildTextField( + controller: _addressController, + focusNode: _addressFocus, + label: 'Alamat Lengkap', + hint: 'Jalan, RT/RW, Kelurahan', + icon: Icons.home, + maxLines: 3, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Alamat lengkap tidak boleh kosong'; + } + return null; + }, + onFieldSubmitted: (value) { + FocusScope.of(context).requestFocus(_detailFocus); + }, + ), + + const SizedBox(height: 16), + + _buildTextField( + controller: _detailController, + focusNode: _detailFocus, + label: 'Detail Alamat (Opsional)', + hint: 'Patokan, warna rumah, nomor rumah, dll', + icon: Icons.info, + maxLines: 2, + onFieldSubmitted: (value) { + FocusScope.of(context).requestFocus(_noteFocus); + }, + ), + + const SizedBox(height: 16), + + _buildTextField( + controller: _noteController, + focusNode: _noteFocus, + label: 'Catatan untuk Kurir (Opsional)', + hint: 'Instruksi khusus untuk kurir', + icon: Icons.note, + maxLines: 2, + ), + + const SizedBox(height: 20), + + Row( + children: [ + Checkbox( + value: _isDefault, + onChanged: (bool? value) { + setState(() { + _isDefault = value ?? false; + _hasChanges = true; + }); + }, + activeColor: primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + Expanded( + child: Text( + 'Jadikan sebagai alamat utama', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + ), + ), + ), + ], + ), + ], + ), + ), + + const SizedBox(height: 80), + ], + ), + ), + ), + + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 10, + offset: const Offset(0, -2), + ), + ], + ), + child: SafeArea( + child: SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: _isLoading ? null : _updateAddress, + style: ElevatedButton.styleFrom( + backgroundColor: + _hasChanges ? primaryColor : Colors.grey.shade400, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + child: + _isLoading + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ), + const SizedBox(width: 12), + const Text( + 'Memperbarui...', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ], + ) + : Text( + _hasChanges + ? 'Simpan Perubahan' + : 'Simpan Alamat', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildTextField({ + required TextEditingController controller, + required FocusNode focusNode, + required String label, + required String hint, + required IconData icon, + int maxLines = 1, + TextInputType? keyboardType, + String? Function(String?)? validator, + void Function(String)? onFieldSubmitted, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, + ), + ), + const SizedBox(height: 8), + TextFormField( + controller: controller, + focusNode: focusNode, + maxLines: maxLines, + keyboardType: keyboardType, + validator: validator, + onFieldSubmitted: onFieldSubmitted, + decoration: InputDecoration( + hintText: hint, + prefixIcon: Icon(icon, color: Colors.grey.shade500), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: primaryColor, width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: Colors.red), + ), + filled: true, + fillColor: Colors.grey.shade50, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + ), + ], + ); + } + + Widget _buildDropdown({ + required String label, + required String? value, + required List items, + required void Function(String?) onChanged, + required String hint, + required IconData icon, + bool enabled = true, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, + ), + ), + const SizedBox(height: 8), + DropdownButtonFormField( + value: value, + items: + items.map((String item) { + return DropdownMenuItem(value: item, child: Text(item)); + }).toList(), + onChanged: enabled ? onChanged : null, + validator: (value) { + if (enabled && (value == null || value.isEmpty)) { + return '$label tidak boleh kosong'; + } + return null; + }, + decoration: InputDecoration( + hintText: hint, + prefixIcon: Icon( + icon, + color: enabled ? Colors.grey.shade500 : Colors.grey.shade400, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: primaryColor, width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: Colors.red), + ), + filled: true, + fillColor: enabled ? Colors.grey.shade50 : Colors.grey.shade100, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + ), + ], + ); + } +} diff --git a/lib/features/profil/components/profile_menu_option.dart b/lib/features/profil/components/profile_menu_option.dart index d9a2d95..24a248b 100644 --- a/lib/features/profil/components/profile_menu_option.dart +++ b/lib/features/profil/components/profile_menu_option.dart @@ -1,98 +1,100 @@ -import 'package:flutter/material.dart'; -import 'package:iconsax_flutter/iconsax_flutter.dart'; -import 'package:rijig_mobile/core/router.dart'; -import 'package:rijig_mobile/core/utils/guide.dart'; -import 'package:rijig_mobile/features/profil/components/profile_list_tile.dart'; -import 'package:rijig_mobile/widget/buttoncard.dart'; -import 'package:rijig_mobile/widget/custom_bottom_sheet.dart'; +// import 'package:flutter/material.dart'; +// import 'package:iconsax_flutter/iconsax_flutter.dart'; +// import 'package:rijig_mobile/core/router.dart'; +// import 'package:rijig_mobile/core/utils/guide.dart'; +// import 'package:rijig_mobile/features/profil/components/profile_list_tile.dart'; +// import 'package:rijig_mobile/widget/buttoncard.dart'; +// import 'package:rijig_mobile/widget/custom_bottom_sheet.dart'; -class ProfileMenuOptions extends StatelessWidget { - const ProfileMenuOptions({super.key}); +// class ProfileMenuOptions extends StatelessWidget { +// const ProfileMenuOptions({super.key}); - @override - Widget build(BuildContext context) { - return Container( - padding: PaddingCustom().paddingAll(10), - decoration: BoxDecoration( - color: whiteColor, - border: Border.all(color: greyColor), - borderRadius: BorderRadius.circular(10), - ), - child: Column( - children: [ - ProfileListTile( - title: 'Ubah Pin', - iconColor: primaryColor, - icon: Iconsax.wallet, - onTap: () { - router.push('/pinsecureinput'); - }, - ), - Divider(thickness: 0.7, color: greyColor), - ProfileListTile( - title: 'Alamat', - iconColor: primaryColor, - icon: Iconsax.wallet, - onTap: () {}, - ), - Divider(thickness: 0.7, color: greyColor), - ProfileListTile( - title: 'Bantuan', - icon: Iconsax.wallet, - iconColor: primaryColor, - onTap: () {}, - ), - Divider(thickness: 0.7, color: greyColor), - ProfileListTile( - title: 'Ulasan', - icon: Iconsax.wallet, - iconColor: primaryColor, - onTap: () {}, - ), - Divider(thickness: 0.7, color: greyColor), - ProfileListTile( - title: 'Keluar', - icon: Iconsax.logout, - iconColor: redColor, - onTap: - () => CustomBottomSheet.show( - context: context, - title: "Logout Sekarang?", - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("Yakin ingin logout dari akun ini?"), - // tambahan konten - ], - ), - button1: CardButtonOne( - textButton: "Logout", - onTap: () {}, - fontSized: 14, - colorText: Colors.white, - color: Colors.red, - borderRadius: 10, - horizontal: double.infinity, - vertical: 50, - loadingTrue: false, - usingRow: false, - ), - button2: CardButtonOne( - textButton: "Batal", - onTap: () => router.pop(), - fontSized: 14, - colorText: Colors.red, - color: Colors.white, - borderRadius: 10, - horizontal: double.infinity, - vertical: 50, - loadingTrue: false, - usingRow: false, - ), - ), - ), - ], - ), - ); - } -} +// @override +// Widget build(BuildContext context) { +// return Container( +// padding: PaddingCustom().paddingAll(10), +// decoration: BoxDecoration( +// color: whiteColor, +// border: Border.all(color: greyColor), +// borderRadius: BorderRadius.circular(10), +// ), +// child: Column( +// children: [ +// ProfileListTile( +// title: 'Pin', +// iconColor: primaryColor, +// icon: Iconsax.wallet, +// onTap: () { +// router.push('/pinsecureinput'); +// }, +// ), +// Divider(thickness: 0.7, color: greyColor), +// ProfileListTile( +// title: 'Alamat', +// iconColor: primaryColor, +// icon: Iconsax.wallet, +// onTap: () { +// router.push('/address'); +// }, +// ), +// Divider(thickness: 0.7, color: greyColor), +// ProfileListTile( +// title: 'Bantuan', +// icon: Iconsax.wallet, +// iconColor: primaryColor, +// onTap: () {}, +// ), +// Divider(thickness: 0.7, color: greyColor), +// ProfileListTile( +// title: 'Ulasan', +// icon: Iconsax.wallet, +// iconColor: primaryColor, +// onTap: () {}, +// ), +// Divider(thickness: 0.7, color: greyColor), +// ProfileListTile( +// title: 'Keluar', +// icon: Iconsax.logout, +// iconColor: redColor, +// onTap: +// () => CustomBottomSheet.show( +// context: context, +// title: "Logout Sekarang?", +// content: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Text("Yakin ingin logout dari akun ini?"), +// // tambahan konten +// ], +// ), +// button1: CardButtonOne( +// textButton: "Logout", +// onTap: () {}, +// fontSized: 14, +// colorText: Colors.white, +// color: Colors.red, +// borderRadius: 10, +// horizontal: double.infinity, +// vertical: 50, +// loadingTrue: false, +// usingRow: false, +// ), +// button2: CardButtonOne( +// textButton: "Batal", +// onTap: () => router.pop(), +// fontSized: 14, +// colorText: Colors.red, +// color: Colors.white, +// borderRadius: 10, +// horizontal: double.infinity, +// vertical: 50, +// loadingTrue: false, +// usingRow: false, +// ), +// ), +// ), +// ], +// ), +// ); +// } +// } diff --git a/lib/features/profil/presentation/screen/profil_screen.dart b/lib/features/profil/presentation/screen/profil_screen.dart index 15dc13f..e7357ec 100644 --- a/lib/features/profil/presentation/screen/profil_screen.dart +++ b/lib/features/profil/presentation/screen/profil_screen.dart @@ -230,6 +230,7 @@ class _ProfilScreenState extends State { switch (menuTitle) { case 'Profil': debugPrint('Profil'); + router.push('/akunprofil'); break; case 'Ubah Pin': @@ -239,6 +240,7 @@ class _ProfilScreenState extends State { case 'Alamat': debugPrint('Alamat'); + router.push('/address'); break; case 'Bantuan': diff --git a/lib/widget/buttoncard.dart b/lib/widget/buttoncard.dart index 98cd751..27bf717 100644 --- a/lib/widget/buttoncard.dart +++ b/lib/widget/buttoncard.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:rijig_mobile/core/utils/guide.dart'; class CardButtonOne extends StatelessWidget { @@ -53,25 +52,21 @@ class CardButtonOne extends StatelessWidget { ? usingRow == false ? Text( textButton, - style: GoogleFonts.roboto( - textStyle: TextStyle( - fontWeight: bold, - fontSize: fontSized.sp, - color: colorText, - ), + style: Tulisan.customText( + color: colorText, + fontWeight: extraBold, + fontsize: 16, ), ) : Row( mainAxisSize: mainAxisSize, + mainAxisAlignment: MainAxisAlignment.center, children: [ Text( textButton, - style: GoogleFonts.roboto( - textStyle: TextStyle( - fontWeight: medium, - fontSize: fontSized.sp, - color: colorText, - ), + style: Tulisan.customText( + color: colorText, + fontsize: 14, ), ), GapCustom().gapValue(10, false), diff --git a/pubspec.lock b/pubspec.lock index 22321b2..ade22d1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -400,6 +400,14 @@ packages: description: flutter source: sdk version: "0.0.0" + fuzzywuzzy: + dependency: "direct main" + description: + name: fuzzywuzzy + sha256: "3004379ffd6e7f476a0c2091f38f16588dc45f67de7adf7c41aa85dec06b432c" + url: "https://pub.dev" + source: hosted + version: "1.2.0" gap: dependency: "direct main" description: @@ -744,6 +752,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + nik_validator: + dependency: "direct main" + description: + name: nik_validator + sha256: "7c24d2a396468177a689561fafd8bb59d3366e6795514a6db124441c0ad68107" + url: "https://pub.dev" + source: hosted + version: "1.1.2" nm: dependency: transitive description: @@ -1069,6 +1085,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.4" + timelines_plus: + dependency: "direct main" + description: + name: timelines_plus + sha256: "335cd832e0aed7035458a5f336bc2c882dc70dcfa4d500d3bcadd764f1ac6553" + url: "https://pub.dev" + source: hosted + version: "1.0.7" toastification: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 69d1abf..47f5364 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,6 +28,7 @@ dependencies: flutter_secure_storage: ^9.2.4 flutter_slidable: ^4.0.0 flutter_svg: ^2.1.0 + fuzzywuzzy: ^1.2.0 gap: ^3.0.1 geolocator: ^14.0.0 get_it: ^8.0.3 @@ -43,6 +44,7 @@ dependencies: jwt_decoder: ^2.0.1 latlong2: ^0.9.1 localstorage: ^6.0.0 + nik_validator: ^1.1.2 path_provider: ^2.1.5 permission_handler: ^12.0.0+1 pin_code_fields: ^8.0.1 @@ -50,6 +52,7 @@ dependencies: shared_preferences: ^2.3.3 shimmer: ^3.0.0 smooth_page_indicator: ^1.2.1 + timelines_plus: ^1.0.7 toastification: ^3.0.2 url_launcher: ^6.3.1 uuid: ^4.5.1