import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:flutter/scheduler.dart' show timeDilation;
import 'package:intl/intl.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// Slow down animations for demonstration (remove in production)
timeDilation = 2.0;
return MaterialApp(
title: 'Absensi Management',
theme: ThemeData(
primarySwatch: Colors.blue,
fontFamily: 'Poppins',
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
filled: true,
fillColor: Colors.white,
),
),
home: const HomePage(),
debugShowCheckedModeBanner: false,
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
final String apiUrl = "https://68a51cab2a3deed2960c6dbb.mockapi.io/absen";
List items = [];
bool isLoading = true;
bool isError = false;
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
final TextEditingController nameController = TextEditingController();
final TextEditingController dateController = TextEditingController();
final TextEditingController timeInController = TextEditingController();
final TextEditingController timeOutController = TextEditingController();
final TextEditingController noteController = TextEditingController();
String selectedStatus = "Hadir";
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
);
_fadeAnimation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
),
);
_animationController.forward();
fetchData();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
Future<void> fetchData() async {
setState(() {
isLoading = true;
isError = false;
});
try {
final response = await http.get(Uri.parse(apiUrl));
if (response.statusCode == 200) {
setState(() {
items = json.decode(response.body);
isLoading = false;
});
} else {
setState(() {
isLoading = false;
isError = true;
});
}
} catch (e) {
setState(() {
isLoading = false;
isError = true;
});
}
}
Future<void> createData(String name, String date, String timeIn,
String timeOut, String status, String note) async {
try {
await http.post(
Uri.parse(apiUrl),
headers: {"Content-Type": "application/json"},
body: json.encode({
"name": name,
"date": date,
"timeIn": timeIn,
"timeOut": timeOut,
"status": status,
"note": note
}),
);
fetchData();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Data absensi berhasil ditambahkan!'),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Gagal menambahkan data absensi!'),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
),
);
}
}
Future<void> updateData(String id, String name, String date, String timeIn,
String timeOut, String status, String note) async {
try {
await http.put(
Uri.parse("$apiUrl/$id"),
headers: {"Content-Type": "application/json"},
body: json.encode({
"name": name,
"date": date,
"timeIn": timeIn,
"timeOut": timeOut,
"status": status,
"note": note
}),
);
fetchData();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Data absensi berhasil diperbarui!'),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Gagal memperbarui data absensi!'),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
),
);
}
}
Future<void> deleteData(String id) async {
try {
await http.delete(Uri.parse("$apiUrl/$id"));
fetchData();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Data absensi berhasil dihapus!'),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Gagal menghapus data absensi!'),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
),
);
}
}
void showForm({String? id, Map<String, dynamic>? item}) {
if (item != null) {
nameController.text = item["name"] ?? "";
dateController.text = item["date"] ?? "";
timeInController.text = item["timeIn"] ?? "";
timeOutController.text = item["timeOut"] ?? "";
noteController.text = item["note"] ?? "";
selectedStatus = item["status"] ?? "Hadir";
} else {
nameController.clear();
dateController.clear();
timeInController.clear();
timeOutController.clear();
noteController.clear();
selectedStatus = "Hadir";
}
showDialog(
context: context,
builder: (context) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
elevation: 10,
child: SingleChildScrollView(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
id == null ? "Tambah Data Absensi" : "Edit Data Absensi",
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
const SizedBox(height: 20),
TextField(
controller: nameController,
decoration: const InputDecoration(
labelText: "Nama",
prefixIcon: Icon(Icons.person),
),
),
const SizedBox(height: 16),
TextField(
controller: dateController,
decoration: const InputDecoration(
labelText: "Tanggal (YYYY-MM-DD)",
prefixIcon: Icon(Icons.calendar_today),
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextField(
controller: timeInController,
decoration: const InputDecoration(
labelText: "Jam Masuk",
prefixIcon: Icon(Icons.access_time),
),
),
),
const SizedBox(width: 10),
Expanded(
child: TextField(
controller: timeOutController,
decoration: const InputDecoration(
labelText: "Jam Keluar",
prefixIcon: Icon(Icons.access_time),
),
),
),
],
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
value: selectedStatus,
decoration: const InputDecoration(
labelText: "Status",
prefixIcon: Icon(Icons.circle),
),
items: ["Hadir", "Izin", "Sakit", "Alpha", "Cuti"]
.map((status) => DropdownMenuItem(
value: status,
child: Text(status),
))
.toList(),
onChanged: (value) {
setState(() {
selectedStatus = value!;
});
},
),
const SizedBox(height: 16),
TextField(
controller: noteController,
maxLines: 3,
decoration: const InputDecoration(
labelText: "Catatan",
prefixIcon: Icon(Icons.note),
),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Batal"),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: () {
if (id == null) {
createData(
nameController.text,
dateController.text,
timeInController.text,
timeOutController.text,
selectedStatus,
noteController.text
);
} else {
updateData(
id,
nameController.text,
dateController.text,
timeInController.text,
timeOutController.text,
selectedStatus,
noteController.text
);
}
Navigator.pop(context);
},
child: const Text("Simpan"),
),
],
),
],
),
),
),
);
}
void _confirmDelete(String id, String name) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("Konfirmasi Hapus"),
content: Text("Apakah Anda yakin ingin menghapus data absensi $name?"),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Batal"),
),
ElevatedButton(
onPressed: () {
deleteData(id);
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text("Hapus"),
),
],
),
);
}
Color _getStatusColor(String status) {
switch (status.toLowerCase()) {
case "hadir":
return Colors.green;
case "izin":
return Colors.orange;
case "sakit":
return Colors.yellow;
case "cuti":
return Colors.blue;
case "alpha":
return Colors.red;
default:
return Colors.grey;
}
}
IconData _getStatusIcon(String status) {
switch (status.toLowerCase()) {
case "hadir":
return Icons.check_circle;
case "izin":
return Icons.info;
case "sakit":
return Icons.medical_services;
case "cuti":
return Icons.beach_access;
case "alpha":
return Icons.cancel;
default:
return Icons.help;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Manajemen Absensi"),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
elevation: 5,
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: fetchData,
),
],
),
body: isLoading
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
),
SizedBox(height: 16),
Text('Memuat data absensi...'),
],
),
)
: isError
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: Colors.red,
),
const SizedBox(height: 16),
const Text(
'Gagal memuat data absensi',
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: fetchData,
child: const Text('Coba Lagi'),
),
],
),
)
: items.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.calendar_today,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
const Text(
'Tidak ada data absensi',
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 8),
const Text('Klik tombol + untuk menambahkan data absensi'),
],
),
)
: FadeTransition(
opacity: _fadeAnimation,
child: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return Dismissible(
key: Key(item["id"].toString()),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
child: const Icon(
Icons.delete,
color: Colors.white,
),
),
confirmDismiss: (direction) async {
return await showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("Konfirmasi"),
content: Text("Hapus data absensi ${item["name"]}?"),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text("Batal"),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text("Hapus"),
),
],
),
);
},
onDismissed: (direction) {
deleteData(item["id"].toString());
},
child: Card(
margin: const EdgeInsets.symmetric(
horizontal: 16, vertical: 8),
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
item["name"] ?? "Tidak Ada Nama",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: _getStatusColor(item["status"] ?? "Hadir"),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_getStatusIcon(item["status"] ?? "Hadir"),
color: Colors.white,
size: 16,
),
const SizedBox(width: 4),
Text(
item["status"] ?? "Hadir",
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
),
),
],
),
const SizedBox(height: 12),
Text(
item["date"] ?? "Tidak Ada Tanggal",
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
),
const SizedBox(height: 12),
Row(
children: [
_buildTimeCard("MASUK", item["timeIn"] ?? "--:--", Icons.login),
const SizedBox(width: 12),
_buildTimeCard("KELUAR", item["timeOut"] ?? "--:--", Icons.logout),
],
),
if (item["note"] != null && item["note"].toString().isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 12),
const Text(
"Catatan:",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
const SizedBox(height: 4),
Text(
item["note"] ?? "",
style: TextStyle(
color: Colors.grey[700],
fontStyle: FontStyle.italic,
),
),
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
icon: const Icon(Icons.edit, color: Colors.blue),
onPressed: () => showForm(
id: item["id"].toString(),
item: item,
),
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _confirmDelete(
item["id"].toString(), item["name"]),
),
],
),
],
),
),
),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => showForm(),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
child: const Icon(Icons.add),
),
);
}
Widget _buildTimeCard(String label, String time, IconData icon) {
return Expanded(
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey),
),
child: Column(
children: [
Icon(icon, size: 20, color: Colors.blue),
const SizedBox(height: 4),
Text(
label,
style: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.grey,
),
),
const SizedBox(height: 4),
Text(
time,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
}
Komentar
Posting Komentar