MIF_E31222451/lib/detail_laporan.dart

651 lines
24 KiB
Dart

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:intl/intl.dart';
import 'package:ternak/components/colors.dart';
import 'package:excel/excel.dart' as xl;
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'dart:io';
import 'package:flutter/foundation.dart';
class DetailLaporanScreen extends StatefulWidget {
final String status;
final User user;
final DateTimeRange selectedRange;
const DetailLaporanScreen(
{super.key,
required this.user,
required this.selectedRange,
required this.status});
@override
State<DetailLaporanScreen> createState() => _DetailLaporanScreenState();
}
class _DetailLaporanScreenState extends State<DetailLaporanScreen> {
final PagingController<int, Map<String, dynamic>> _pagingController =
PagingController(firstPageKey: 0);
static const _pageSize = 5;
DocumentSnapshot<Map<String, dynamic>>? lastDoc;
@override
void initState() {
super.initState();
_pagingController.addPageRequestListener((pageKey) {
_fetchPage(pageKey);
});
}
Future<void> _fetchPage(int pageKey) async {
var query = FirebaseFirestore.instance
.collection("hewan")
.where("user_uid", isEqualTo: widget.user.uid)
.where('tanggal_masuk',
isGreaterThanOrEqualTo: DateFormat('yyyy-MM-dd')
.format(widget.selectedRange.start))
.where('tanggal_masuk',
isLessThanOrEqualTo: DateFormat('yyyy-MM-dd')
.format(widget.selectedRange.end))
.where("status", isEqualTo: widget.status)
.orderBy("nama");
if (lastDoc != null) {
query = query.startAfter([lastDoc!.data()?["nama"]]);
}
var newItems = await query.limit(_pageSize).get();
lastDoc = newItems.docs.last;
var list = newItems.docs.map((e) => e.data()).toList();
final isLastPage = list.length < _pageSize;
if (isLastPage) {
_pagingController.appendLastPage(list);
} else {
final nextPageKey = pageKey + list.length;
_pagingController.appendPage(list, nextPageKey);
}
}
@override
void dispose() {
_pagingController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[50],
appBar: AppBar(
title: Text(
" ${widget.status}",
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
backgroundColor: MyColors.primaryC,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.pop(context);
},
),
actions: [
Container(
margin: const EdgeInsets.only(right: 16),
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(16),
),
child: Text(
"${DateFormat('dd/MM/yyyy').format(widget.selectedRange.start)} - ${DateFormat('dd/MM/yyyy').format(widget.selectedRange.end)}",
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 13,
color: Colors.white,
),
),
),
),
),
IconButton(
icon: const Icon(Icons.download_rounded, color: Colors.white),
tooltip: 'Download Excel',
onPressed: () => _exportToExcel(),
),
],
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header section
Container(
color: MyColors.primaryC,
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.assignment_outlined,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Detail Laporan",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
Text(
"Daftar hewan dengan status ${widget.status}",
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.8),
),
),
],
),
],
),
),
// Curved transition
Container(
height: 20,
decoration: BoxDecoration(
color: MyColors.primaryC,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
),
// Legend
Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 0),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.amber.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.amber.withOpacity(0.3)),
),
child: Row(
children: [
const Icon(
Icons.info_outline,
color: Colors.amber,
size: 20,
),
const SizedBox(width: 8),
Flexible(
child: Text(
"Geser ke kanan untuk melihat semua data",
style: TextStyle(
fontSize: 13,
color: Colors.grey[800],
),
),
),
],
),
),
),
// Table
Expanded(
child: Container(
margin: const EdgeInsets.fromLTRB(20, 16, 20, 20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
spreadRadius: 0,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
child: SizedBox(
width: 1100,
child: Column(
children: [
// Header row
Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: MyColors.primaryC.withOpacity(0.05),
border: Border(
bottom: BorderSide(
color: Colors.grey.withOpacity(0.2),
width: 1.0,
),
),
),
child: Row(
children: [
_buildHeaderCell("No.", 50),
_buildHeaderCell("Nama", 100),
_buildHeaderCell("Jenis Kelamin", 130),
_buildHeaderCell("Usia", 130),
_buildHeaderCell("Jenis Hewan", 130),
_buildHeaderCell("Kategori", 130),
_buildHeaderCell("Kondisi", 100),
_buildHeaderCell("Kandang", 100),
_buildHeaderCell("Blok", 100),
],
),
),
// Data rows
Expanded(
child: PagedListView<int, Map<String, dynamic>>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<Map<String, dynamic>>(
itemBuilder: (context, item, index) {
return Container(
decoration: BoxDecoration(
color: index % 2 == 0 ? Colors.white : Colors.grey[50],
border: Border(
bottom: BorderSide(
color: Colors.grey.withOpacity(0.2),
width: 1.0,
),
),
),
padding: const EdgeInsets.symmetric(vertical: 14),
child: Row(
children: [
_buildDataCell((index + 1).toString(), 50),
_buildDataCell(item["nama"].toString(), 100),
_buildDataCell(item["jenis_kelamin"].toString(), 130),
_buildDataCell(item["usia"].toString(), 130),
_buildDataCell(item["jenis"].toString(), 130),
_buildDataCell(item["kategori"].toString(), 130),
_buildStatusCell(item["status_kesehatan"].toString(), 100),
_buildDataCell(item["kandang"].toString(), 100),
_buildDataCell(item["blok"].toString(), 100),
],
),
);
},
firstPageErrorIndicatorBuilder: (_) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 60,
color: Colors.red[300],
),
const SizedBox(height: 16),
const Text(
"Terjadi kesalahan",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () => _pagingController.refresh(),
style: ElevatedButton.styleFrom(
backgroundColor: MyColors.primaryC,
foregroundColor: Colors.white,
),
child: const Text("Coba Lagi"),
),
],
),
),
noItemsFoundIndicatorBuilder: (_) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 60,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
"Tidak ada data hewan",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
Text(
"Tidak ditemukan hewan dengan status ${widget.status}",
style: TextStyle(
fontSize: 14,
color: Colors.grey[500],
),
textAlign: TextAlign.center,
),
],
),
),
newPageProgressIndicatorBuilder: (_) => const Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(
color: MyColors.primaryC,
),
),
),
),
),
),
],
),
),
),
),
),
),
],
),
);
}
Widget _buildHeaderCell(String text, double width) {
return Container(
margin: const EdgeInsets.only(right: 10),
width: width,
child: Text(
text,
style: TextStyle(
fontWeight: FontWeight.bold,
color: MyColors.primaryC,
fontSize: 15,
),
textAlign: TextAlign.center,
),
);
}
Widget _buildDataCell(String text, double width) {
return Container(
margin: const EdgeInsets.only(right: 10),
width: width,
child: Text(
text,
style: TextStyle(
color: Colors.grey[800],
fontSize: 14,
),
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
);
}
Widget _buildStatusCell(String status, double width) {
Color statusColor;
if (status.toLowerCase().contains('sehat')) {
statusColor = Colors.green;
} else if (status.toLowerCase().contains('sakit')) {
statusColor = Colors.red;
} else {
statusColor = Colors.orange;
}
return Container(
margin: const EdgeInsets.only(right: 10),
width: width,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: statusColor.withOpacity(0.3)),
),
child: Text(
status,
style: TextStyle(
color: statusColor,
fontWeight: FontWeight.w600,
fontSize: 13,
),
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
),
);
}
Future<void> _exportToExcel() async {
try {
// Show loading dialog
if (mounted) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Menyiapkan file Excel...'),
],
),
),
);
}
// Create Excel object
final excel = xl.Excel.createExcel();
final sheet = excel['Laporan ${widget.status}'];
// Add header row with styling
final headerStyle = xl.CellStyle(
backgroundColorHex: xl.ExcelColor.fromHexString('#4C72AF'),
fontColorHex: xl.ExcelColor.fromHexString('#FFFFFF'),
bold: true,
horizontalAlign: xl.HorizontalAlign.Center,
);
final headers = [
'No.',
'Nama',
'Jenis Kelamin',
'Usia',
'Jenis Hewan',
'Kategori',
'Kondisi',
'Kandang',
'Blok'
];
// Add headers
for (var i = 0; i < headers.length; i++) {
final cell = sheet.cell(xl.CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 0));
cell.value = xl.TextCellValue(headers[i]);
cell.cellStyle = headerStyle;
}
// Fetch all data from Firestore (not just the paginated data)
var query = FirebaseFirestore.instance
.collection("hewan")
.where("user_uid", isEqualTo: widget.user.uid)
.where('tanggal_masuk',
isGreaterThanOrEqualTo: DateFormat('yyyy-MM-dd').format(widget.selectedRange.start))
.where('tanggal_masuk',
isLessThanOrEqualTo: DateFormat('yyyy-MM-dd').format(widget.selectedRange.end))
.where("status", isEqualTo: widget.status)
.orderBy("tanggal_masuk");
final snapshot = await query.get();
final data = snapshot.docs.map((e) => e.data()).toList();
// Add data rows
for (var i = 0; i < data.length; i++) {
final item = data[i];
final row = i + 1; // Excel rows are 0-indexed, but we start data at row 1
sheet.cell(xl.CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: row)).value = xl.TextCellValue((i + 1).toString());
sheet.cell(xl.CellIndex.indexByColumnRow(columnIndex: 1, rowIndex: row)).value = xl.TextCellValue(item["nama"]?.toString() ?? '');
sheet.cell(xl.CellIndex.indexByColumnRow(columnIndex: 2, rowIndex: row)).value = xl.TextCellValue(item["jenis_kelamin"]?.toString() ?? '');
sheet.cell(xl.CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: row)).value = xl.TextCellValue(item["usia"]?.toString() ?? '');
sheet.cell(xl.CellIndex.indexByColumnRow(columnIndex: 4, rowIndex: row)).value = xl.TextCellValue(item["jenis"]?.toString() ?? '');
sheet.cell(xl.CellIndex.indexByColumnRow(columnIndex: 5, rowIndex: row)).value = xl.TextCellValue(item["kategori"]?.toString() ?? '');
sheet.cell(xl.CellIndex.indexByColumnRow(columnIndex: 6, rowIndex: row)).value = xl.TextCellValue(item["status_kesehatan"]?.toString() ?? '');
sheet.cell(xl.CellIndex.indexByColumnRow(columnIndex: 7, rowIndex: row)).value = xl.TextCellValue(item["kandang"]?.toString() ?? '');
sheet.cell(xl.CellIndex.indexByColumnRow(columnIndex: 8, rowIndex: row)).value = xl.TextCellValue(item["blok"]?.toString() ?? '');
}
// Auto fit columns
for (var i = 0; i < headers.length; i++) {
sheet.setColumnWidth(i, 15);
}
// Get directory for saving file and create filename
final now = DateTime.now();
final fileName = 'Laporan_${widget.status}_${DateFormat('yyyyMMdd_HHmmss').format(now)}.xlsx';
String? filePath;
if (!kIsWeb) {
if (Platform.isAndroid) {
// On Android, save to Downloads folder
final status = await Permission.storage.request();
if (status.isGranted) {
// Use the Downloads directory
final directory = Directory('/storage/emulated/0/Download');
if (!await directory.exists()) {
await directory.create(recursive: true);
}
filePath = '${directory.path}/$fileName';
}
} else if (Platform.isIOS) {
// On iOS, use documents directory
final directory = await getApplicationDocumentsDirectory();
filePath = '${directory.path}/$fileName';
} else {
// Fallback for other platforms
final directory = await getExternalStorageDirectory();
filePath = '${directory?.path ?? (await getTemporaryDirectory()).path}/$fileName';
}
} else {
// Web platform handling (not implementing here)
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Download pada web tidak didukung'),
backgroundColor: Colors.red,
),
);
}
return;
}
// Check if filePath was successfully determined
if (filePath == null) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Gagal mendapatkan lokasi penyimpanan'),
backgroundColor: Colors.red,
),
);
}
return;
}
// Save the Excel file
final fileBytes = excel.encode();
if (fileBytes != null) {
final file = File(filePath);
await file.writeAsBytes(fileBytes);
// Close loading dialog
if (mounted) {
Navigator.pop(context);
}
// Show success notification with more helpful message
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('File Excel berhasil disimpan!',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
if (Platform.isAndroid)
const Text('Lokasi: folder Download di penyimpanan internal',
style: TextStyle(fontSize: 13),
)
else
Text('Lokasi: $filePath', style: TextStyle(fontSize: 13)),
],
),
backgroundColor: Colors.green,
duration: const Duration(seconds: 6),
action: SnackBarAction(
label: 'OK',
textColor: Colors.white,
onPressed: () {},
),
),
);
}
}
} catch (e) {
// Close loading dialog if open
if (mounted) {
Navigator.maybeOf(context)?.pop();
}
// Show error message
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Gagal mengunduh file: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
}
print('Error exporting to Excel: $e');
}
}
}