MIF_E31221388/salonbooking/lib/pemilik/data_layanan_page.dart

409 lines
14 KiB
Dart

// 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<String, dynamic> 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<DataLayananPage> createState() => _DataLayananPageState();
}
class _DataLayananPageState extends State<DataLayananPage> {
List categories = [];
Map<int, List<Service>> servicesByCategory = {};
int? selectedCategoryId;
bool loadingCategories = true;
bool loadingServices = false;
String? imagePath;
@override
void initState() {
super.initState();
fetchCategories();
}
Future<void> 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<void> 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<Service> services = (data as List)
.map((item) => Service.fromJson(item))
.toList();
setState(() {
servicesByCategory[categoryId] = services;
});
}
} catch (e) {
print(e);
} finally {
setState(() => loadingServices = false);
}
}
Future<void> 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<void> 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<int>(
value: selectedDialogCategoryId,
items: categories.map<DropdownMenuItem<int>>((category) {
return DropdownMenuItem<int>(
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<bool>(
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!);
}
},
),
),
);
},
),
),
],
),
);
}
}