1092 lines
47 KiB
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;
|
|
}
|
|
}
|