MIF_E31221305/TA_API/app/Http/Controllers/Api/BookingController.php

1092 lines
47 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Models\Booking;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
class BookingController extends BaseController
{
/**
* Create a new booking
*/
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'tailor_id' => 'required|exists:users,id',
'appointment_date' => 'required|date|after_or_equal:today',
'appointment_time' => 'required|date_format:H:i',
'service_type' => 'required|in:Perbaikan,Jahit Baru',
'category' => 'required|in:Atasan,Bawahan,Terusan',
'design_photo' => 'required|image|max:2048', // max 2MB
'notes' => 'nullable|string',
'payment_method' => 'required|string|max:50'
]);
if ($validator->fails()) {
return $this->sendError('Error validasi.', $validator->errors(), 422);
}
try {
// Check if tailor exists and is actually a tailor
$tailor = User::findOrFail($request->tailor_id);
if (!$tailor->isPenjahit()) {
return $this->sendError('Error validasi.', ['tailor_id' => 'ID yang dipilih bukan penjahit'], 422);
}
// Convert appointment time to Carbon instance
$requestedTime = Carbon::parse($request->appointment_time);
$requestedDate = Carbon::parse($request->appointment_date);
// Calculate time slot (2 hours)
$slotStart = $requestedTime->copy();
$slotEnd = $requestedTime->copy()->addHours(2);
// Check if there is any booking that overlaps with this time slot
$existingBooking = Booking::where('tailor_id', $request->tailor_id)
->where('appointment_date', $request->appointment_date)
->where('status', '!=', 'dibatalkan')
->where(function($query) use ($slotStart, $slotEnd) {
$query->where(function($q) use ($slotStart, $slotEnd) {
// Check if existing booking's time slot overlaps with requested slot
$q->where(function($q) use ($slotStart, $slotEnd) {
$q->whereRaw('TIME(appointment_time) BETWEEN ? AND ?', [
$slotStart->format('H:i:s'),
$slotEnd->format('H:i:s')
]);
})
->orWhere(function($q) use ($slotStart, $slotEnd) {
$q->whereRaw('TIME(DATE_ADD(appointment_time, INTERVAL 2 HOUR)) BETWEEN ? AND ?', [
$slotStart->format('H:i:s'),
$slotEnd->format('H:i:s')
]);
});
});
})
->first();
if ($existingBooking) {
return $this->sendError('Error validasi.', [
'appointment' => 'Jadwal tidak tersedia. Penjahit sudah memiliki booking pada waktu tersebut (durasi 2 jam)'
], 422);
}
$booking = new Booking($request->all());
$booking->customer_id = Auth::id();
$booking->status = 'reservasi';
$booking->payment_status = 'unpaid';
$booking->payment_method = $request->payment_method;
// Generate transaction code
$booking->transaction_code = $this->generateTransactionCode();
// Handle design photo upload
if ($request->hasFile('design_photo')) {
$path = $request->file('design_photo')->store('design_photos', 'public');
$booking->design_photo = $path;
}
$booking->save();
// Load relationships
$booking->load(['customer', 'tailor']);
return $this->sendResponse($booking, 'Booking berhasil dibuat.');
} catch (\Exception $e) {
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat membuat booking'], 500);
}
}
/**
* Get customer's bookings with filter
*/
public function customerBookings(Request $request)
{
try {
$query = Booking::where('customer_id', Auth::id())
->with(['tailor', 'ratings']);
// Filter by status if provided
if ($request->has('status')) {
$query->where('status', $request->status);
}
$bookings = $query->orderBy('created_at', 'desc')->get();
// Transform the bookings to add completion date information
$transformedBookings = $bookings->map(function ($booking) {
$data = $booking->toArray();
// Add formatted completed_at date if booking is completed
if ($booking->status === 'selesai' && $booking->completed_at) {
$data['completed_at_formatted'] = Carbon::parse($booking->completed_at)->format('Y-m-d');
}
// Add rating information if exists
if ($booking->ratings->count() > 0) {
$rating = $booking->ratings->first();
$data['rating'] = [
'id' => $rating->id,
'rating' => $rating->rating,
'review' => $rating->review,
'created_at' => $rating->created_at,
'updated_at' => $rating->updated_at
];
} else {
$data['rating'] = null;
}
// Pastikan transaction_code dan payment_method selalu tersedia di respons
$data['transaction_code'] = $booking->transaction_code;
$data['payment_method'] = $booking->payment_method;
return $data;
});
return $this->sendResponse($transformedBookings, 'Data booking berhasil diambil.');
} catch (\Exception $e) {
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengambil data booking'], 500);
}
}
/**
* Get tailor's bookings with filter
*/
public function tailorBookings(Request $request)
{
try {
$query = Booking::where('tailor_id', Auth::id())
->with(['customer', 'ratings']);
// Filter by status if provided
if ($request->has('status')) {
$query->where('status', $request->status);
}
$bookings = $query->orderBy('created_at', 'desc')->get();
// Transform bookings to include rating information
$transformedBookings = $bookings->map(function ($booking) {
$data = $booking->toArray();
// Add rating information if exists
if ($booking->ratings->count() > 0) {
$rating = $booking->ratings->first();
$data['rating'] = [
'id' => $rating->id,
'rating' => $rating->rating,
'review' => $rating->review,
'created_at' => $rating->created_at,
'updated_at' => $rating->updated_at
];
} else {
$data['rating'] = null;
}
// Pastikan transaction_code dan payment_method selalu tersedia di respons
$data['transaction_code'] = $booking->transaction_code;
$data['payment_method'] = $booking->payment_method;
return $data;
});
return $this->sendResponse($transformedBookings, 'Data booking berhasil diambil.');
} catch (\Exception $e) {
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengambil data booking'], 500);
}
}
/**
* Update booking status
*/
public function updateStatus(Request $request, Booking $booking)
{
$validator = Validator::make($request->all(), [
'status' => 'required|in:diproses,selesai,dibatalkan',
'total_price' => 'required_if:status,diproses|numeric|min:0',
'completion_date' => 'required_if:status,diproses|date|after_or_equal:today'
]);
if ($validator->fails()) {
return $this->sendError('Error validasi.', $validator->errors(), 422);
}
try {
// Check if the authenticated user is the tailor of this booking
if ($booking->tailor_id !== Auth::id()) {
return $this->sendError('Unauthorized.', ['error' => 'Anda tidak memiliki akses untuk mengubah status booking ini'], 403);
}
// Validate status transition
if ($request->status === 'diproses' && !$booking->canBeProcessed()) {
return $this->sendError('Error validasi.', ['status' => 'Booking tidak dapat diproses. Pastikan pembayaran sudah dilakukan.'], 422);
}
if ($request->status === 'selesai' && !$booking->canBeCompleted()) {
return $this->sendError('Error validasi.', ['status' => 'Booking tidak dapat diselesaikan. Status harus diproses terlebih dahulu.'], 422);
}
$booking->status = $request->status;
if ($request->status === 'diproses') {
$booking->total_price = $request->total_price;
$booking->completion_date = $request->completion_date;
}
$booking->save();
return $this->sendResponse($booking, 'Status booking berhasil diupdate.');
} catch (\Exception $e) {
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengupdate status booking'], 500);
}
}
/**
* Cancel booking (for customer)
*/
public function cancelBooking(Booking $booking)
{
try {
// Check if the authenticated user is the customer of this booking
if ($booking->customer_id !== Auth::id()) {
return $this->sendError('Unauthorized.', ['error' => 'Anda tidak memiliki akses untuk membatalkan booking ini'], 403);
}
// Check if booking can be cancelled
if (!$booking->canBeCancelled()) {
return $this->sendError('Error validasi.', ['status' => 'Booking tidak dapat dibatalkan karena sudah diproses'], 422);
}
$booking->status = 'dibatalkan';
$booking->save();
return $this->sendResponse($booking, 'Booking berhasil dibatalkan.');
} catch (\Exception $e) {
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat membatalkan booking'], 500);
}
}
/**
* Update payment status
*/
public function updatePaymentStatus(Request $request, Booking $booking)
{
$validator = Validator::make($request->all(), [
'payment_status' => 'required|in:paid,success'
]);
if ($validator->fails()) {
return $this->sendError('Error validasi.', $validator->errors(), 422);
}
try {
// Periksa apakah pengguna yang terautentikasi adalah penjahit dari booking ini
if ($booking->tailor_id !== Auth::id()) {
return $this->sendError('Unauthorized.', ['error' => 'Anda tidak memiliki akses untuk mengubah status pembayaran ini'], 403);
}
// Periksa apakah status booking valid untuk perubahan status pembayaran
if (!in_array($booking->status, ['reservasi', 'diproses', 'selesai'])) {
return $this->sendError('Invalid status', ['error' => 'Status pembayaran hanya dapat diubah untuk booking yang aktif'], 422);
}
// Tentukan status pembayaran berdasarkan metode pembayaran
if ($booking->payment_method === 'COD') {
$booking->payment_status = 'success';
} else {
$booking->payment_status = 'paid';
}
$booking->save();
// Memberikan informasi lebih detail pada respons
return $this->sendResponse([
'booking' => [
'id' => $booking->id,
'customer_name' => $booking->customer->name,
'payment_status' => $booking->payment_status,
'payment_method' => $booking->payment_method,
'status' => $booking->status,
'total_price' => $booking->total_price,
'appointment_date' => $booking->appointment_date
]
], 'Status pembayaran berhasil diubah menjadi ' . $booking->payment_status . ' oleh penjahit.');
} catch (\Exception $e) {
\Log::error('Error in updatePaymentStatus: ' . $e->getMessage(), [
'booking_id' => $booking->id,
'tailor_id' => Auth::id(),
'exception' => $e
]);
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengupdate status pembayaran'], 500);
}
}
/**
* Tailor accepts booking and changes status to "diproses"
*/
public function acceptBooking(Booking $booking)
{
try {
// Check if user is the tailor
if ($booking->tailor_id !== Auth::id()) {
return $this->sendError('Unauthorized.', ['error' => 'Anda tidak memiliki akses untuk menerima pesanan ini'], 403);
}
// Debug: Log current booking status
\Log::info('Current booking status: ' . $booking->status);
// Check if booking can be processed
if (!in_array($booking->status, ['pending', 'reservasi'])) {
return $this->sendError('Invalid status', ['error' => 'Pesanan tidak dapat diproses. Status saat ini: ' . $booking->status], 422);
}
// Update booking status to diproses
$booking->status = 'diproses';
$booking->accepted_at = now();
$booking->save();
// Load customer relationship for response with correct column name
$booking->load(['customer:id,name,email,phone_number']);
return $this->sendResponse([
'booking' => [
'id' => $booking->id,
'customer_name' => $booking->customer->name,
'customer_phone' => $booking->customer->phone_number,
'service_type' => $booking->service_type,
'category' => $booking->category,
'appointment_date' => Carbon::parse($booking->appointment_date)->format('Y-m-d'),
'appointment_time' => $booking->appointment_time,
'design_photo' => $booking->design_photo,
'notes' => $booking->notes,
'status' => $booking->status,
'accepted_at' => Carbon::parse($booking->accepted_at)->format('Y-m-d H:i:s')
]
], 'Pesanan berhasil diterima dan diproses');
} catch (\Exception $e) {
\Log::error('Error in acceptBooking: ' . $e->getMessage());
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat memproses pesanan'], 500);
}
}
/**
* Reject booking by tailor
*/
public function rejectBooking(Request $request, Booking $booking)
{
try {
// Validate request
$validator = Validator::make($request->all(), [
'rejection_reason' => 'required|string|max:255'
]);
if ($validator->fails()) {
return $this->sendError('Error validasi.', $validator->errors(), 422);
}
// Check if the authenticated user is the tailor assigned to this booking
if ($booking->tailor_id !== Auth::id()) {
return $this->sendError('Unauthorized.', ['error' => 'Anda tidak memiliki akses untuk menolak booking ini'], 403);
}
// Check if booking is in reservasi status
if ($booking->status !== 'reservasi') {
return $this->sendError('Error validasi.', ['error' => 'Booking ini tidak dapat ditolak karena status bukan reservasi'], 422);
}
$booking->update([
'status' => 'dibatalkan',
'rejection_reason' => $request->rejection_reason,
'rejected_at' => now()
]);
// Load the customer relationship
$booking->load('customer');
// TODO: Send notification to customer that booking has been rejected
return $this->sendResponse($booking, 'Booking telah ditolak');
} catch (\Exception $e) {
// Log error detail untuk keperluan debugging
\Log::error('Error in rejectBooking: ' . $e->getMessage(), [
'booking_id' => $booking->id,
'exception' => $e
]);
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat menolak booking'], 500);
}
}
/**
* Get booking details
*/
public function show(Booking $booking)
{
try {
// Check if user is authorized to view this booking
if ($booking->customer_id !== Auth::id() && $booking->tailor_id !== Auth::id()) {
return $this->sendError('Unauthorized.', ['error' => 'Anda tidak memiliki akses ke pesanan ini'], 403);
}
// Load relationships
$booking->load(['customer:id,name,phone_number', 'tailor:id,name,profile_photo']);
$response = [
'id' => $booking->id,
'transaction_code' => $booking->transaction_code,
'customer' => [
'name' => $booking->customer->name,
'phone_number' => $booking->customer->phone_number
],
'tailor' => [
'name' => $booking->tailor->name,
'profile_photo' => $booking->tailor->profile_photo
],
'appointment_date' => Carbon::parse($booking->appointment_date)->format('Y-m-d'),
'appointment_time' => $booking->appointment_time,
'service_type' => $booking->service_type,
'category' => $booking->category,
'status' => $booking->status,
'total_price' => $booking->total_price,
'completion_date' => $booking->completion_date ? Carbon::parse($booking->completion_date)->format('Y-m-d') : null,
'payment_status' => $booking->payment_status,
'payment_method' => $booking->payment_method,
'notes' => $booking->notes,
'measurements' => $booking->measurements,
'repair_details' => $booking->repair_details,
'repair_photo' => $booking->repair_photo,
'repair_notes' => $booking->repair_notes,
'completion_photo' => $booking->completion_photo,
'completion_notes' => $booking->completion_notes,
'pickup_date' => $booking->pickup_date ? Carbon::parse($booking->pickup_date)->format('Y-m-d') : null
];
return $this->sendResponse($response, 'Detail pesanan berhasil diambil');
} catch (\Exception $e) {
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengambil detail pesanan'], 500);
}
}
/**
* Update measurements for the booking
*/
public function updateMeasurements(Request $request, Booking $booking)
{
try {
// Check if the authenticated user is the tailor assigned to this booking
if ($booking->tailor_id !== Auth::id()) {
return $this->sendError('Unauthorized.', ['error' => 'Anda tidak memiliki akses untuk mengupdate ukuran pesanan ini'], 403);
}
// Check if booking is in correct status
if ($booking->status !== 'diproses') {
return $this->sendError('Error validasi.', ['error' => 'Ukuran hanya dapat diinput setelah booking diterima dan dalam status diproses'], 422);
}
// Validate measurements based on category
$validator = Validator::make($request->all(), [
'measurements' => 'required|array',
'measurements.lingkar_pinggang' => 'required_if:category,Bawahan,Terusan|numeric',
'measurements.lingkar_pinggul' => 'required_if:category,Bawahan,Terusan|numeric',
'measurements.lingkar_paha' => 'required_if:category,Bawahan|numeric',
'measurements.lingkar_lutut' => 'required_if:category,Bawahan|numeric',
'measurements.panjang_celana' => 'required_if:category,Bawahan|numeric',
'measurements.lingkar_badan' => 'required_if:category,Atasan,Terusan|numeric',
'measurements.lingkar_dada' => 'required_if:category,Atasan,Terusan|numeric',
'measurements.panjang_lengan' => 'required_if:category,Atasan,Terusan|numeric',
'measurements.lingkar_lengan' => 'required_if:category,Atasan,Terusan|numeric',
'measurements.lingkar_pergelangan' => 'required_if:category,Atasan,Terusan|numeric',
'measurements.panjang_baju' => 'required_if:category,Atasan,Terusan|numeric',
'measurements.lebar_bahu' => 'required_if:category,Atasan,Terusan|numeric',
'measurements.panjang_rok' => 'required_if:category,Terusan|numeric',
'measurements.lingkar_pinggang_dress' => 'required_if:category,Terusan|numeric',
'measurements.lingkar_ketiak' => 'required_if:category,Terusan|numeric',
'measurements.panjang_dress' => 'required_if:category,Terusan|numeric'
]);
if ($validator->fails()) {
return $this->sendError('Error validasi.', $validator->errors(), 422);
}
// Update measurements
$booking->measurements = $request->measurements;
$booking->save();
return $this->sendResponse($booking, 'Ukuran pesanan berhasil diupdate');
} catch (\Exception $e) {
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengupdate ukuran pesanan'], 500);
}
}
/**
* Complete booking and change status to "selesai"
*/
public function completeBooking(Request $request, Booking $booking)
{
try {
// Check if user is the tailor
if ($booking->tailor_id !== Auth::id()) {
return $this->sendError('Unauthorized.', ['error' => 'Anda tidak memiliki akses untuk menyelesaikan pesanan ini'], 403);
}
// Check if booking status is diproses
if ($booking->status !== 'diproses') {
return $this->sendError('Invalid status', ['error' => 'Hanya pesanan dengan status diproses yang dapat diselesaikan'], 422);
}
// Validate request
$validator = Validator::make($request->all(), [
'completion_photo' => 'required|image|mimes:jpeg,jpg,png|max:2048',
'completion_notes' => 'nullable|string|max:500',
'total_price' => 'nullable|numeric|min:0'
]);
if ($validator->fails()) {
return $this->sendError('Error validasi.', $validator->errors(), 422);
}
// Upload completion photo
if ($request->hasFile('completion_photo')) {
$photo = $request->file('completion_photo');
// Buat direktori jika belum ada
$storagePath = storage_path('app/public/completion_photos');
if (!file_exists($storagePath)) {
mkdir($storagePath, 0755, true);
}
// Simpan file dengan nama yang unik
$filename = 'completion_' . time() . '_' . $booking->id . '.' . $photo->getClientOriginalExtension();
$photo->move($storagePath, $filename);
// Simpan path relatif ke database untuk akses via URL /storage/completion_photos/...
$booking->completion_photo = 'completion_photos/' . $filename;
}
// Update price if provided
if ($request->has('total_price')) {
$booking->total_price = $request->total_price;
}
// Update booking
$booking->status = 'selesai';
$booking->completion_notes = $request->completion_notes;
$booking->completed_at = now();
$booking->save();
// Load relationships
$booking->load(['customer:id,name,phone_number', 'tailor:id,name']);
// Tambahkan URL lengkap untuk akses gambar
$completionPhotoUrl = $booking->completion_photo ? url('storage/' . $booking->completion_photo) : null;
return $this->sendResponse([
'id' => $booking->id,
'status' => $booking->status,
'completion_photo' => $booking->completion_photo,
'completion_photo_url' => $completionPhotoUrl,
'completion_notes' => $booking->completion_notes,
'total_price' => $booking->total_price,
'completed_at' => Carbon::parse($booking->completed_at)->format('Y-m-d H:i:s'),
'customer' => [
'name' => $booking->customer->name,
'phone_number' => $booking->customer->phone_number
],
'tailor' => [
'name' => $booking->tailor->name
]
], 'Pesanan berhasil diselesaikan');
} catch (\Exception $e) {
\Log::error('Error in completeBooking: ' . $e->getMessage());
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat menyelesaikan pesanan'], 500);
}
}
/**
* Update repair details for the booking
*/
public function updateRepairDetails(Request $request, Booking $booking)
{
try {
// Check if the authenticated user is the tailor assigned to this booking
if ($booking->tailor_id !== Auth::id()) {
return $this->sendError('Unauthorized.', ['error' => 'Anda tidak memiliki akses untuk mengupdate detail perbaikan'], 403);
}
// Validate repair details
$validator = Validator::make($request->all(), [
'repair_details' => 'required|string|max:1000',
'repair_photo' => 'nullable|image|max:2048', // max 2MB
'repair_notes' => 'nullable|string|max:1000'
]);
if ($validator->fails()) {
return $this->sendError('Error validasi.', $validator->errors(), 422);
}
// Handle repair photo upload
if ($request->hasFile('repair_photo')) {
$path = $request->file('repair_photo')->store('repair_photos', 'public');
$booking->repair_photo = $path;
}
// Update repair details
$booking->repair_details = $request->repair_details;
$booking->repair_notes = $request->repair_notes;
$booking->save();
return $this->sendResponse($booking, 'Detail perbaikan berhasil diupdate');
} catch (\Exception $e) {
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengupdate detail perbaikan'], 500);
}
}
/**
* Get tailor's bookings by status
*/
public function tailorBookingsByStatus($status)
{
try {
// Validate status
if (!in_array($status, ['pending', 'diterima', 'diproses', 'selesai', 'dibatalkan', 'reservasi'])) {
return $this->sendError('Error validasi.', ['status' => 'Status tidak valid'], 422);
}
$bookings = Booking::where('tailor_id', Auth::id())
->where('status', $status)
->with(['customer'])
->orderBy('created_at', 'desc')
->get();
return $this->sendResponse($bookings, 'Data booking ' . $status . ' berhasil diambil.');
} catch (\Exception $e) {
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengambil data booking'], 500);
}
}
/**
* Simplified booking creation from tailor detail page
*/
public function bookTailor(Request $request, $tailorId)
{
try {
// Set timezone to Asia/Jakarta
date_default_timezone_set('Asia/Jakarta');
// Get today's date in Asia/Jakarta timezone
$today = Carbon::now('Asia/Jakarta')->startOfDay();
// Validate request
$validator = Validator::make($request->all(), [
'appointment_date' => ['required', 'date', function ($attribute, $value, $fail) use ($today) {
$appointmentDate = Carbon::parse($value)->startOfDay();
if ($appointmentDate->lt($today)) {
$fail('Tanggal booking harus hari ini atau setelah hari ini.');
}
}],
'appointment_time' => 'required|date_format:H:i',
'service_type' => 'required|in:Perbaikan,Jahit Baru',
'category' => 'required|in:Atasan,Bawahan,Terusan',
'design_photo' => 'required|image|mimes:jpeg,png,jpg|max:2048',
'notes' => 'nullable|string',
'payment_method' => 'required|string|max:50'
]);
if ($validator->fails()) {
return $this->sendError('Error validasi.', $validator->errors(), 422);
}
// Check if tailor exists and is a valid tailor
$tailor = User::where('id', $tailorId)
->where('role', 'penjahit')
->first();
if (!$tailor) {
return $this->sendError('Not found.', ['error' => 'Penjahit tidak ditemukan'], 404);
}
// Convert appointment time to Carbon instance
$requestedTime = Carbon::parse($request->appointment_time);
$requestedDate = Carbon::parse($request->appointment_date);
// Calculate time slot (2 hours)
$slotStart = $requestedTime->copy();
$slotEnd = $requestedTime->copy()->addHours(2);
// Check if there is any booking that overlaps with this time slot
$existingBooking = Booking::where('tailor_id', $tailorId)
->where('appointment_date', $request->appointment_date)
->where('status', '!=', 'dibatalkan')
->where(function($query) use ($slotStart, $slotEnd) {
$query->where(function($q) use ($slotStart, $slotEnd) {
// Check if existing booking's time slot overlaps with requested slot
$q->where(function($q) use ($slotStart, $slotEnd) {
$q->whereRaw('TIME(appointment_time) BETWEEN ? AND ?', [
$slotStart->format('H:i:s'),
$slotEnd->format('H:i:s')
]);
})
->orWhere(function($q) use ($slotStart, $slotEnd) {
$q->whereRaw('TIME(DATE_ADD(appointment_time, INTERVAL 2 HOUR)) BETWEEN ? AND ?', [
$slotStart->format('H:i:s'),
$slotEnd->format('H:i:s')
]);
});
});
})
->first();
if ($existingBooking) {
return $this->sendError('Error validasi.', [
'appointment' => 'Jadwal tidak tersedia. Penjahit sudah memiliki booking pada waktu tersebut (durasi 2 jam)'
], 422);
}
// Create booking
$booking = new Booking();
$booking->tailor_id = $tailorId;
$booking->customer_id = Auth::id();
$booking->appointment_date = $request->appointment_date;
$booking->appointment_time = $request->appointment_time;
$booking->service_type = $request->service_type;
$booking->category = $request->category;
$booking->notes = $request->notes;
$booking->status = 'reservasi';
$booking->payment_status = 'unpaid';
$booking->payment_method = $request->payment_method;
// Generate transaction code
$booking->transaction_code = $this->generateTransactionCode();
// Handle design photo if provided
if ($request->hasFile('design_photo')) {
$path = $request->file('design_photo')->store('design_photos', 'public');
$booking->design_photo = $path;
}
$booking->save();
// Load relationships
$booking->load(['customer', 'tailor']);
return $this->sendResponse([
'booking' => $booking,
'message' => 'Booking berhasil dibuat',
'next_steps' => [
'payment' => 'Silakan lakukan pembayaran untuk mengkonfirmasi booking',
'cancellation' => 'Anda dapat membatalkan booking sebelum melakukan pembayaran'
]
], 'Booking berhasil dibuat');
} catch (\Exception $e) {
\Log::error('Error creating booking: ' . $e->getMessage());
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat membuat booking: ' . $e->getMessage()], 500);
}
}
/**
* Get customer bookings by status
*/
public function customerBookingsByStatus($status)
{
try {
$allowedStatuses = ['reservasi', 'dikonfirmasi', 'diproses', 'selesai', 'dibatalkan'];
if (!in_array($status, $allowedStatuses)) {
return $this->sendError('Invalid status', ['error' => 'Status tidak valid'], 422);
}
$bookings = Booking::where('customer_id', Auth::id())
->where('status', $status)
->with(['tailor:id,name,profile_photo', 'ratings'])
->orderBy('created_at', 'desc')
->get()
->map(function ($booking) use ($status) {
$data = [
'id' => $booking->id,
'transaction_code' => $booking->transaction_code,
'tailor_name' => $booking->tailor->name,
'tailor_photo' => $booking->tailor->profile_photo,
'payment_method' => $booking->payment_method,
'payment_status' => $booking->payment_status
];
if ($status === 'selesai') {
$data = array_merge($data, [
'service_type' => $booking->service_type,
'category' => $booking->category,
'appointment_date' => Carbon::parse($booking->appointment_date)->format('Y-m-d'),
'completed_at' => $booking->completed_at ? Carbon::parse($booking->completed_at)->format('Y-m-d') : null,
'total_price' => $booking->total_price ?? 0,
'has_rating' => $booking->ratings->count() > 0,
'rating_id' => $booking->ratings->first() ? $booking->ratings->first()->id : null
]);
} else if ($status === 'diproses') {
$data = array_merge($data, [
'service_type' => $booking->service_type,
'appointment_date' => Carbon::parse($booking->appointment_date)->format('Y-m-d'),
'completed_at' => $booking->completed_at ? Carbon::parse($booking->completed_at)->format('Y-m-d') : null,
'total_price' => $booking->total_price ?? 0,
'design_photo' => $booking->design_photo ? '/storage/' . $booking->design_photo : null,
'repair_photo' => $booking->repair_photo ? '/storage/' . $booking->repair_photo : null,
'repair_details' => $booking->repair_details,
'repair_notes' => $booking->repair_notes,
'category' => $booking->category
]);
} else if (in_array($status, ['reservasi', 'dikonfirmasi'])) {
$data = array_merge($data, [
'service_type' => $booking->service_type,
'category' => $booking->category,
'appointment_date' => Carbon::parse($booking->appointment_date)->format('Y-m-d'),
'appointment_time' => $booking->appointment_time,
'design_photo' => $booking->design_photo ? '/storage/' . $booking->design_photo : null,
'notes' => $booking->notes,
'status' => $status === 'reservasi' ? 'Menunggu Konfirmasi' : 'Dikonfirmasi',
'status_code' => $status
]);
}
return $data;
});
return $this->sendResponse([
'bookings' => $bookings,
'total' => $bookings->count()
], 'Daftar pesanan berhasil diambil');
} catch (\Exception $e) {
\Log::error('Error getting bookings: ' . $e->getMessage());
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengambil data pesanan'], 500);
}
}
/**
* Get completed booking details with measurements
*/
public function getCompletedBookingDetails(Booking $booking)
{
try {
// Check if user is authorized to view this booking
if ($booking->customer_id !== Auth::id()) {
return $this->sendError('Unauthorized.', ['error' => 'Anda tidak memiliki akses ke pesanan ini'], 403);
}
// Check if booking is completed
if ($booking->status !== 'selesai') {
return $this->sendError('Invalid status', ['error' => 'Pesanan belum selesai'], 422);
}
// Load relationships and measurements
$booking->load(['tailor:id,name,profile_photo']);
$response = [
'id' => $booking->id,
'transaction_code' => $booking->transaction_code,
'tailor_name' => $booking->tailor->name,
'tailor_photo' => $booking->tailor->profile_photo,
'service_type' => $booking->service_type,
'category' => $booking->category,
'appointment_date' => Carbon::parse($booking->appointment_date)->format('Y-m-d'),
'completed_at' => $booking->completed_at ? Carbon::parse($booking->completed_at)->format('Y-m-d') : null,
'total_price' => $booking->total_price ?? 0,
'payment_status' => $booking->payment_status,
'payment_method' => $booking->payment_method,
'measurements' => $booking->measurements ?? [],
'completion_photo' => $booking->completion_photo,
'completion_notes' => $booking->completion_notes,
'pickup_date' => $booking->pickup_date
];
return $this->sendResponse($response, 'Detail pesanan selesai berhasil diambil');
} catch (\Exception $e) {
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengambil detail pesanan'], 500);
}
}
/**
* Process final payment and set pickup date for completed booking
*/
public function processCompletionPayment(Request $request, Booking $booking)
{
try {
// Add debugging
\Log::info('Attempting completion payment', [
'booking_id' => $booking->id,
'status' => $booking->status,
'payment_status' => $booking->payment_status,
'tailor_id' => $booking->tailor_id,
'auth_id' => Auth::id()
]);
// Check if user is authorized (penjahit)
if ($booking->tailor_id !== Auth::id()) {
\Log::warning('Unauthorized payment attempt', [
'booking_id' => $booking->id,
'tailor_id' => $booking->tailor_id,
'auth_id' => Auth::id()
]);
return $this->sendError('Unauthorized.', ['error' => 'Anda tidak memiliki akses untuk menandai pembayaran selesai'], 403);
}
// Validate booking status
if ($booking->status !== 'selesai') {
\Log::warning('Invalid booking status for payment', [
'booking_id' => $booking->id,
'status' => $booking->status
]);
return $this->sendError('Invalid status', ['error' => 'Pembayaran hanya dapat dilakukan untuk pesanan yang sudah selesai'], 422);
}
// Check if already paid
if ($booking->payment_status === 'paid' || $booking->payment_status === 'success') {
\Log::warning('Booking already paid', ['booking_id' => $booking->id]);
return $this->sendError('Already paid', ['error' => 'Pesanan ini sudah dibayar'], 422);
}
// Validate request
$validator = Validator::make($request->all(), [
'pickup_date' => 'required|date|after_or_equal:today'
]);
if ($validator->fails()) {
return $this->sendError('Error validasi.', $validator->errors(), 422);
}
// Update booking with payment and pickup date
if ($booking->payment_method === 'COD') {
$booking->payment_status = 'success';
} else {
$booking->payment_status = 'paid';
}
$booking->pickup_date = $request->pickup_date;
$booking->save();
// Load customer for response
$booking->load('customer:id,name,phone_number');
return $this->sendResponse([
'booking' => [
'id' => $booking->id,
'customer_name' => $booking->customer->name,
'customer_phone' => $booking->customer->phone_number,
'payment_status' => $booking->payment_status,
'payment_method' => $booking->payment_method,
'status' => $booking->status,
'pickup_date' => Carbon::parse($booking->pickup_date)->format('Y-m-d')
],
'message' => 'Pembayaran telah dikonfirmasi oleh penjahit',
'pickup_info' => [
'date' => Carbon::parse($booking->pickup_date)->format('Y-m-d'),
'status' => 'Sudah dibayar dengan status ' . $booking->payment_status
]
], 'Pembayaran telah dikonfirmasi dan tanggal pengambilan berhasil diatur');
} catch (\Exception $e) {
\Log::error('Error processing completion payment: ' . $e->getMessage(), [
'booking_id' => $booking->id,
'exception' => $e
]);
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat memproses pembayaran'], 500);
}
}
/**
* Get all bookings (Admin only)
*
* @param Request $request
* @return JsonResponse
*/
public function getAllBookings(Request $request): JsonResponse
{
$status = $request->query('status');
$perPage = $request->query('per_page', 10);
$query = Booking::with(['customer', 'tailor'])
->orderBy('created_at', 'desc');
if ($status) {
$query->where('status', $status);
}
$bookings = $query->paginate($perPage);
return response()->json([
'status' => 'success',
'message' => 'Bookings retrieved successfully',
'data' => $bookings
]);
}
/**
* Update booking price and completion date
*
* @param Request $request
* @param Booking $booking
* @return JsonResponse
*/
public function updatePrice(Request $request, Booking $booking): JsonResponse
{
try {
// Check if the authenticated user is the tailor of this booking
if ($booking->tailor_id !== Auth::id()) {
return $this->sendError('Unauthorized.', ['error' => 'Anda tidak memiliki akses untuk mengubah harga booking ini'], 403);
}
// Validate request
$validator = Validator::make($request->all(), [
'total_price' => 'required|numeric|min:0',
'completion_date' => 'required|date|after_or_equal:today'
]);
if ($validator->fails()) {
return $this->sendError('Error validasi.', $validator->errors(), 422);
}
// Update price and completion date
$booking->total_price = $request->total_price;
$booking->completion_date = $request->completion_date;
$booking->save();
return $this->sendResponse([
'id' => $booking->id,
'total_price' => $booking->total_price,
'completion_date' => Carbon::parse($booking->completion_date)->format('Y-m-d'),
'status' => $booking->status,
'customer' => [
'name' => $booking->customer->name,
'phone_number' => $booking->customer->phone_number
]
], 'Harga dan tanggal selesai pesanan berhasil diupdate');
} catch (\Exception $e) {
return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengupdate harga dan tanggal selesai pesanan'], 500);
}
}
/**
* Generate unique transaction code
*
* @return string
*/
private function generateTransactionCode()
{
// Format: TRX-{YYYYMMDD}-{6 random characters}
$date = Carbon::now()->format('Ymd');
$random = strtoupper(substr(md5(uniqid()), 0, 6));
$code = "TRX-{$date}-{$random}";
// Check if code already exists, if so, regenerate
while (Booking::where('transaction_code', $code)->exists()) {
$random = strtoupper(substr(md5(uniqid()), 0, 6));
$code = "TRX-{$date}-{$random}";
}
return $code;
}
}