// Lengkap: CRUD layanan Flutter (Tanpa Edit) import 'dart:convert'; import 'dart:io'; import 'package:http_parser/http_parser.dart'; import 'package:mime/mime.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:http/http.dart' as http; class Service { final int id; final String name; final int price; final String? imageUrl; Service({ required this.id, required this.name, required this.price, this.imageUrl, }); factory Service.fromJson(Map json) { return Service( id: json['id'], name: json['name'], price: json['price'], imageUrl: json['image'], ); } } class DataLayananPage extends StatefulWidget { final String token; const DataLayananPage({super.key, required this.token}); @override State createState() => _DataLayananPageState(); } class _DataLayananPageState extends State { List categories = []; Map> servicesByCategory = {}; int? selectedCategoryId; bool loadingCategories = true; bool loadingServices = false; String? imagePath; @override void initState() { super.initState(); fetchCategories(); } Future fetchCategories() async { setState(() => loadingCategories = true); try { final response = await http.get( Uri.parse('http://angeliasalon.my.id/api/categories'), headers: { 'Authorization': 'Bearer ${widget.token}', 'Accept': 'application/json', }, ); if (response.statusCode == 200) { final data = json.decode(response.body); setState(() { categories = data is List ? data : data['categories']; if (categories.isNotEmpty) { selectedCategoryId = categories[0]['id']; fetchServicesByCategory(selectedCategoryId!); } }); } } catch (e) { print(e); } finally { setState(() => loadingCategories = false); } } Future fetchServicesByCategory(int categoryId) async { setState(() => loadingServices = true); try { final response = await http.get( Uri.parse('http://angeliasalon.my.id/api/services?category_id=$categoryId'), headers: { 'Authorization': 'Bearer ${widget.token}', 'Accept': 'application/json', }, ); if (response.statusCode == 200) { final data = json.decode(response.body); final List services = (data as List) .map((item) => Service.fromJson(item)) .toList(); setState(() { servicesByCategory[categoryId] = services; }); } } catch (e) { print(e); } finally { setState(() => loadingServices = false); } } Future addService({ required String name, required String price, required String description, required String duration, required int categoryId, required File? imageFile, }) async { final url = Uri.parse('http://angeliasalon.my.id/api/services'); final request = http.MultipartRequest('POST', url); request.headers.addAll({ 'Authorization': 'Bearer ${widget.token}', 'Accept': 'application/json', }); request.fields['name'] = name; request.fields['category_id'] = categoryId.toString(); request.fields['price'] = price; request.fields['description'] = description; request.fields['duration'] = duration; if (imageFile != null) { final mimeType = lookupMimeType(imageFile.path); // butuh: import 'package:mime/mime.dart'; final fileStream = http.ByteStream(imageFile.openRead()); final length = await imageFile.length(); final multipartFile = http.MultipartFile( 'image', fileStream, length, filename: imageFile.path.split('/').last, contentType: MediaType.parse(mimeType ?? 'image/jpeg'), // butuh: import 'package:http_parser/http_parser.dart'; ); request.files.add(multipartFile); } final response = await request.send(); final respStr = await response.stream.bytesToString(); print("Response: $respStr"); if (response.statusCode == 201) { print("Layanan berhasil ditambahkan."); await fetchServicesByCategory(categoryId); } else { print("Gagal tambah layanan: $respStr"); } } Future deleteService(int id, int categoryId) async { final url = Uri.parse('http://angeliasalon.my.id/api/services/$id'); final response = await http.delete( url, headers: { 'Authorization': 'Bearer ${widget.token}', 'Accept': 'application/json', }, ); if (response.statusCode == 200) { print("Layanan berhasil dihapus."); await fetchServicesByCategory(categoryId); } else { print("Gagal hapus layanan: ${response.body}"); } } void showAddServiceDialog() { final nameController = TextEditingController(); final priceController = TextEditingController(); final descriptionController = TextEditingController(); final durationController = TextEditingController(); // Tambahan durasi int? selectedDialogCategoryId = selectedCategoryId; String? previewImagePath; imagePath = null; showDialog( context: context, builder: (context) => StatefulBuilder( builder: (context, setStateDialog) { return AlertDialog( title: const Text('Tambah Layanan'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ DropdownButtonFormField( value: selectedDialogCategoryId, items: categories.map>((category) { return DropdownMenuItem( value: category['id'], child: Text(category['name']), ); }).toList(), onChanged: (value) => setStateDialog(() => selectedDialogCategoryId = value), decoration: const InputDecoration(labelText: 'Kategori'), ), TextField( controller: nameController, decoration: const InputDecoration(labelText: 'Nama Layanan'), ), TextField( controller: priceController, keyboardType: TextInputType.number, decoration: const InputDecoration(labelText: 'Harga'), ), TextField( controller: descriptionController, decoration: const InputDecoration(labelText: 'Deskripsi'), ), TextField( controller: durationController, // Durasi dalam menit keyboardType: TextInputType.number, decoration: const InputDecoration(labelText: 'Durasi (menit)'), ), ElevatedButton( onPressed: () async { final picker = ImagePicker(); final pickedFile = await picker.pickImage(source: ImageSource.gallery); if (pickedFile != null) { setStateDialog(() { previewImagePath = pickedFile.path; imagePath = pickedFile.path; }); } }, child: const Text('Pilih Gambar'), ), if (previewImagePath != null) Image.file(File(previewImagePath!), height: 100), ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Batal'), ), ElevatedButton( onPressed: () async { if (nameController.text.isEmpty || priceController.text.isEmpty || descriptionController.text.isEmpty || durationController.text.isEmpty || selectedDialogCategoryId == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Semua field harus diisi')), ); return; } await addService( name: nameController.text, price: priceController.text, description: descriptionController.text, duration: durationController.text, // kirim durasi categoryId: selectedDialogCategoryId!, imageFile: imagePath != null ? File(imagePath!) : null, ); Navigator.pop(context); }, child: const Text('Simpan'), ), ], ); }, ), ); } void selectCategory(int categoryId) async { if (selectedCategoryId == categoryId) return; setState(() => selectedCategoryId = categoryId); await fetchServicesByCategory(categoryId); } @override Widget build(BuildContext context) { final Color primaryColor = const Color(0xFFF06292); // pink terang (hot pink) final Color backgroundColor = const Color(0xFFFFF6F9); // pink pucat banget final services = selectedCategoryId != null ? (servicesByCategory[selectedCategoryId!] ?? []) : []; return Scaffold( backgroundColor: backgroundColor, appBar: AppBar(title: const Text('Data Layanan'), backgroundColor: primaryColor), floatingActionButton: FloatingActionButton( backgroundColor: primaryColor, onPressed: showAddServiceDialog, child: const Icon(Icons.add), ), body: Column( children: [ SizedBox( height: 60, child: loadingCategories ? const Center(child: CircularProgressIndicator()) : ListView.builder( scrollDirection: Axis.horizontal, itemCount: categories.length, itemBuilder: (context, index) { final category = categories[index]; final isSelected = category['id'] == selectedCategoryId; return GestureDetector( onTap: () => selectCategory(category['id']), child: Container( margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 20), decoration: BoxDecoration( color: isSelected ? primaryColor : Colors.pink.shade100, borderRadius: BorderRadius.circular(20), ), child: Center( child: Text( category['name'], style: TextStyle( color: isSelected ? Colors.white : Colors.black, fontWeight: FontWeight.bold, ), ), ), ), ); }, ), ), const Divider(), Expanded( child: loadingServices ? const Center(child: CircularProgressIndicator()) : services.isEmpty ? const Center(child: Text('Belum ada layanan.')) : ListView.builder( itemCount: services.length, itemBuilder: (context, index) { final service = services[index]; return Card( margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 4, child: ListTile( contentPadding: const EdgeInsets.all(10), leading: service.imageUrl != null && service.imageUrl!.isNotEmpty ? ClipRRect( borderRadius: BorderRadius.circular(10), child: Image.network( service.imageUrl!, width: 60, height: 60, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => const Icon(Icons.broken_image, size: 60), ), ) : const Icon(Icons.image, size: 60, color: Colors.grey), title: Text(service.name, style: const TextStyle(fontWeight: FontWeight.bold)), subtitle: Text("Rp ${service.price}"), trailing: IconButton( icon: const Icon(Icons.delete, color: Colors.red), onPressed: () async { final confirm = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Hapus Layanan'), content: const Text('Apakah Anda yakin ingin menghapus layanan ini?'), actions: [ TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Batal')), ElevatedButton(onPressed: () => Navigator.pop(context, true), child: const Text('Hapus')), ], ), ); if (confirm == true) { await deleteService(service.id, selectedCategoryId!); } }, ), ), ); }, ), ), ], ), ); } }