perbaikan
This commit is contained in:
parent
e736908004
commit
e526b8ff1a
|
|
@ -38,7 +38,6 @@ import 'screens/notification/notification_screen.dart';
|
||||||
import 'screens/profile/face_test_screen.dart';
|
import 'screens/profile/face_test_screen.dart';
|
||||||
import 'screens/profile/signature_screen.dart';
|
import 'screens/profile/signature_screen.dart';
|
||||||
import 'screens/profile/edit_profile_screen.dart';
|
import 'screens/profile/edit_profile_screen.dart';
|
||||||
import 'screens/documents/surat_izin_screen.dart';
|
|
||||||
import 'screens/onboarding/onboarding_check_screen.dart';
|
import 'screens/onboarding/onboarding_check_screen.dart';
|
||||||
|
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
|
|
@ -49,6 +48,8 @@ import 'package:intl/date_symbol_data_local.dart';
|
||||||
import 'package:timezone/data/latest_all.dart' as tz;
|
import 'package:timezone/data/latest_all.dart' as tz;
|
||||||
import 'services/reminder_service.dart';
|
import 'services/reminder_service.dart';
|
||||||
|
|
||||||
|
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await dotenv.load(fileName: ".env");
|
await dotenv.load(fileName: ".env");
|
||||||
|
|
@ -85,6 +86,7 @@ class MyApp extends StatelessWidget {
|
||||||
ChangeNotifierProvider(create: (_) => SetupCheckProvider()),
|
ChangeNotifierProvider(create: (_) => SetupCheckProvider()),
|
||||||
],
|
],
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
|
navigatorKey: navigatorKey,
|
||||||
title: 'MPG HRIS',
|
title: 'MPG HRIS',
|
||||||
scaffoldMessengerKey: ErrorHandler.scaffoldMessengerKey,
|
scaffoldMessengerKey: ErrorHandler.scaffoldMessengerKey,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
|
|
@ -117,7 +119,6 @@ class MyApp extends StatelessWidget {
|
||||||
'/profile/face-test': (context) => const FaceTestScreen(),
|
'/profile/face-test': (context) => const FaceTestScreen(),
|
||||||
'/profile/signature': (context) => const SignatureScreen(),
|
'/profile/signature': (context) => const SignatureScreen(),
|
||||||
'/profile/edit': (context) => const EditProfileScreen(),
|
'/profile/edit': (context) => const EditProfileScreen(),
|
||||||
'/surat-izin': (context) => const SuratIzinScreen(),
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,30 @@
|
||||||
class AnnouncementModel {
|
class AnnouncementModel {
|
||||||
|
final int id;
|
||||||
final String title;
|
final String title;
|
||||||
final String description;
|
final String description;
|
||||||
final String date;
|
final String date;
|
||||||
final String jabatan;
|
final String jabatan;
|
||||||
|
final String namaPembuat;
|
||||||
final String? avatarUrl;
|
final String? avatarUrl;
|
||||||
|
|
||||||
AnnouncementModel({
|
AnnouncementModel({
|
||||||
|
required this.id,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.description,
|
required this.description,
|
||||||
required this.date,
|
required this.date,
|
||||||
required this.jabatan,
|
required this.jabatan,
|
||||||
|
this.namaPembuat = 'Admin',
|
||||||
this.avatarUrl,
|
this.avatarUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory AnnouncementModel.fromJson(Map<String, dynamic> json) {
|
factory AnnouncementModel.fromJson(Map<String, dynamic> json) {
|
||||||
return AnnouncementModel(
|
return AnnouncementModel(
|
||||||
|
id: json['id'] ?? 0,
|
||||||
title: json['title']?.toString() ?? '',
|
title: json['title']?.toString() ?? '',
|
||||||
description: json['description']?.toString() ?? '',
|
description: json['description']?.toString() ?? '',
|
||||||
date: json['tanggal']?.toString() ?? '',
|
date: json['tanggal']?.toString() ?? '',
|
||||||
jabatan: json['jabatan']?.toString() ?? '',
|
jabatan: json['jabatan']?.toString() ?? '',
|
||||||
|
namaPembuat: json['nama_pembuat']?.toString() ?? 'Admin',
|
||||||
avatarUrl: json['avatar_url']?.toString(),
|
avatarUrl: json['avatar_url']?.toString(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,12 @@ class PengajuanModel {
|
||||||
final String? approvedAt;
|
final String? approvedAt;
|
||||||
final String? jamMulai;
|
final String? jamMulai;
|
||||||
final String? jamSelesai;
|
final String? jamSelesai;
|
||||||
|
final String? lampiranUrl;
|
||||||
|
final bool hasSurat;
|
||||||
|
final String? idSurat;
|
||||||
|
final String? namaKompensasi;
|
||||||
|
final int? durasiMenit;
|
||||||
|
final int? jumlahPoin;
|
||||||
|
|
||||||
PengajuanModel({
|
PengajuanModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
|
@ -23,6 +29,12 @@ class PengajuanModel {
|
||||||
this.approvedAt,
|
this.approvedAt,
|
||||||
this.jamMulai,
|
this.jamMulai,
|
||||||
this.jamSelesai,
|
this.jamSelesai,
|
||||||
|
this.lampiranUrl,
|
||||||
|
this.hasSurat = false,
|
||||||
|
this.idSurat,
|
||||||
|
this.namaKompensasi,
|
||||||
|
this.durasiMenit,
|
||||||
|
this.jumlahPoin,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory PengajuanModel.fromJson(Map<String, dynamic> json) {
|
factory PengajuanModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
|
@ -40,6 +52,9 @@ class PengajuanModel {
|
||||||
keterangan: json['alasan'] ?? '',
|
keterangan: json['alasan'] ?? '',
|
||||||
status: _mapStatus(json['id_status']),
|
status: _mapStatus(json['id_status']),
|
||||||
approvedAt: json['approved_at'],
|
approvedAt: json['approved_at'],
|
||||||
|
lampiranUrl: json['bukti_file_url'],
|
||||||
|
hasSurat: json['has_surat'] == true,
|
||||||
|
idSurat: json['id_surat']?.toString(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,6 +73,15 @@ class PengajuanModel {
|
||||||
approvedAt: json['approved_at'],
|
approvedAt: json['approved_at'],
|
||||||
jamMulai: json['jam_mulai'],
|
jamMulai: json['jam_mulai'],
|
||||||
jamSelesai: json['jam_selesai'],
|
jamSelesai: json['jam_selesai'],
|
||||||
|
namaKompensasi: json['kompensasi'] != null
|
||||||
|
? json['kompensasi']['nama_kompensasi']
|
||||||
|
: null,
|
||||||
|
durasiMenit: json['durasi_menit'] != null
|
||||||
|
? int.tryParse(json['durasi_menit'].toString())
|
||||||
|
: null,
|
||||||
|
jumlahPoin: json['jumlah_poin'] != null
|
||||||
|
? int.tryParse(json['jumlah_poin'].toString())
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,9 @@ class AttendanceProvider extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
Position position = await Geolocator.getCurrentPosition(
|
Position position = await Geolocator.getCurrentPosition(
|
||||||
desiredAccuracy: LocationAccuracy.bestForNavigation,
|
locationSettings: const LocationSettings(
|
||||||
|
accuracy: LocationAccuracy.bestForNavigation,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await updateLocation(position.latitude, position.longitude);
|
await updateLocation(position.latitude, position.longitude);
|
||||||
|
|
@ -94,7 +96,10 @@ class AttendanceProvider extends ChangeNotifier {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
try {
|
try {
|
||||||
historyList = await _repository.getHistory();
|
historyList = await _repository.getHistory(
|
||||||
|
month: selectedMonth,
|
||||||
|
year: selectedYear,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error fetching history: $e');
|
print('Error fetching history: $e');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -103,6 +108,18 @@ class AttendanceProvider extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setMonth(int month) {
|
||||||
|
selectedMonth = month;
|
||||||
|
notifyListeners();
|
||||||
|
fetchHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setYear(int year) {
|
||||||
|
selectedYear = year;
|
||||||
|
notifyListeners();
|
||||||
|
fetchHistory();
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> submitPresensi(String type, File photoFile) async {
|
Future<bool> submitPresensi(String type, File photoFile) async {
|
||||||
if (currentLocation == null) {
|
if (currentLocation == null) {
|
||||||
print('Location not available');
|
print('Location not available');
|
||||||
|
|
@ -137,11 +154,6 @@ class AttendanceProvider extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setMonth(int month) {
|
|
||||||
selectedMonth = month;
|
|
||||||
fetchHistory();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> resubmitPresensi(int idPresensi, String keterangan) async {
|
Future<bool> resubmitPresensi(int idPresensi, String keterangan) async {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
|
|
|
||||||
|
|
@ -84,9 +84,12 @@ class AttendanceRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<PresensiHistoryModel>> getHistory() async {
|
Future<List<PresensiHistoryModel>> getHistory({required int month, required int year}) async {
|
||||||
try {
|
try {
|
||||||
final response = await _client.dio.get('${ApiUrl.baseUrl}/presensi/history');
|
final response = await _client.dio.get(
|
||||||
|
'${ApiUrl.baseUrl}/presensi/history',
|
||||||
|
queryParameters: {'month': month, 'year': year},
|
||||||
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200 && response.data['success'] == true) {
|
if (response.statusCode == 200 && response.data['success'] == true) {
|
||||||
final List data = response.data['data']['data'] ?? [];
|
final List data = response.data['data']['data'] ?? [];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,232 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import '../../core/theme.dart';
|
||||||
|
import '../../models/announcement_model.dart';
|
||||||
|
import '../../widgets/atoms/custom_avatar.dart';
|
||||||
|
import '../../widgets/atoms/fade_in_up.dart';
|
||||||
|
|
||||||
|
class PengumumanDetailScreen extends StatelessWidget {
|
||||||
|
final AnnouncementModel pengumuman;
|
||||||
|
|
||||||
|
const PengumumanDetailScreen({super.key, required this.pengumuman});
|
||||||
|
|
||||||
|
String _formatTanggal(String date) {
|
||||||
|
try {
|
||||||
|
final dt = DateTime.parse(date);
|
||||||
|
return DateFormat('EEEE, d MMMM yyyy', 'id_ID').format(dt);
|
||||||
|
} catch (_) {
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: AppTheme.bgLight,
|
||||||
|
body: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverAppBar(
|
||||||
|
expandedHeight: 200,
|
||||||
|
pinned: true,
|
||||||
|
backgroundColor: AppTheme.primaryBlue,
|
||||||
|
leading: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withValues(alpha: 0.2),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white, size: 18),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
|
background: Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
Color(0xFF1E3A8A),
|
||||||
|
Color(0xFF3B82F6),
|
||||||
|
Color(0xFF6366F1),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 56, 20, 20),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withValues(alpha: 0.2),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.campaign_rounded, color: Colors.white, size: 14),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'Pengumuman',
|
||||||
|
style: AppTheme.bodySmall.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
pengumuman.title,
|
||||||
|
style: AppTheme.heading2.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 20,
|
||||||
|
height: 1.3,
|
||||||
|
),
|
||||||
|
maxLines: 3,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
collapseMode: CollapseMode.parallax,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(AppTheme.spacingMd),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
FadeInUp(
|
||||||
|
delayMs: 0,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
||||||
|
boxShadow: AppTheme.shadowSm,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
CustomAvatar(
|
||||||
|
name: pengumuman.namaPembuat,
|
||||||
|
imageUrl: pengumuman.avatarUrl,
|
||||||
|
size: 44,
|
||||||
|
backgroundColor: AppTheme.primaryBlue.withValues(alpha: 0.1),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
pengumuman.namaPembuat,
|
||||||
|
style: AppTheme.labelLarge.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
pengumuman.jabatan,
|
||||||
|
style: AppTheme.bodySmall.copyWith(
|
||||||
|
color: AppTheme.textTertiary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.primaryBlue.withValues(alpha: 0.08),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.calendar_today_rounded, size: 12, color: AppTheme.primaryBlue),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
_formatTanggal(pengumuman.date),
|
||||||
|
style: AppTheme.bodySmall.copyWith(
|
||||||
|
color: AppTheme.primaryBlue,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
FadeInUp(
|
||||||
|
delayMs: 80,
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
||||||
|
boxShadow: AppTheme.shadowSm,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 4,
|
||||||
|
height: 20,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.primaryBlue,
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Isi Pengumuman',
|
||||||
|
style: AppTheme.heading3.copyWith(fontSize: 14),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
pengumuman.description,
|
||||||
|
style: AppTheme.bodyLarge.copyWith(
|
||||||
|
color: AppTheme.textSecondary,
|
||||||
|
height: 1.7,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 80),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import '../../core/theme.dart';
|
||||||
import '../../providers/notification_provider.dart';
|
import '../../providers/notification_provider.dart';
|
||||||
import '../../models/notifikasi_model.dart';
|
import '../../models/notifikasi_model.dart';
|
||||||
import '../../widgets/atoms/fade_in_up.dart';
|
import '../../widgets/atoms/fade_in_up.dart';
|
||||||
|
import 'notifikasi_detail_screen.dart';
|
||||||
|
|
||||||
class NotificationScreen extends StatefulWidget {
|
class NotificationScreen extends StatefulWidget {
|
||||||
const NotificationScreen({super.key});
|
const NotificationScreen({super.key});
|
||||||
|
|
@ -81,6 +82,18 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _openDetail(NotifikasiModel item, NotificationProvider provider) {
|
||||||
|
if (!item.isRead) {
|
||||||
|
provider.markAsRead(item.id);
|
||||||
|
}
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => NotifikasiDetailScreen(notifikasi: item),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PopScope(
|
return PopScope(
|
||||||
|
|
@ -214,11 +227,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(bottom: AppTheme.spacingSm),
|
padding: const EdgeInsets.only(bottom: AppTheme.spacingSm),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () {
|
onTap: () => _openDetail(item, provider),
|
||||||
if (!item.isRead) {
|
|
||||||
provider.markAsRead(item.id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(14),
|
padding: const EdgeInsets.all(14),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -277,12 +286,23 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 5),
|
const SizedBox(height: 5),
|
||||||
Text(
|
Row(
|
||||||
_relativeTime(item.createdAt),
|
children: [
|
||||||
style: AppTheme.bodySmall.copyWith(
|
Expanded(
|
||||||
color: AppTheme.textTertiary,
|
child: Text(
|
||||||
fontSize: 11,
|
_relativeTime(item.createdAt),
|
||||||
),
|
style: AppTheme.bodySmall.copyWith(
|
||||||
|
color: AppTheme.textTertiary,
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Icon(
|
||||||
|
Icons.chevron_right_rounded,
|
||||||
|
size: 16,
|
||||||
|
color: AppTheme.textTertiary.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -305,3 +325,5 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,397 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../core/theme.dart';
|
||||||
|
import '../../models/notifikasi_model.dart';
|
||||||
|
import '../../widgets/atoms/fade_in_up.dart';
|
||||||
|
|
||||||
|
class NotifikasiDetailScreen extends StatelessWidget {
|
||||||
|
final NotifikasiModel notifikasi;
|
||||||
|
|
||||||
|
const NotifikasiDetailScreen({super.key, required this.notifikasi});
|
||||||
|
|
||||||
|
IconData _iconForTipe(String tipe) {
|
||||||
|
if (tipe.contains('disetujui')) return Icons.check_circle_rounded;
|
||||||
|
if (tipe.contains('ditolak')) return Icons.cancel_rounded;
|
||||||
|
if (tipe.contains('proses')) return Icons.hourglass_top_rounded;
|
||||||
|
if (tipe.contains('pengumuman')) return Icons.campaign_rounded;
|
||||||
|
if (tipe.contains('lembur')) return Icons.schedule_rounded;
|
||||||
|
if (tipe.contains('presensi')) return Icons.fingerprint_rounded;
|
||||||
|
if (tipe.contains('izin')) return Icons.description_rounded;
|
||||||
|
if (tipe.contains('cuti')) return Icons.beach_access_rounded;
|
||||||
|
if (tipe.contains('poin')) return Icons.stars_rounded;
|
||||||
|
return Icons.notifications_outlined;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _colorForTipe(String tipe) {
|
||||||
|
if (tipe.contains('disetujui')) return const Color(0xFF10b981);
|
||||||
|
if (tipe.contains('ditolak')) return const Color(0xFFef4444);
|
||||||
|
if (tipe.contains('pengumuman')) return AppTheme.primaryBlue;
|
||||||
|
if (tipe.contains('lembur')) return const Color(0xFFf59e0b);
|
||||||
|
if (tipe.contains('poin')) return const Color(0xFFe11d48);
|
||||||
|
if (tipe.contains('izin')) return const Color(0xFF8b5cf6);
|
||||||
|
if (tipe.contains('cuti')) return const Color(0xFF06b6d4);
|
||||||
|
if (tipe.contains('presensi')) return const Color(0xFF06b6d4);
|
||||||
|
return const Color(0xFF6366f1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _labelForTipe(String tipe) {
|
||||||
|
if (tipe.contains('disetujui')) return 'Disetujui';
|
||||||
|
if (tipe.contains('ditolak')) return 'Ditolak';
|
||||||
|
if (tipe.contains('proses')) return 'Dalam Proses';
|
||||||
|
if (tipe.contains('pengumuman')) return 'Pengumuman';
|
||||||
|
if (tipe.contains('lembur')) return 'Lembur';
|
||||||
|
if (tipe.contains('presensi')) return 'Presensi';
|
||||||
|
if (tipe.contains('izin')) return 'Izin';
|
||||||
|
if (tipe.contains('cuti')) return 'Cuti';
|
||||||
|
if (tipe.contains('poin')) return 'Poin';
|
||||||
|
return 'Notifikasi';
|
||||||
|
}
|
||||||
|
|
||||||
|
String _relativeTime(String createdAt) {
|
||||||
|
try {
|
||||||
|
final dt = DateTime.parse(createdAt).toLocal();
|
||||||
|
final now = DateTime.now();
|
||||||
|
final diff = now.difference(dt);
|
||||||
|
|
||||||
|
if (diff.inSeconds < 60) return 'Baru saja';
|
||||||
|
if (diff.inMinutes < 60) return '${diff.inMinutes} menit lalu';
|
||||||
|
if (diff.inHours < 24) return '${diff.inHours} jam lalu';
|
||||||
|
if (diff.inDays == 1) return 'Kemarin';
|
||||||
|
if (diff.inDays < 7) return '${diff.inDays} hari lalu';
|
||||||
|
if (diff.inDays < 30) return '${(diff.inDays / 7).floor()} minggu lalu';
|
||||||
|
|
||||||
|
return '${dt.day.toString().padLeft(2, '0')}/'
|
||||||
|
'${dt.month.toString().padLeft(2, '0')}/'
|
||||||
|
'${dt.year}';
|
||||||
|
} catch (_) {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _fullDateTime(String createdAt) {
|
||||||
|
try {
|
||||||
|
final dt = DateTime.parse(createdAt).toLocal();
|
||||||
|
final hari = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Minggu'];
|
||||||
|
final bulan = [
|
||||||
|
'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni',
|
||||||
|
'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'
|
||||||
|
];
|
||||||
|
return '${hari[dt.weekday - 1]}, ${dt.day} ${bulan[dt.month - 1]} ${dt.year} • '
|
||||||
|
'${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
|
||||||
|
} catch (_) {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _actionRoute(String tipe) {
|
||||||
|
if (tipe.contains('pengumuman')) return null;
|
||||||
|
if (tipe.contains('izin') || tipe.contains('cuti') || tipe.contains('lembur') || tipe.contains('sakit')) {
|
||||||
|
return '/pengajuan';
|
||||||
|
}
|
||||||
|
if (tipe.contains('presensi')) return '/presensi';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _actionLabel(String tipe) {
|
||||||
|
if (tipe.contains('izin') || tipe.contains('cuti') || tipe.contains('lembur') || tipe.contains('sakit')) {
|
||||||
|
return 'Lihat Pengajuan';
|
||||||
|
}
|
||||||
|
if (tipe.contains('presensi')) return 'Lihat Riwayat Presensi';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final color = _colorForTipe(notifikasi.tipe);
|
||||||
|
final icon = _iconForTipe(notifikasi.tipe);
|
||||||
|
final label = _labelForTipe(notifikasi.tipe);
|
||||||
|
final actionRoute = _actionRoute(notifikasi.tipe);
|
||||||
|
final actionLabel = _actionLabel(notifikasi.tipe);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: AppTheme.bgLight,
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text("Detail Notifikasi", style: AppTheme.heading3),
|
||||||
|
backgroundColor: AppTheme.bgLight,
|
||||||
|
elevation: 0,
|
||||||
|
centerTitle: true,
|
||||||
|
leading: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.bgInput,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back_ios_new, color: AppTheme.textPrimary, size: 18),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(AppTheme.spacingMd),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
FadeInUp(
|
||||||
|
delayMs: 0,
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusXl),
|
||||||
|
boxShadow: AppTheme.shadowMd,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
color.withValues(alpha: 0.15),
|
||||||
|
color.withValues(alpha: 0.05),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: color, size: 32),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: AppTheme.bodySmall.copyWith(
|
||||||
|
color: color,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
notifikasi.judul,
|
||||||
|
style: AppTheme.heading2.copyWith(fontSize: 18),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.access_time_rounded, size: 13, color: AppTheme.textTertiary),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
_relativeTime(notifikasi.createdAt),
|
||||||
|
style: AppTheme.bodySmall.copyWith(
|
||||||
|
color: AppTheme.textTertiary,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
FadeInUp(
|
||||||
|
delayMs: 60,
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
||||||
|
boxShadow: AppTheme.shadowSm,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 4,
|
||||||
|
height: 20,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color,
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Detail Pesan',
|
||||||
|
style: AppTheme.heading3.copyWith(fontSize: 14),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
notifikasi.pesan,
|
||||||
|
style: AppTheme.bodyLarge.copyWith(
|
||||||
|
color: AppTheme.textSecondary,
|
||||||
|
height: 1.7,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.bgInput,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.schedule_rounded, size: 14, color: AppTheme.textTertiary),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_fullDateTime(notifikasi.createdAt),
|
||||||
|
style: AppTheme.bodySmall.copyWith(
|
||||||
|
color: AppTheme.textSecondary,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (notifikasi.data != null && notifikasi.data!.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
FadeInUp(
|
||||||
|
delayMs: 120,
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
||||||
|
boxShadow: AppTheme.shadowSm,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 4,
|
||||||
|
height: 20,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.textTertiary,
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Informasi Tambahan',
|
||||||
|
style: AppTheme.heading3.copyWith(fontSize: 14),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
...notifikasi.data!.entries
|
||||||
|
.where((e) => e.key != 'tipe' && e.value != null && e.value.toString().isNotEmpty)
|
||||||
|
.map((e) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 120,
|
||||||
|
child: Text(
|
||||||
|
_formatDataKey(e.key),
|
||||||
|
style: AppTheme.bodySmall.copyWith(
|
||||||
|
color: AppTheme.textTertiary,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
e.value.toString(),
|
||||||
|
style: AppTheme.bodyMedium.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
if (actionRoute != null && actionLabel != null) ...[
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
FadeInUp(
|
||||||
|
delayMs: 160,
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
Navigator.pushNamed(context, actionRoute);
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
notifikasi.tipe.contains('presensi')
|
||||||
|
? Icons.fingerprint_rounded
|
||||||
|
: Icons.description_rounded,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
label: Text(actionLabel),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: color,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
textStyle: AppTheme.labelLarge.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatDataKey(String key) {
|
||||||
|
return key
|
||||||
|
.replaceAll('_', ' ')
|
||||||
|
.split(' ')
|
||||||
|
.map((w) => w.isNotEmpty ? '${w[0].toUpperCase()}${w.substring(1)}' : '')
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -44,6 +44,12 @@ class _CutiFormScreenState extends State<CutiFormScreen> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final jumlahHari = _endDate!.difference(_startDate!).inDays + 1;
|
||||||
|
if (jumlahHari > 3) {
|
||||||
|
ErrorHandler.showWarning('Pengajuan cuti maksimal 3 hari berturut-turut');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final success = await context.read<PengajuanProvider>().submitCuti({
|
final success = await context.read<PengajuanProvider>().submitCuti({
|
||||||
'tanggal_mulai': DateFormat('yyyy-MM-dd').format(_startDate!),
|
'tanggal_mulai': DateFormat('yyyy-MM-dd').format(_startDate!),
|
||||||
'tanggal_selesai': DateFormat('yyyy-MM-dd').format(_endDate!),
|
'tanggal_selesai': DateFormat('yyyy-MM-dd').format(_endDate!),
|
||||||
|
|
@ -79,12 +85,13 @@ class _CutiFormScreenState extends State<CutiFormScreen> {
|
||||||
|
|
||||||
Future<void> _selectEndDate(DateTime minDate) async {
|
Future<void> _selectEndDate(DateTime minDate) async {
|
||||||
final DateTime initialDate = _startDate ?? minDate;
|
final DateTime initialDate = _startDate ?? minDate;
|
||||||
|
final DateTime maxEndDate = initialDate.add(const Duration(days: 2));
|
||||||
final DateTime? picked = await showDatePicker(
|
final DateTime? picked = await showDatePicker(
|
||||||
context: context,
|
context: context,
|
||||||
initialDate: _endDate ?? initialDate,
|
initialDate: _endDate != null && _endDate!.isAfter(maxEndDate) ? initialDate : (_endDate ?? initialDate),
|
||||||
firstDate: initialDate,
|
firstDate: initialDate,
|
||||||
lastDate: DateTime(2100),
|
lastDate: maxEndDate,
|
||||||
helpText: "Pilih Tanggal Selesai",
|
helpText: "Pilih Tanggal Selesai (maks 3 hari)",
|
||||||
);
|
);
|
||||||
if (picked != null) {
|
if (picked != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -149,7 +156,7 @@ class _CutiFormScreenState extends State<CutiFormScreen> {
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
"Cuti harus diajukan minimal 7 hari sebelum tanggal mulai",
|
"Cuti harus diajukan minimal H-7 dan maksimal 3 hari berturut-turut",
|
||||||
style: AppTheme.bodySmall.copyWith(
|
style: AppTheme.bodySmall.copyWith(
|
||||||
color: AppTheme.primaryBlue,
|
color: AppTheme.primaryBlue,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import '../../../core/theme.dart';
|
||||||
import '../../../widgets/atoms/custom_button.dart';
|
import '../../../widgets/atoms/custom_button.dart';
|
||||||
import '../../../widgets/atoms/custom_text_field.dart';
|
import '../../../widgets/atoms/custom_text_field.dart';
|
||||||
import '../../../providers/pengajuan_provider.dart';
|
import '../../../providers/pengajuan_provider.dart';
|
||||||
import '../../../providers/home_provider.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
|
@ -110,7 +110,7 @@ class _IzinFormScreenState extends State<IzinFormScreen> {
|
||||||
final pengajuanProvider = context.watch<PengajuanProvider>();
|
final pengajuanProvider = context.watch<PengajuanProvider>();
|
||||||
final hasSignature = pengajuanProvider.hasSignature;
|
final hasSignature = pengajuanProvider.hasSignature;
|
||||||
final isLoading = pengajuanProvider.isLoading;
|
final isLoading = pengajuanProvider.isLoading;
|
||||||
final sisaCuti = context.watch<HomeProvider>().sisaCuti;
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: AppTheme.bgLight,
|
backgroundColor: AppTheme.bgLight,
|
||||||
|
|
@ -148,50 +148,6 @@ class _IzinFormScreenState extends State<IzinFormScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Banner sisa cuti
|
|
||||||
Container(
|
|
||||||
margin: const EdgeInsets.only(bottom: 16),
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: sisaCuti > 0
|
|
||||||
? AppTheme.statusGreen.withValues(alpha: 0.08)
|
|
||||||
: AppTheme.statusRed.withValues(alpha: 0.08),
|
|
||||||
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
|
||||||
border: Border.all(
|
|
||||||
color: sisaCuti > 0
|
|
||||||
? AppTheme.statusGreen.withValues(alpha: 0.3)
|
|
||||||
: AppTheme.statusRed.withValues(alpha: 0.3),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
sisaCuti > 0 ? Icons.event_available : Icons.event_busy,
|
|
||||||
color: sisaCuti > 0 ? AppTheme.statusGreen : AppTheme.statusRed,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Expanded(
|
|
||||||
child: RichText(
|
|
||||||
text: TextSpan(
|
|
||||||
style: AppTheme.bodySmall.copyWith(
|
|
||||||
color: sisaCuti > 0 ? AppTheme.statusGreen : AppTheme.statusRed,
|
|
||||||
),
|
|
||||||
children: [
|
|
||||||
const TextSpan(text: 'Sisa cuti Anda: '),
|
|
||||||
TextSpan(
|
|
||||||
text: '$sisaCuti hari',
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
if (sisaCuti <= 0)
|
|
||||||
const TextSpan(text: '. Kuota cuti Anda sudah habis.'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
CustomTextField(
|
CustomTextField(
|
||||||
label: "Tanggal Izin",
|
label: "Tanggal Izin",
|
||||||
|
|
|
||||||
|
|
@ -317,7 +317,12 @@ class _PointUsageScreenState extends State<PointUsageScreen> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: SingleChildScrollView(
|
body: RefreshIndicator(
|
||||||
|
onRefresh: () async {
|
||||||
|
await context.read<PoinProvider>().loadExpiringPoints();
|
||||||
|
},
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
padding: const EdgeInsets.all(AppTheme.spacingMd),
|
padding: const EdgeInsets.all(AppTheme.spacingMd),
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
|
|
@ -568,6 +573,7 @@ class _PointUsageScreenState extends State<PointUsageScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,38 @@ class _PresensiScreenState extends State<PresensiScreen> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const List<String> _monthNames = [
|
||||||
|
'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni',
|
||||||
|
'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember',
|
||||||
|
];
|
||||||
|
|
||||||
|
String _getMonthName(int month) => _monthNames[month - 1];
|
||||||
|
|
||||||
|
void _goToPrevMonth(AttendanceProvider p) {
|
||||||
|
if (p.selectedMonth == 1) {
|
||||||
|
p.setYear(p.selectedYear - 1);
|
||||||
|
p.setMonth(12);
|
||||||
|
} else {
|
||||||
|
p.setMonth(p.selectedMonth - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _goToNextMonth(AttendanceProvider p) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
if (p.selectedYear == now.year && p.selectedMonth == now.month) return;
|
||||||
|
if (p.selectedMonth == 12) {
|
||||||
|
p.setYear(p.selectedYear + 1);
|
||||||
|
p.setMonth(1);
|
||||||
|
} else {
|
||||||
|
p.setMonth(p.selectedMonth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isCurrentMonth(AttendanceProvider p) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
return p.selectedYear == now.year && p.selectedMonth == now.month;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final homeProvider = context.watch<HomeProvider>();
|
final homeProvider = context.watch<HomeProvider>();
|
||||||
|
|
@ -88,207 +120,195 @@ class _PresensiScreenState extends State<PresensiScreen> {
|
||||||
const SizedBox(height: AppTheme.spacingXl),
|
const SizedBox(height: AppTheme.spacingXl),
|
||||||
|
|
||||||
Text("History Presensi", style: AppTheme.heading3),
|
Text("History Presensi", style: AppTheme.heading3),
|
||||||
const SizedBox(height: AppTheme.spacingMd),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
attendanceProvider.historyList.isEmpty
|
// Month/Year Picker
|
||||||
? Center(
|
Container(
|
||||||
child: Container(
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
padding: const EdgeInsets.all(AppTheme.spacingXl),
|
decoration: BoxDecoration(
|
||||||
child: Column(
|
color: AppTheme.bgWhite,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
||||||
|
boxShadow: AppTheme.shadowSm,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
InkWell(
|
||||||
|
onTap: () => _goToPrevMonth(attendanceProvider),
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusFull),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.primaryDark.withValues(alpha: 0.08),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.chevron_left, size: 20, color: AppTheme.primaryDark),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => _showMonthYearPicker(context, attendanceProvider),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.calendar_today, size: 48, color: AppTheme.textTertiary),
|
const Icon(Icons.calendar_month, size: 18, color: AppTheme.primaryDark),
|
||||||
const SizedBox(height: AppTheme.spacingMd),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
"Tidak ada data presensi",
|
"${_getMonthName(attendanceProvider.selectedMonth)} ${attendanceProvider.selectedYear}",
|
||||||
style: AppTheme.bodyMedium.copyWith(color: AppTheme.textSecondary),
|
style: AppTheme.labelLarge.copyWith(fontSize: 16, color: AppTheme.primaryDark),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
InkWell(
|
||||||
: ListView.builder(
|
onTap: _isCurrentMonth(attendanceProvider) ? null : () => _goToNextMonth(attendanceProvider),
|
||||||
shrinkWrap: true,
|
borderRadius: BorderRadius.circular(AppTheme.radiusFull),
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
child: Container(
|
||||||
itemCount: attendanceProvider.historyList.length,
|
padding: const EdgeInsets.all(8),
|
||||||
itemBuilder: (context, index) {
|
decoration: BoxDecoration(
|
||||||
final history = attendanceProvider.historyList[index];
|
color: _isCurrentMonth(attendanceProvider)
|
||||||
return FadeInUp(
|
? AppTheme.textTertiary.withValues(alpha: 0.1)
|
||||||
delayMs: index * 50,
|
: AppTheme.primaryDark.withValues(alpha: 0.08),
|
||||||
child: InkWell(
|
shape: BoxShape.circle,
|
||||||
onTap: () => _showPresensiDetail(context, history),
|
),
|
||||||
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
child: Icon(
|
||||||
child: Container(
|
Icons.chevron_right,
|
||||||
margin: const EdgeInsets.only(bottom: AppTheme.spacingMd),
|
size: 20,
|
||||||
padding: const EdgeInsets.all(AppTheme.spacingMd),
|
color: _isCurrentMonth(attendanceProvider)
|
||||||
decoration: BoxDecoration(
|
? AppTheme.textTertiary
|
||||||
color: AppTheme.bgWhite,
|
: AppTheme.primaryDark,
|
||||||
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
),
|
||||||
boxShadow: AppTheme.shadowSm,
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
],
|
||||||
children: [
|
),
|
||||||
Column(
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
const SizedBox(height: AppTheme.spacingMd),
|
||||||
children: [
|
|
||||||
Row(
|
// History List
|
||||||
children: [
|
if (attendanceProvider.isLoading)
|
||||||
const Icon(Icons.calendar_today, size: 16, color: AppTheme.primaryDark),
|
const Center(
|
||||||
const SizedBox(width: 8),
|
child: Padding(
|
||||||
Expanded(
|
padding: EdgeInsets.all(32),
|
||||||
child: Text(
|
child: CircularProgressIndicator(),
|
||||||
history.tanggal,
|
),
|
||||||
style: AppTheme.labelMedium,
|
)
|
||||||
overflow: TextOverflow.ellipsis,
|
else if (attendanceProvider.historyList.isEmpty)
|
||||||
),
|
Center(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(AppTheme.spacingXl),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.calendar_today, size: 48, color: AppTheme.textTertiary),
|
||||||
|
const SizedBox(height: AppTheme.spacingMd),
|
||||||
|
Text(
|
||||||
|
"Tidak ada data presensi bulan ini",
|
||||||
|
style: AppTheme.bodyMedium.copyWith(color: AppTheme.textSecondary),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: attendanceProvider.historyList.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final history = attendanceProvider.historyList[index];
|
||||||
|
return FadeInUp(
|
||||||
|
delayMs: index * 50,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => _showPresensiDetail(context, history),
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: AppTheme.spacingMd),
|
||||||
|
padding: const EdgeInsets.all(AppTheme.spacingMd),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.bgWhite,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
||||||
|
boxShadow: AppTheme.shadowSm,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.calendar_today, size: 16, color: AppTheme.primaryDark),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
history.tanggal,
|
||||||
|
style: AppTheme.labelMedium,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Wrap(
|
|
||||||
spacing: 6,
|
|
||||||
runSpacing: 6,
|
|
||||||
children: [
|
|
||||||
if (history.statusValidasi == 3)
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppTheme.statusRed.withValues(alpha: 0.1),
|
|
||||||
borderRadius: BorderRadius.circular(AppTheme.radiusFull),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
"Ditolak",
|
|
||||||
style: AppTheme.bodySmall.copyWith(
|
|
||||||
color: AppTheme.statusRed,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 10,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (history.statusValidasi == 2)
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppTheme.statusYellow.withValues(alpha: 0.15),
|
|
||||||
borderRadius: BorderRadius.circular(AppTheme.radiusFull),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
"Menunggu Validasi",
|
|
||||||
style: AppTheme.bodySmall.copyWith(
|
|
||||||
color: AppTheme.statusOrange,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 10,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (!history.verifikasiWajah && history.statusMasuk != 'Izin' && history.statusMasuk != 'Cuti' && history.statusMasuk != 'Sakit' && history.statusMasuk != 'Alpha')
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppTheme.statusOrange.withValues(alpha: 0.1),
|
|
||||||
borderRadius: BorderRadius.circular(AppTheme.radiusFull),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
"Wajah Tidak Terverifikasi",
|
|
||||||
style: AppTheme.bodySmall.copyWith(
|
|
||||||
color: AppTheme.statusOrange,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 10,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (history.keteranganLuarRadius != null)
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppTheme.primaryDark.withValues(alpha: 0.1),
|
|
||||||
borderRadius: BorderRadius.circular(AppTheme.radiusFull),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
"Luar Radius",
|
|
||||||
style: AppTheme.bodySmall.copyWith(
|
|
||||||
color: AppTheme.primaryDark,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 10,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (history.statusMasuk != null && history.statusMasuk != '-')
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: _getStatusColor(history.statusMasuk!).withValues(alpha: 0.1),
|
|
||||||
borderRadius: BorderRadius.circular(AppTheme.radiusFull),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
history.statusMasuk!,
|
|
||||||
style: AppTheme.bodySmall.copyWith(
|
|
||||||
color: _getStatusColor(history.statusMasuk!),
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 10,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (history.statusPulang != null &&
|
|
||||||
history.statusPulang != '-' &&
|
|
||||||
history.statusPulang != 'Tepat Waktu' &&
|
|
||||||
history.statusPulang != history.statusMasuk)
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: _getStatusColor(history.statusPulang!).withValues(alpha: 0.1),
|
|
||||||
borderRadius: BorderRadius.circular(AppTheme.radiusFull),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
history.statusPulang!,
|
|
||||||
style: AppTheme.bodySmall.copyWith(
|
|
||||||
color: _getStatusColor(history.statusPulang!),
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 10,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: AppTheme.spacingMd),
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: StatCard(
|
|
||||||
label: "Total Jam Kerja",
|
|
||||||
value: history.totalJam,
|
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Wrap(
|
||||||
|
spacing: 6,
|
||||||
|
runSpacing: 6,
|
||||||
|
children: [
|
||||||
|
if (history.statusValidasi == 3)
|
||||||
|
_buildBadge("Ditolak", AppTheme.statusRed),
|
||||||
|
if (history.statusValidasi == 2)
|
||||||
|
_buildBadge("Menunggu Validasi", AppTheme.statusOrange, bgColor: AppTheme.statusYellow),
|
||||||
|
if (!history.verifikasiWajah && history.statusMasuk != 'Izin' && history.statusMasuk != 'Cuti' && history.statusMasuk != 'Sakit' && history.statusMasuk != 'Alpha')
|
||||||
|
_buildBadge("Wajah Tidak Terverifikasi", AppTheme.statusOrange),
|
||||||
|
if (history.keteranganLuarRadius != null)
|
||||||
|
_buildBadge("Luar Radius", AppTheme.primaryDark),
|
||||||
|
if (history.statusMasuk != null && history.statusMasuk != '-')
|
||||||
|
_buildBadge(history.statusMasuk!, _getStatusColor(history.statusMasuk!)),
|
||||||
|
if (history.statusPulang != null &&
|
||||||
|
history.statusPulang != '-' &&
|
||||||
|
history.statusPulang != 'Tepat Waktu' &&
|
||||||
|
history.statusPulang != history.statusMasuk)
|
||||||
|
_buildBadge(history.statusPulang!, _getStatusColor(history.statusPulang!)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: AppTheme.spacingMd),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: StatCard(
|
||||||
|
label: "Total Jam Kerja",
|
||||||
|
value: history.totalJam,
|
||||||
),
|
),
|
||||||
const SizedBox(width: AppTheme.spacingMd),
|
),
|
||||||
Expanded(
|
const SizedBox(width: AppTheme.spacingMd),
|
||||||
child: AttendanceTimeCard(
|
Expanded(
|
||||||
jamMasuk: history.jamMasuk ?? '-',
|
child: AttendanceTimeCard(
|
||||||
jamPulang: history.jamPulang ?? '-',
|
jamMasuk: history.jamMasuk ?? '-',
|
||||||
),
|
jamPulang: history.jamPulang ?? '-',
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
const SizedBox(height: 8),
|
),
|
||||||
Row(
|
const SizedBox(height: 8),
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
Row(
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
Text(
|
children: [
|
||||||
"Lihat Detail",
|
Text(
|
||||||
style: AppTheme.bodySmall.copyWith(color: AppTheme.primaryBlue, fontWeight: FontWeight.w500),
|
"Lihat Detail",
|
||||||
),
|
style: AppTheme.bodySmall.copyWith(color: AppTheme.primaryBlue, fontWeight: FontWeight.w500),
|
||||||
const SizedBox(width: 4),
|
),
|
||||||
const Icon(Icons.chevron_right, size: 16, color: AppTheme.primaryBlue),
|
const SizedBox(width: 4),
|
||||||
],
|
const Icon(Icons.chevron_right, size: 16, color: AppTheme.primaryBlue),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
),
|
},
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -297,6 +317,152 @@ class _PresensiScreenState extends State<PresensiScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildBadge(String label, Color color, {Color? bgColor}) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: (bgColor ?? color).withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusFull),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: AppTheme.bodySmall.copyWith(
|
||||||
|
color: color,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showMonthYearPicker(BuildContext context, AttendanceProvider provider) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
int tempMonth = provider.selectedMonth;
|
||||||
|
int tempYear = provider.selectedYear;
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
builder: (ctx) {
|
||||||
|
return StatefulBuilder(
|
||||||
|
builder: (ctx, setModalState) {
|
||||||
|
return Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: AppTheme.bgWhite,
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(AppTheme.radiusXl)),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.fromLTRB(24, 16, 24, 32),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 40, height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.textTertiary.withValues(alpha: 0.3),
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text("Pilih Bulan", style: AppTheme.heading3),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Year selector row
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => setModalState(() => tempYear--),
|
||||||
|
icon: const Icon(Icons.chevron_left),
|
||||||
|
),
|
||||||
|
Text("$tempYear", style: AppTheme.labelLarge.copyWith(fontSize: 18)),
|
||||||
|
IconButton(
|
||||||
|
onPressed: tempYear >= now.year ? null : () => setModalState(() => tempYear++),
|
||||||
|
icon: const Icon(Icons.chevron_right),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Month grid
|
||||||
|
GridView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 3,
|
||||||
|
childAspectRatio: 2.5,
|
||||||
|
mainAxisSpacing: 8,
|
||||||
|
crossAxisSpacing: 8,
|
||||||
|
),
|
||||||
|
itemCount: 12,
|
||||||
|
itemBuilder: (ctx, idx) {
|
||||||
|
final m = idx + 1;
|
||||||
|
final isSelected = m == tempMonth && tempYear == provider.selectedYear;
|
||||||
|
final isFuture = tempYear == now.year && m > now.month;
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: isFuture ? null : () {
|
||||||
|
setModalState(() => tempMonth = m);
|
||||||
|
},
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected
|
||||||
|
? AppTheme.primaryDark
|
||||||
|
: isFuture
|
||||||
|
? AppTheme.textTertiary.withValues(alpha: 0.05)
|
||||||
|
: m == tempMonth
|
||||||
|
? AppTheme.primaryDark.withValues(alpha: 0.1)
|
||||||
|
: AppTheme.bgCard,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
_monthNames[idx].substring(0, 3),
|
||||||
|
style: AppTheme.labelMedium.copyWith(
|
||||||
|
color: isSelected
|
||||||
|
? Colors.white
|
||||||
|
: isFuture
|
||||||
|
? AppTheme.textTertiary
|
||||||
|
: m == tempMonth
|
||||||
|
? AppTheme.primaryDark
|
||||||
|
: AppTheme.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
provider.setYear(tempYear);
|
||||||
|
provider.setMonth(tempMonth);
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppTheme.primaryDark,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text("Terapkan", style: AppTheme.labelLarge.copyWith(color: Colors.white)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _showScheduleModal(BuildContext context) {
|
void _showScheduleModal(BuildContext context) {
|
||||||
Navigator.pushNamed(context, '/schedule');
|
Navigator.pushNamed(context, '/schedule');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -348,11 +348,6 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||||
label: "Tanda Tangan Digital",
|
label: "Tanda Tangan Digital",
|
||||||
onTap: () => Navigator.pushNamed(context, '/profile/signature'),
|
onTap: () => Navigator.pushNamed(context, '/profile/signature'),
|
||||||
),
|
),
|
||||||
_buildActionItem(
|
|
||||||
icon: Icons.description_outlined,
|
|
||||||
label: "Surat Izin",
|
|
||||||
onTap: () => Navigator.pushNamed(context, '/surat-izin'),
|
|
||||||
),
|
|
||||||
_buildActionItem(
|
_buildActionItem(
|
||||||
icon: Icons.settings_ethernet,
|
icon: Icons.settings_ethernet,
|
||||||
label: "Pengaturan Server",
|
label: "Pengaturan Server",
|
||||||
|
|
|
||||||
|
|
@ -57,19 +57,18 @@ class _SignatureScreenState extends State<SignatureScreen> {
|
||||||
|
|
||||||
Future<Uint8List?> _captureCanvas() async {
|
Future<Uint8List?> _captureCanvas() async {
|
||||||
try {
|
try {
|
||||||
// If we want PNG, hand_signature has a way to export to image
|
final byteData = await _signatureController.toImage(
|
||||||
final picture = _signatureController.toPicture(
|
|
||||||
width: 1080,
|
width: 1080,
|
||||||
height: 720,
|
height: 720,
|
||||||
color: const Color(0xFF1E293B),
|
color: const Color(0xFF1E293B),
|
||||||
strokeWidth: 4.5,
|
background: Colors.white,
|
||||||
|
fit: true,
|
||||||
|
border: 20,
|
||||||
|
format: ImageByteFormat.png,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (byteData == null) return null;
|
||||||
final image = await picture?.toImage(1080, 720);
|
return byteData.buffer.asUint8List();
|
||||||
if (image == null) return null;
|
|
||||||
final byteData = await image.toByteData(format: ImageByteFormat.png);
|
|
||||||
return byteData?.buffer.asUint8List();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('[Signature] Capture error: $e');
|
print('[Signature] Capture error: $e');
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:provider/provider.dart';
|
||||||
import '../core/theme.dart';
|
import '../core/theme.dart';
|
||||||
import '../providers/notification_provider.dart';
|
import '../providers/notification_provider.dart';
|
||||||
import '../repositories/notifikasi_repository.dart';
|
import '../repositories/notifikasi_repository.dart';
|
||||||
|
import '../main.dart';
|
||||||
|
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {}
|
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {}
|
||||||
|
|
@ -18,7 +19,12 @@ class FcmService {
|
||||||
final FlutterLocalNotificationsPlugin _localNotifications = FlutterLocalNotificationsPlugin();
|
final FlutterLocalNotificationsPlugin _localNotifications = FlutterLocalNotificationsPlugin();
|
||||||
final NotifikasiRepository _repository = NotifikasiRepository();
|
final NotifikasiRepository _repository = NotifikasiRepository();
|
||||||
|
|
||||||
|
bool _initialized = false;
|
||||||
|
|
||||||
Future<void> initialize(BuildContext context) async {
|
Future<void> initialize(BuildContext context) async {
|
||||||
|
if (_initialized) return;
|
||||||
|
_initialized = true;
|
||||||
|
|
||||||
final settings = await _messaging.requestPermission(
|
final settings = await _messaging.requestPermission(
|
||||||
alert: true,
|
alert: true,
|
||||||
badge: true,
|
badge: true,
|
||||||
|
|
@ -29,12 +35,7 @@ class FcmService {
|
||||||
const initSettings = InitializationSettings(android: androidSettings);
|
const initSettings = InitializationSettings(android: androidSettings);
|
||||||
await _localNotifications.initialize(
|
await _localNotifications.initialize(
|
||||||
settings: initSettings,
|
settings: initSettings,
|
||||||
onDidReceiveNotificationResponse: (details) {
|
onDidReceiveNotificationResponse: _onNotificationTap,
|
||||||
if (context.mounted) {
|
|
||||||
Navigator.of(context).pushNamedAndRemoveUntil('/home', (route) => false);
|
|
||||||
Navigator.of(context).pushNamed('/notification');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (settings.authorizationStatus == AuthorizationStatus.denied) return;
|
if (settings.authorizationStatus == AuthorizationStatus.denied) return;
|
||||||
|
|
@ -53,17 +54,29 @@ class FcmService {
|
||||||
});
|
});
|
||||||
|
|
||||||
FirebaseMessaging.onMessageOpenedApp.listen((msg) {
|
FirebaseMessaging.onMessageOpenedApp.listen((msg) {
|
||||||
if (context.mounted) _handleTap(context, msg);
|
_navigateToNotification();
|
||||||
});
|
});
|
||||||
|
|
||||||
final initial = await _messaging.getInitialMessage();
|
final initial = await _messaging.getInitialMessage();
|
||||||
if (initial != null) {
|
if (initial != null) {
|
||||||
Future.delayed(const Duration(seconds: 1), () {
|
Future.delayed(const Duration(seconds: 1), () {
|
||||||
if (context.mounted) _handleTap(context, initial);
|
_navigateToNotification();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onNotificationTap(NotificationResponse details) {
|
||||||
|
_navigateToNotification();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _navigateToNotification() {
|
||||||
|
final nav = navigatorKey.currentState;
|
||||||
|
if (nav != null) {
|
||||||
|
nav.pushNamedAndRemoveUntil('/home', (route) => false);
|
||||||
|
nav.pushNamed('/notification');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
IconData _iconForTipe(String? tipe) {
|
IconData _iconForTipe(String? tipe) {
|
||||||
if (tipe == null) return Icons.notifications_outlined;
|
if (tipe == null) return Icons.notifications_outlined;
|
||||||
if (tipe.contains('disetujui')) return Icons.check_circle_rounded;
|
if (tipe.contains('disetujui')) return Icons.check_circle_rounded;
|
||||||
|
|
@ -106,7 +119,7 @@ class FcmService {
|
||||||
color: color,
|
color: color,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
entry.remove();
|
entry.remove();
|
||||||
Navigator.of(context).pushNamed('/notification');
|
_navigateToNotification();
|
||||||
},
|
},
|
||||||
onDismiss: () => entry.remove(),
|
onDismiss: () => entry.remove(),
|
||||||
),
|
),
|
||||||
|
|
@ -137,13 +150,9 @@ class FcmService {
|
||||||
title: title,
|
title: title,
|
||||||
body: body,
|
body: body,
|
||||||
notificationDetails: details,
|
notificationDetails: details,
|
||||||
|
payload: 'notification',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleTap(BuildContext context, RemoteMessage message) {
|
|
||||||
Navigator.of(context).pushNamedAndRemoveUntil('/home', (route) => false);
|
|
||||||
Navigator.of(context).pushNamed('/notification');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NotificationBanner extends StatefulWidget {
|
class _NotificationBanner extends StatefulWidget {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../../core/theme.dart';
|
import '../../core/theme.dart';
|
||||||
import '../../models/announcement_model.dart';
|
import '../../models/announcement_model.dart';
|
||||||
|
import '../../screens/home/pengumuman_detail_screen.dart';
|
||||||
import '../atoms/custom_avatar.dart';
|
import '../atoms/custom_avatar.dart';
|
||||||
|
|
||||||
class PengumumanCard extends StatelessWidget {
|
class PengumumanCard extends StatelessWidget {
|
||||||
|
|
@ -27,7 +28,14 @@ class PengumumanCard extends StatelessWidget {
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTap ?? () {},
|
onTap: onTap ?? () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => PengumumanDetailScreen(pengumuman: pengumuman),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(AppTheme.spacingMd),
|
padding: const EdgeInsets.all(AppTheme.spacingMd),
|
||||||
|
|
@ -35,7 +43,7 @@ class PengumumanCard extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
CustomAvatar(
|
CustomAvatar(
|
||||||
name: "HR",
|
name: pengumuman.namaPembuat,
|
||||||
imageUrl: pengumuman.avatarUrl,
|
imageUrl: pengumuman.avatarUrl,
|
||||||
size: 40,
|
size: 40,
|
||||||
backgroundColor: AppTheme.primaryOrange.withOpacity(0.1),
|
backgroundColor: AppTheme.primaryOrange.withOpacity(0.1),
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||||
import '../../core/theme.dart';
|
import '../../core/theme.dart';
|
||||||
import '../../models/pengajuan_model.dart';
|
import '../../models/pengajuan_model.dart';
|
||||||
import '../atoms/badge_widget.dart';
|
import '../atoms/badge_widget.dart';
|
||||||
|
import 'surat_izin_detail_sheet.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
class PengajuanDetailSheet extends StatelessWidget {
|
class PengajuanDetailSheet extends StatelessWidget {
|
||||||
|
|
@ -81,13 +82,15 @@ class PengajuanDetailSheet extends StatelessWidget {
|
||||||
return ClipRRect(
|
return ClipRRect(
|
||||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(AppTheme.radiusXl)),
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(AppTheme.radiusXl)),
|
||||||
child: Container(
|
child: Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxHeight: MediaQuery.of(context).size.height * 0.85,
|
||||||
|
),
|
||||||
padding: const EdgeInsets.fromLTRB(24, 12, 24, 32),
|
padding: const EdgeInsets.fromLTRB(24, 12, 24, 32),
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: AppTheme.bgWhite,
|
color: AppTheme.bgWhite,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
Center(
|
Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
|
|
@ -100,7 +103,12 @@ class PengajuanDetailSheet extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
Flexible(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|
@ -209,6 +217,185 @@ class PengajuanDetailSheet extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// Lampiran Section
|
||||||
|
if (pengajuan.lampiranUrl != null && pengajuan.lampiranUrl!.isNotEmpty) ...[
|
||||||
|
const Divider(height: 1, color: AppTheme.bgCard),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.attach_file, size: 20, color: _jenisColor),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Lampiran / Bukti",
|
||||||
|
style: AppTheme.bodySmall.copyWith(color: AppTheme.textTertiary),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => _showFullImage(context, pengajuan.lampiranUrl!),
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
constraints: const BoxConstraints(maxHeight: 200),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
||||||
|
border: Border.all(color: AppTheme.textTertiary.withValues(alpha: 0.2)),
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
||||||
|
child: Image.network(
|
||||||
|
pengajuan.lampiranUrl!,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
loadingBuilder: (context, child, progress) {
|
||||||
|
if (progress == null) return child;
|
||||||
|
return Container(
|
||||||
|
height: 120,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: progress.expectedTotalBytes != null
|
||||||
|
? progress.cumulativeBytesLoaded / progress.expectedTotalBytes!
|
||||||
|
: null,
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return Container(
|
||||||
|
height: 80,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
color: AppTheme.bgCard,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.broken_image_outlined, size: 32, color: AppTheme.textTertiary),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
"Gagal memuat lampiran",
|
||||||
|
style: AppTheme.bodySmall.copyWith(color: AppTheme.textTertiary),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
"Ketuk untuk memperbesar",
|
||||||
|
style: AppTheme.bodySmall.copyWith(
|
||||||
|
color: AppTheme.primaryBlue,
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
|
||||||
|
if (pengajuan.hasSurat && pengajuan.idSurat != null) ...[
|
||||||
|
const Divider(height: 1, color: AppTheme.bgCard),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
builder: (_) => SuratIzinDetailSheet(idSurat: pengajuan.idSurat!),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.description_outlined, size: 20),
|
||||||
|
label: const Text("Lihat Surat Izin"),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppTheme.primaryDark,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
textStyle: AppTheme.labelLarge.copyWith(fontSize: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showFullImage(BuildContext context, String imageUrl) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => Dialog(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
insetPadding: EdgeInsets.zero,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
InteractiveViewer(
|
||||||
|
minScale: 0.5,
|
||||||
|
maxScale: 4.0,
|
||||||
|
child: Center(
|
||||||
|
child: Image.network(
|
||||||
|
imageUrl,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
loadingBuilder: (context, child, progress) {
|
||||||
|
if (progress == null) return child;
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(color: Colors.white),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.broken_image, color: Colors.white54, size: 48),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
"Gagal memuat gambar",
|
||||||
|
style: AppTheme.bodyMedium.copyWith(color: Colors.white54),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: MediaQuery.of(ctx).padding.top + 8,
|
||||||
|
right: 8,
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
icon: Container(
|
||||||
|
padding: const EdgeInsets.all(6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withValues(alpha: 0.5),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.close, color: Colors.white, size: 22),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -102,14 +102,17 @@ class _PresensiDetailSheetState extends State<PresensiDetailSheet> {
|
||||||
return ClipRRect(
|
return ClipRRect(
|
||||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(AppTheme.radiusXl)),
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(AppTheme.radiusXl)),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.fromLTRB(24, 12, 24, 32),
|
constraints: BoxConstraints(
|
||||||
|
maxHeight: MediaQuery.of(context).size.height * 0.85,
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.fromLTRB(24, 12, 24, MediaQuery.of(context).viewInsets.bottom + 32),
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: AppTheme.bgWhite,
|
color: AppTheme.bgWhite,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
|
// Drag handle
|
||||||
Center(
|
Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 40,
|
width: 40,
|
||||||
|
|
@ -122,360 +125,371 @@ class _PresensiDetailSheetState extends State<PresensiDetailSheet> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
Row(
|
// Scrollable content
|
||||||
children: [
|
Flexible(
|
||||||
Expanded(
|
child: SingleChildScrollView(
|
||||||
child: Text("Detail Presensi", style: AppTheme.heading3),
|
child: Column(
|
||||||
),
|
|
||||||
if (history.shift != null && history.shift != '-')
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppTheme.primaryDark.withValues(alpha: 0.08),
|
|
||||||
borderRadius: BorderRadius.circular(AppTheme.radiusFull),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
history.shift!,
|
|
||||||
style: AppTheme.labelMedium.copyWith(color: AppTheme.primaryDark),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
history.tanggal,
|
|
||||||
style: AppTheme.bodyMedium.copyWith(color: AppTheme.textSecondary),
|
|
||||||
),
|
|
||||||
if (history.statusValidasi == 3) ...[
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppTheme.statusRed.withValues(alpha: 0.1),
|
|
||||||
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
|
||||||
border: Border.all(color: AppTheme.statusRed.withValues(alpha: 0.3)),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
|
||||||
const Icon(Icons.cancel, color: AppTheme.statusRed, size: 16),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text(
|
|
||||||
"Presensi Ditolak",
|
|
||||||
style: AppTheme.labelMedium.copyWith(color: AppTheme.statusRed),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
if (history.statusValidasi == 2) ...[
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppTheme.statusYellow.withValues(alpha: 0.1),
|
|
||||||
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
|
||||||
border: Border.all(color: AppTheme.statusYellow.withValues(alpha: 0.3)),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.hourglass_top, color: AppTheme.statusOrange, size: 16),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text(
|
|
||||||
"Menunggu Validasi Admin",
|
|
||||||
style: AppTheme.labelMedium.copyWith(color: AppTheme.statusOrange),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.all(AppTheme.spacingMd),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppTheme.bgCard,
|
|
||||||
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.login_rounded, color: AppTheme.statusGreen, size: 28),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text("Masuk", style: AppTheme.bodySmall),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
_formatTime(history.jamMasuk),
|
|
||||||
style: AppTheme.heading3.copyWith(fontSize: 20),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
width: 1,
|
|
||||||
height: 60,
|
|
||||||
color: AppTheme.textTertiary.withValues(alpha: 0.2),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.logout_rounded, color: AppTheme.primaryOrange, size: 28),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text("Pulang", style: AppTheme.bodySmall),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
_formatTime(history.jamPulang),
|
|
||||||
style: AppTheme.heading3.copyWith(fontSize: 20),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
width: 1,
|
|
||||||
height: 60,
|
|
||||||
color: AppTheme.textTertiary.withValues(alpha: 0.2),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.schedule_rounded, color: AppTheme.primaryBlue, size: 28),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text("Total", style: AppTheme.bodySmall),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
history.totalJam,
|
|
||||||
style: AppTheme.heading3.copyWith(fontSize: 20),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
|
|
||||||
_buildDetailRow(
|
|
||||||
icon: _statusIcon(history.statusMasuk),
|
|
||||||
iconColor: _statusColor(history.statusMasuk),
|
|
||||||
label: "Status Masuk",
|
|
||||||
value: history.statusMasuk ?? '-',
|
|
||||||
valueColor: _statusColor(history.statusMasuk),
|
|
||||||
),
|
|
||||||
|
|
||||||
if (history.jamPulang != null)
|
|
||||||
_buildDetailRow(
|
|
||||||
icon: _statusIcon(history.statusPulang),
|
|
||||||
iconColor: _statusColor(history.statusPulang),
|
|
||||||
label: "Status Pulang",
|
|
||||||
value: history.statusPulang ?? '-',
|
|
||||||
valueColor: _statusColor(history.statusPulang),
|
|
||||||
),
|
|
||||||
|
|
||||||
if (history.waktuTerlambat != null)
|
|
||||||
_buildDetailRow(
|
|
||||||
icon: Icons.timer_off_outlined,
|
|
||||||
iconColor: AppTheme.statusRed,
|
|
||||||
label: "Waktu Terlambat",
|
|
||||||
value: history.waktuTerlambat!,
|
|
||||||
valueColor: AppTheme.statusRed,
|
|
||||||
),
|
|
||||||
|
|
||||||
_buildDetailRow(
|
|
||||||
icon: history.verifikasiWajah ? Icons.face : Icons.face_retouching_off,
|
|
||||||
iconColor: history.verifikasiWajah ? AppTheme.statusGreen : AppTheme.textTertiary,
|
|
||||||
label: "Verifikasi Wajah",
|
|
||||||
value: history.verifikasiWajah ? "Terverifikasi" : "Tidak Terverifikasi",
|
|
||||||
valueColor: history.verifikasiWajah ? AppTheme.statusGreen : AppTheme.textTertiary,
|
|
||||||
),
|
|
||||||
|
|
||||||
if (history.keteranganLuarRadius != null && history.keteranganLuarRadius!.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppTheme.statusYellow.withValues(alpha: 0.08),
|
|
||||||
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
|
||||||
border: Border.all(color: AppTheme.statusYellow.withValues(alpha: 0.3)),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.info_outline, size: 18, color: AppTheme.statusYellow),
|
Row(
|
||||||
const SizedBox(width: 8),
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Text("Detail Presensi", style: AppTheme.heading3),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Keterangan Luar Radius",
|
|
||||||
style: AppTheme.labelMedium.copyWith(color: AppTheme.textSecondary),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
history.keteranganLuarRadius!,
|
|
||||||
style: AppTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
|
|
||||||
if (history.statusValidasi == 3 && history.alasanPenolakan != null && history.alasanPenolakan!.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppTheme.statusRed.withValues(alpha: 0.08),
|
|
||||||
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
|
||||||
border: Border.all(color: AppTheme.statusRed.withValues(alpha: 0.3)),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.error_outline, size: 18, color: AppTheme.statusRed),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Alasan Penolakan",
|
|
||||||
style: AppTheme.labelMedium.copyWith(color: AppTheme.statusRed),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
history.alasanPenolakan!,
|
|
||||||
style: AppTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
|
|
||||||
if (history.statusValidasi == 3 && history.idPresensi != null) ...[
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
if (!_showResubmitForm)
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: () => setState(() => _showResubmitForm = true),
|
|
||||||
icon: const Icon(Icons.refresh, size: 18),
|
|
||||||
label: const Text("Ajukan Ulang"),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: AppTheme.primaryBlue,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
if (_showResubmitForm) ...[
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppTheme.primaryBlue.withValues(alpha: 0.05),
|
|
||||||
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
|
||||||
border: Border.all(color: AppTheme.primaryBlue.withValues(alpha: 0.2)),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Ajukan Ulang Presensi",
|
|
||||||
style: AppTheme.labelLarge.copyWith(color: AppTheme.primaryBlue),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
"Jelaskan alasan pengajuan ulang presensi Anda.",
|
|
||||||
style: AppTheme.bodySmall.copyWith(color: AppTheme.textSecondary),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
TextField(
|
|
||||||
controller: _keteranganController,
|
|
||||||
maxLines: 3,
|
|
||||||
maxLength: 500,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: "Contoh: Saya berada di lokasi klien untuk meeting...",
|
|
||||||
hintStyle: AppTheme.bodySmall.copyWith(color: AppTheme.textTertiary),
|
|
||||||
filled: true,
|
|
||||||
fillColor: AppTheme.bgWhite,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
|
||||||
borderSide: BorderSide(color: AppTheme.textTertiary.withValues(alpha: 0.3)),
|
|
||||||
),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
|
||||||
borderSide: BorderSide(color: AppTheme.textTertiary.withValues(alpha: 0.3)),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
|
||||||
borderSide: const BorderSide(color: AppTheme.primaryBlue),
|
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.all(12),
|
|
||||||
),
|
),
|
||||||
),
|
if (history.shift != null && history.shift != '-')
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.primaryDark.withValues(alpha: 0.08),
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusFull),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
history.shift!,
|
||||||
|
style: AppTheme.labelMedium.copyWith(color: AppTheme.primaryDark),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
history.tanggal,
|
||||||
|
style: AppTheme.bodyMedium.copyWith(color: AppTheme.textSecondary),
|
||||||
|
),
|
||||||
|
if (history.statusValidasi == 3) ...[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Row(
|
Container(
|
||||||
children: [
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
Expanded(
|
decoration: BoxDecoration(
|
||||||
child: OutlinedButton(
|
color: AppTheme.statusRed.withValues(alpha: 0.1),
|
||||||
onPressed: _isSubmitting ? null : () => setState(() => _showResubmitForm = false),
|
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
||||||
style: OutlinedButton.styleFrom(
|
border: Border.all(color: AppTheme.statusRed.withValues(alpha: 0.3)),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
),
|
||||||
shape: RoundedRectangleBorder(
|
child: Row(
|
||||||
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
mainAxisSize: MainAxisSize.min,
|
||||||
),
|
children: [
|
||||||
),
|
const Icon(Icons.cancel, color: AppTheme.statusRed, size: 16),
|
||||||
child: const Text("Batal"),
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
"Presensi Ditolak",
|
||||||
|
style: AppTheme.labelMedium.copyWith(color: AppTheme.statusRed),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
const SizedBox(width: 12),
|
),
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: _isSubmitting ? null : _handleResubmit,
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: AppTheme.primaryBlue,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: _isSubmitting
|
|
||||||
? const SizedBox(
|
|
||||||
height: 18,
|
|
||||||
width: 18,
|
|
||||||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
|
|
||||||
)
|
|
||||||
: const Text("Kirim"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
if (history.statusValidasi == 2) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.statusYellow.withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
||||||
|
border: Border.all(color: AppTheme.statusYellow.withValues(alpha: 0.3)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.hourglass_top, color: AppTheme.statusOrange, size: 16),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
"Menunggu Validasi Admin",
|
||||||
|
style: AppTheme.labelMedium.copyWith(color: AppTheme.statusOrange),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(AppTheme.spacingMd),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.bgCard,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.login_rounded, color: AppTheme.statusGreen, size: 28),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text("Masuk", style: AppTheme.bodySmall),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
_formatTime(history.jamMasuk),
|
||||||
|
style: AppTheme.heading3.copyWith(fontSize: 20),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: 1,
|
||||||
|
height: 60,
|
||||||
|
color: AppTheme.textTertiary.withValues(alpha: 0.2),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.logout_rounded, color: AppTheme.primaryOrange, size: 28),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text("Pulang", style: AppTheme.bodySmall),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
_formatTime(history.jamPulang),
|
||||||
|
style: AppTheme.heading3.copyWith(fontSize: 20),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: 1,
|
||||||
|
height: 60,
|
||||||
|
color: AppTheme.textTertiary.withValues(alpha: 0.2),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.schedule_rounded, color: AppTheme.primaryBlue, size: 28),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text("Total", style: AppTheme.bodySmall),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
history.totalJam,
|
||||||
|
style: AppTheme.heading3.copyWith(fontSize: 20),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
_buildDetailRow(
|
||||||
|
icon: _statusIcon(history.statusMasuk),
|
||||||
|
iconColor: _statusColor(history.statusMasuk),
|
||||||
|
label: "Status Masuk",
|
||||||
|
value: history.statusMasuk ?? '-',
|
||||||
|
valueColor: _statusColor(history.statusMasuk),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (history.jamPulang != null)
|
||||||
|
_buildDetailRow(
|
||||||
|
icon: _statusIcon(history.statusPulang),
|
||||||
|
iconColor: _statusColor(history.statusPulang),
|
||||||
|
label: "Status Pulang",
|
||||||
|
value: history.statusPulang ?? '-',
|
||||||
|
valueColor: _statusColor(history.statusPulang),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (history.waktuTerlambat != null)
|
||||||
|
_buildDetailRow(
|
||||||
|
icon: Icons.timer_off_outlined,
|
||||||
|
iconColor: AppTheme.statusRed,
|
||||||
|
label: "Waktu Terlambat",
|
||||||
|
value: history.waktuTerlambat!,
|
||||||
|
valueColor: AppTheme.statusRed,
|
||||||
|
),
|
||||||
|
|
||||||
|
_buildDetailRow(
|
||||||
|
icon: history.verifikasiWajah ? Icons.face : Icons.face_retouching_off,
|
||||||
|
iconColor: history.verifikasiWajah ? AppTheme.statusGreen : AppTheme.textTertiary,
|
||||||
|
label: "Verifikasi Wajah",
|
||||||
|
value: history.verifikasiWajah ? "Terverifikasi" : "Tidak Terverifikasi",
|
||||||
|
valueColor: history.verifikasiWajah ? AppTheme.statusGreen : AppTheme.textTertiary,
|
||||||
|
),
|
||||||
|
|
||||||
|
if (history.keteranganLuarRadius != null && history.keteranganLuarRadius!.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.statusYellow.withValues(alpha: 0.08),
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
||||||
|
border: Border.all(color: AppTheme.statusYellow.withValues(alpha: 0.3)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.info_outline, size: 18, color: AppTheme.statusYellow),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Keterangan Luar Radius",
|
||||||
|
style: AppTheme.labelMedium.copyWith(color: AppTheme.textSecondary),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
history.keteranganLuarRadius!,
|
||||||
|
style: AppTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
if (history.statusValidasi == 3 && history.alasanPenolakan != null && history.alasanPenolakan!.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.statusRed.withValues(alpha: 0.08),
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
||||||
|
border: Border.all(color: AppTheme.statusRed.withValues(alpha: 0.3)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.error_outline, size: 18, color: AppTheme.statusRed),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Alasan Penolakan",
|
||||||
|
style: AppTheme.labelMedium.copyWith(color: AppTheme.statusRed),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
history.alasanPenolakan!,
|
||||||
|
style: AppTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
if (history.statusValidasi == 3 && history.idPresensi != null) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
if (!_showResubmitForm)
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () => setState(() => _showResubmitForm = true),
|
||||||
|
icon: const Icon(Icons.refresh, size: 18),
|
||||||
|
label: const Text("Ajukan Ulang"),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppTheme.primaryBlue,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (_showResubmitForm) ...[
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.primaryBlue.withValues(alpha: 0.05),
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
||||||
|
border: Border.all(color: AppTheme.primaryBlue.withValues(alpha: 0.2)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Ajukan Ulang Presensi",
|
||||||
|
style: AppTheme.labelLarge.copyWith(color: AppTheme.primaryBlue),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
"Jelaskan alasan pengajuan ulang presensi Anda.",
|
||||||
|
style: AppTheme.bodySmall.copyWith(color: AppTheme.textSecondary),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextField(
|
||||||
|
controller: _keteranganController,
|
||||||
|
maxLines: 3,
|
||||||
|
maxLength: 500,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: "Contoh: Saya berada di lokasi klien untuk meeting...",
|
||||||
|
hintStyle: AppTheme.bodySmall.copyWith(color: AppTheme.textTertiary),
|
||||||
|
filled: true,
|
||||||
|
fillColor: AppTheme.bgWhite,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
||||||
|
borderSide: BorderSide(color: AppTheme.textTertiary.withValues(alpha: 0.3)),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
||||||
|
borderSide: BorderSide(color: AppTheme.textTertiary.withValues(alpha: 0.3)),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
||||||
|
borderSide: const BorderSide(color: AppTheme.primaryBlue),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.all(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: _isSubmitting ? null : () => setState(() => _showResubmitForm = false),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text("Batal"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _isSubmitting ? null : _handleResubmit,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppTheme.primaryBlue,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: _isSubmitting
|
||||||
|
? const SizedBox(
|
||||||
|
height: 18,
|
||||||
|
width: 18,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
|
||||||
|
)
|
||||||
|
: const Text("Kirim"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
],
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,295 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import '../../core/theme.dart';
|
||||||
|
import '../../providers/surat_izin_provider.dart';
|
||||||
|
|
||||||
|
class SuratIzinDetailSheet extends StatefulWidget {
|
||||||
|
final String idSurat;
|
||||||
|
|
||||||
|
const SuratIzinDetailSheet({super.key, required this.idSurat});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SuratIzinDetailSheet> createState() => _SuratIzinDetailSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SuratIzinDetailSheetState extends State<SuratIzinDetailSheet> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
context.read<SuratIzinProvider>().fetchDetail(widget.idSurat);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _statusColor(String status) {
|
||||||
|
switch (status) {
|
||||||
|
case 'disetujui': return AppTheme.statusGreen;
|
||||||
|
case 'ditolak': return AppTheme.statusRed;
|
||||||
|
case 'menunggu_hrd': return AppTheme.primaryDark;
|
||||||
|
default: return AppTheme.statusYellow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DraggableScrollableSheet(
|
||||||
|
initialChildSize: 0.85,
|
||||||
|
maxChildSize: 0.95,
|
||||||
|
minChildSize: 0.5,
|
||||||
|
builder: (_, scrollController) => Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(AppTheme.radiusXl)),
|
||||||
|
),
|
||||||
|
child: Consumer<SuratIzinProvider>(
|
||||||
|
builder: (context, provider, _) {
|
||||||
|
final detail = provider.selectedSurat;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(top: 12),
|
||||||
|
width: 40,
|
||||||
|
height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.textTertiary,
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(AppTheme.spacingMd),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => Navigator.pop(context),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.bgCard,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.arrow_back, size: 20, color: AppTheme.textPrimary),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text("Detail Surat Izin", style: AppTheme.heading3),
|
||||||
|
),
|
||||||
|
if (detail != null)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _statusColor(detail.statusSurat).withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusFull),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
detail.statusLabel,
|
||||||
|
style: AppTheme.labelMedium.copyWith(
|
||||||
|
color: _statusColor(detail.statusSurat),
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (provider.isLoadingDetail)
|
||||||
|
const Expanded(
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
)
|
||||||
|
else if (detail == null)
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.description_outlined, size: 48, color: AppTheme.textTertiary),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
"Gagal memuat detail surat",
|
||||||
|
style: AppTheme.bodyMedium.copyWith(color: AppTheme.textTertiary),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
controller: scrollController,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: AppTheme.spacingMd),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildDetailSection("Informasi Surat", [
|
||||||
|
_buildDetailRow("No. Surat", detail.nomorSurat),
|
||||||
|
_buildDetailRow("Jenis Izin", detail.jenisIzin),
|
||||||
|
if (detail.tanggalMulai != null) _buildDetailRow("Tanggal Mulai", detail.tanggalMulai!),
|
||||||
|
if (detail.tanggalSelesai != null) _buildDetailRow("Tanggal Selesai", detail.tanggalSelesai!),
|
||||||
|
if (detail.alasan != null) _buildDetailRow("Alasan", detail.alasan!),
|
||||||
|
]),
|
||||||
|
|
||||||
|
const SizedBox(height: AppTheme.spacingMd),
|
||||||
|
|
||||||
|
if (detail.isiSurat != null) ...[
|
||||||
|
_buildDetailSection("Isi Surat", []),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(AppTheme.spacingMd),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.bgCard,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
detail.isiSurat!,
|
||||||
|
style: AppTheme.bodyMedium.copyWith(height: 1.6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: AppTheme.spacingMd),
|
||||||
|
],
|
||||||
|
|
||||||
|
_buildDetailSection("Tanda Tangan", []),
|
||||||
|
_buildSignatureArea("Yang Mengajukan", detail.pengajuNama ?? '-', detail.ttdPengaju),
|
||||||
|
|
||||||
|
...detail.approvals.where((a) => a.tahap == 1).map((a) =>
|
||||||
|
_buildSignatureArea("Manajer", a.approverNama, a.ttdApprover,
|
||||||
|
status: a.status, catatan: a.catatan),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (!detail.approvals.any((a) => a.tahap == 1))
|
||||||
|
_buildSignatureArea("Manajer", "Menunggu Approval", null, isPending: true),
|
||||||
|
|
||||||
|
...detail.approvals.where((a) => a.tahap == 2).map((a) =>
|
||||||
|
_buildSignatureArea("HRD", a.approverNama, a.ttdApprover,
|
||||||
|
status: a.status, catatan: a.catatan),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (!detail.approvals.any((a) => a.tahap == 2))
|
||||||
|
_buildSignatureArea("HRD", "Menunggu Approval", null, isPending: true),
|
||||||
|
|
||||||
|
const SizedBox(height: AppTheme.spacingXl),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDetailSection(String title, List<Widget> children) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title.toUpperCase(),
|
||||||
|
style: AppTheme.labelMedium.copyWith(
|
||||||
|
color: AppTheme.textTertiary,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: AppTheme.spacingSm),
|
||||||
|
if (children.isNotEmpty)
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
||||||
|
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||||
|
),
|
||||||
|
child: Column(children: children),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDetailRow(String label, String value) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: AppTheme.spacingMd, vertical: 10),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 120,
|
||||||
|
child: Text(label, style: AppTheme.bodySmall.copyWith(color: AppTheme.textTertiary)),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(value, style: AppTheme.bodyMedium.copyWith(fontWeight: FontWeight.w500)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSignatureArea(String label, String name, String? ttdUrl, {
|
||||||
|
String? status,
|
||||||
|
String? catatan,
|
||||||
|
bool isPending = false,
|
||||||
|
}) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: AppTheme.spacingSm),
|
||||||
|
padding: const EdgeInsets.all(AppTheme.spacingMd),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isPending ? AppTheme.bgCard : Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
||||||
|
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: AppTheme.labelMedium.copyWith(color: AppTheme.textTertiary, letterSpacing: 0.3),
|
||||||
|
),
|
||||||
|
const SizedBox(height: AppTheme.spacingSm),
|
||||||
|
|
||||||
|
SizedBox(
|
||||||
|
height: 80,
|
||||||
|
child: ttdUrl != null
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: ttdUrl,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
placeholder: (_, __) => const SizedBox.shrink(),
|
||||||
|
errorWidget: (_, __, ___) => Icon(Icons.error_outline, color: AppTheme.textTertiary),
|
||||||
|
)
|
||||||
|
: isPending
|
||||||
|
? Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.hourglass_empty, color: AppTheme.textTertiary, size: 28),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text("Menunggu", style: AppTheme.bodySmall.copyWith(color: AppTheme.textTertiary)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: status == 'ditolak'
|
||||||
|
? Text("DITOLAK", style: AppTheme.labelLarge.copyWith(color: AppTheme.statusRed))
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: AppTheme.spacingSm),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.only(top: AppTheme.spacingSm),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
border: Border(top: BorderSide(color: Color(0xFFE2E8F0))),
|
||||||
|
),
|
||||||
|
child: Text(name, style: AppTheme.labelLarge.copyWith(fontSize: 13)),
|
||||||
|
),
|
||||||
|
|
||||||
|
if (catatan != null && catatan.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
"Catatan: $catatan",
|
||||||
|
style: AppTheme.bodySmall.copyWith(color: AppTheme.textSecondary, fontStyle: FontStyle.italic),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue