638 lines
20 KiB
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..."),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
} |