FarisaRahmaSari_E31222327/BBS/lib/view/home/cutipage.dart

638 lines
20 KiB
Dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:get/get.dart';
import 'package:qyuota/view/home/home_view.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:qyuota/config/colors.dart';
import 'package:qyuota/models/cuti_model.dart';
import 'package:qyuota/config/api_config.dart';
import 'package:qyuota/services/api_service.dart';
import 'package:file_picker/file_picker.dart';
import 'package:qyuota/services/auth_service.dart';
import 'dart:async';
import 'package:http_parser/http_parser.dart';
class Cutipage extends StatefulWidget {
const Cutipage({super.key});
@override
State<Cutipage> createState() => _CutipageState();
}
class _CutipageState extends State<Cutipage> {
final controllerName = TextEditingController();
final formController = TextEditingController();
final toController = TextEditingController();
final _keteranganController = TextEditingController();
PlatformFile? _selectedFile;
String? _fileName;
String dropValueCategories = "Pilih";
var categoriesList = <String>[
"Pilih",
"Cuti Tahunan",
"Cuti Bulanan"
];
@override
void dispose() {
controllerName.dispose();
formController.dispose();
toController.dispose();
_keteranganController.dispose();
super.dispose();
}
Future<void> _pickFile() async {
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['pdf'],
);
if (result != null) {
setState(() {
_selectedFile = result.files.first;
_fileName = _selectedFile?.name;
});
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error picking file: ${e.toString()}')),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[100],
appBar: AppBar(
elevation: 0,
backgroundColor: ConstColors.primaryColor,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
),
title: const Text(
"Pengajuan Cuti",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
centerTitle: true,
),
body: SingleChildScrollView(
child: Column(
children: [
Container(
height: 75,
decoration: BoxDecoration(
color: ConstColors.primaryColor,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(30),
bottomRight: Radius.circular(30),
),
),
),
Transform.translate(
offset: const Offset(0, -60),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 5,
blurRadius: 10,
),
],
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Form Pengajuan Cuti",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: ConstColors.primaryColor,
),
),
const SizedBox(height: 20),
_buildTextField(
controller: controllerName,
label: "Nama Lengkap",
hint: "Masukkan nama lengkap Anda",
icon: Icons.person_outline,
),
const SizedBox(height: 15),
_buildTextField(
controller: _keteranganController,
maxLines: 3,
label: "Keterangan",
hint: "Berikan alasan cuti Anda",
icon: Icons.description_outlined,
),
const SizedBox(height: 15),
_buildDropdown(),
const SizedBox(height: 15),
Row(
children: [
Expanded(
child: _buildDateField(
controller: formController,
label: "Tanggal Mulai",
onTap: () => _selectDate(formController),
),
),
const SizedBox(width: 15),
Expanded(
child: _buildDateField(
controller: toController,
label: "Tanggal Selesai",
onTap: () => _selectDate(toController),
),
),
],
),
const SizedBox(height: 15),
_buildFilePicker(),
const SizedBox(height: 30),
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: ConstColors.skyColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
onPressed: _handleSubmit,
child: const Text(
"Ajukan Cuti",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
],
),
),
),
),
],
),
),
);
}
Future<void> _handleSubmit() async {
if (_selectedFile == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Mohon pilih file PDF')),
);
return;
}
if (_keteranganController.text.isEmpty ||
controllerName.text.isEmpty ||
formController.text.isEmpty ||
toController.text.isEmpty ||
dropValueCategories == "Pilih") {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Mohon lengkapi semua field')),
);
return;
}
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return const Center(child: CircularProgressIndicator());
},
);
try {
final token = await AuthService().getToken();
if (token == null) {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Error autentikasi')),
);
return;
}
// Debug: Print token
print('Token: $token');
// Convert dates to correct format
final startDate = DateFormat('dd/MM/yyyy').parse(formController.text);
final endDate = DateFormat('dd/MM/yyyy').parse(toController.text);
final uri = Uri.parse('${ApiConfig.baseUrl}/api/mobile/cuti');
// Debug: Print URL
print('Submitting to URL: $uri');
final request = http.MultipartRequest('POST', uri)
..headers.addAll({
'Authorization': 'Bearer $token',
'Accept': 'application/json',
'Content-Type': 'multipart/form-data',
});
// Add form fields
request.fields.addAll({
'nama': controllerName.text,
'keterangan': _keteranganController.text,
'jenis_cuti': dropValueCategories,
'tanggal_mulai': DateFormat('yyyy-MM-dd').format(startDate),
'tanggal_selesai': DateFormat('yyyy-MM-dd').format(endDate),
});
// Debug: Print request fields
print('Request fields: ${request.fields}');
// Add file to request
if (_selectedFile != null && _selectedFile!.bytes != null) {
final file = http.MultipartFile.fromBytes(
'file_pdf',
_selectedFile!.bytes!,
filename: _fileName ?? 'document.pdf',
contentType: MediaType('application', 'pdf'),
);
request.files.add(file);
// Debug: Print file details
print('File name: ${file.filename}');
print('File size: ${_selectedFile!.bytes!.length} bytes');
}
final streamedResponse = await request.send().timeout(
const Duration(seconds: 30),
onTimeout: () {
throw TimeoutException('Request timeout');
},
);
final response = await http.Response.fromStream(streamedResponse);
// Debug: Print response
print('Response status: ${response.statusCode}');
print('Response body: ${response.body}');
final responseData = json.decode(response.body);
Navigator.pop(context);
if (response.statusCode == 201 || response.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Pengajuan cuti berhasil disimpan'),
backgroundColor: Colors.green,
),
);
setState(() {
controllerName.clear();
_keteranganController.clear();
formController.clear();
toController.clear();
dropValueCategories = "Pilih";
_selectedFile = null;
_fileName = null;
});
Get.off(() => const HomeView());
} else {
throw Exception(responseData['message'] ?? 'Gagal mengajukan cuti');
}
} catch (e) {
Navigator.pop(context);
String errorMessage = 'Gagal mengajukan cuti';
if (e is TimeoutException) {
errorMessage = 'Koneksi timeout, silakan coba lagi';
} else if (e is SocketException) {
errorMessage = 'Tidak dapat terhubung ke server';
} else {
errorMessage = e.toString().replaceAll('Exception: ', '');
}
// Debug: Print error
print('Error submitting form: $errorMessage');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(errorMessage),
backgroundColor: Colors.red,
),
);
}
}
Widget _buildTextField({
TextEditingController? controller,
String? label,
String? hint,
IconData? icon,
int maxLines = 1,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
label ?? "",
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
if (icon != null) ...[
const SizedBox(width: 8),
Icon(icon, size: 18, color: ConstColors.primaryColor),
],
],
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[300]!),
),
child: TextField(
controller: controller,
maxLines: maxLines,
decoration: InputDecoration(
hintText: hint,
hintStyle: TextStyle(color: Colors.grey[400]),
border: InputBorder.none,
contentPadding: const EdgeInsets.all(16),
alignLabelWithHint: true,
),
),
),
],
);
}
Widget _buildDropdown() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Jenis Cuti",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[300]!),
),
child: DropdownButtonFormField<String>(
value: dropValueCategories,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.category_outlined, color: ConstColors.primaryColor),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
isExpanded: true,
items: categoriesList.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
setState(() {
dropValueCategories = newValue;
});
}
},
),
),
],
);
}
Widget _buildDateField({
required TextEditingController controller,
required String label,
required VoidCallback onTap,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[300]!),
),
child: TextField(
controller: controller,
readOnly: true,
onTap: onTap,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.calendar_today, color: ConstColors.primaryColor),
border: InputBorder.none,
contentPadding: EdgeInsets.all(16),
),
),
),
],
);
}
Widget _buildFilePicker() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"File Penunjang (PDF)",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[300]!),
),
child: Row(
children: [
Expanded(
child: Container(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(Icons.picture_as_pdf,
color: _selectedFile != null ? Colors.red : Colors.grey,
),
const SizedBox(width: 8),
Expanded(
child: Text(
_selectedFile != null
? _fileName!
: 'Pilih file PDF',
style: TextStyle(
color: _selectedFile != null
? Colors.black87
: Colors.grey[600],
fontSize: 14,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
),
Container(
decoration: BoxDecoration(
border: Border(
left: BorderSide(color: Colors.grey[300]!),
),
),
child: IconButton(
icon: const Icon(Icons.upload_file),
color: ConstColors.primaryColor,
onPressed: _pickFile,
),
),
],
),
),
if (_selectedFile != null) ...[
const SizedBox(height: 8),
Row(
children: [
Icon(Icons.check_circle,
color: Colors.green,
size: 16,
),
const SizedBox(width: 8),
Text(
'File siap diupload (${(_selectedFile!.size / 1024).toStringAsFixed(2)} KB)',
style: TextStyle(
color: Colors.green[700],
fontSize: 12,
),
),
],
),
],
],
);
}
Future<void> _selectDate(TextEditingController controller) async {
try {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: controller.text.isEmpty
? DateTime.now()
: DateFormat('dd/MM/yyyy').parse(controller.text),
firstDate: DateTime(2024),
lastDate: DateTime(2025, 12, 31),
builder: (context, child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: ColorScheme.light(
primary: ConstColors.skyColor,
onPrimary: Colors.white,
onSurface: Colors.black,
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: ConstColors.skyColor,
),
),
),
child: child!,
);
},
);
if (picked != null) {
setState(() {
controller.text = DateFormat('dd/MM/yyyy').format(picked);
});
}
} catch (e) {
print('Error selecting date: $e');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Terjadi kesalahan saat memilih tanggal')),
);
}
}
}
String _convertDateFormat(String inputDate) {
try {
DateTime parsedDate = DateFormat('dd/MM/yyyy').parse(inputDate);
return DateFormat('yyyy-MM-dd').format(parsedDate);
} catch (e) {
return inputDate; // Jika gagal, kembalikan input asli
}
}
void showLoaderDialog(BuildContext context) {
showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: Row(
children: [
const CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation(ConstColors.primaryColor),
),
Container(
margin: const EdgeInsets.only(left: 20),
child: const Text("Mohon tunggu..."),
),
],
),
);
},
);
}