364 lines
21 KiB
PHP
364 lines
21 KiB
PHP
@extends('admin.layouts.app')
|
|
|
|
@section('title', 'Pesanan')
|
|
|
|
@section('content')
|
|
<div class="p-6" x-data="bookingsData">
|
|
<!-- Header -->
|
|
<div class="flex flex-col md:flex-row md:justify-between md:items-center gap-4 mb-6">
|
|
<h2 class="text-2xl font-semibold text-gray-800">Daftar Pesanan</h2>
|
|
|
|
<!-- Filter Section -->
|
|
<div class="flex flex-wrap items-center gap-3">
|
|
<!-- Search -->
|
|
<div class="relative">
|
|
<input
|
|
type="text"
|
|
x-model="searchQuery"
|
|
@input="filterBookings()"
|
|
placeholder="Cari pesanan..."
|
|
class="w-full md:w-64 pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
>
|
|
<svg class="w-5 h-5 text-gray-400 absolute left-3 top-2.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
|
</svg>
|
|
</div>
|
|
|
|
<!-- Status Filter -->
|
|
<div class="relative" x-data="{ open: false }">
|
|
<button
|
|
@click="open = !open"
|
|
class="flex items-center gap-2 px-4 py-2 border border-gray-300 rounded-lg hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
>
|
|
<span x-text="selectedStatus ? `Status: ${selectedStatus}` : 'Semua Status'"></span>
|
|
<svg class="w-5 h-5 text-gray-400" :class="{'rotate-180': open}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
</svg>
|
|
</button>
|
|
|
|
<div
|
|
x-show="open"
|
|
@click.away="open = false"
|
|
x-transition:enter="transition ease-out duration-100"
|
|
x-transition:enter-start="transform opacity-0 scale-95"
|
|
x-transition:enter-end="transform opacity-100 scale-100"
|
|
x-transition:leave="transition ease-in duration-75"
|
|
x-transition:leave-start="transform opacity-100 scale-100"
|
|
x-transition:leave-end="transform opacity-0 scale-95"
|
|
class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg z-50"
|
|
>
|
|
<div class="py-1">
|
|
<button
|
|
@click="selectedStatus = ''; filterBookings(); open = false"
|
|
class="block w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
|
:class="{'bg-blue-50 text-blue-600': selectedStatus === ''}"
|
|
>
|
|
Semua Status
|
|
</button>
|
|
<template x-for="status in ['reservasi', 'diproses', 'selesai']" :key="status">
|
|
<button
|
|
@click="selectedStatus = status; filterBookings(); open = false"
|
|
class="block w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
|
:class="{'bg-blue-50 text-blue-600': selectedStatus === status}"
|
|
x-text="status.charAt(0).toUpperCase() + status.slice(1)"
|
|
></button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bookings List -->
|
|
<div class="bg-white rounded-lg shadow overflow-hidden">
|
|
<div class="min-w-full">
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Pelanggan</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Penjahit</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tanggal</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Layanan</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Pembayaran</th>
|
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Aksi</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-200" id="bookingsTableBody">
|
|
<template x-for="booking in filteredBookings" :key="booking.id">
|
|
<tr class="hover:bg-gray-50">
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900" x-text="booking.id"></td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="flex items-center">
|
|
<div>
|
|
<div class="text-sm font-medium text-gray-900" x-text="booking.customer.name"></div>
|
|
<div class="text-sm text-gray-500" x-text="booking.customer.phone_number"></div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0 h-10 w-10">
|
|
<template x-if="booking.tailor.profile_photo">
|
|
<img :src="window.assetUrl(booking.tailor.profile_photo)" class="h-10 w-10 rounded-full object-cover">
|
|
</template>
|
|
<template x-if="!booking.tailor.profile_photo">
|
|
<div class="h-10 w-10 rounded-full bg-gray-200 flex items-center justify-center">
|
|
<span class="text-gray-500 font-medium" x-text="booking.tailor.name.charAt(0)"></span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
<div class="ml-4">
|
|
<div class="text-sm font-medium text-gray-900" x-text="booking.tailor.name"></div>
|
|
<div class="text-sm text-gray-500" x-text="booking.tailor.phone_number"></div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="text-sm text-gray-900" x-text="formatDate(booking.appointment_date)"></div>
|
|
<div class="text-sm text-gray-500" x-text="booking.appointment_time"></div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="text-sm text-gray-900" x-text="booking.service_type"></div>
|
|
<div class="text-sm text-gray-500" x-text="booking.category"></div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full"
|
|
:class="{
|
|
'bg-yellow-100 text-yellow-800': booking.status === 'reservasi',
|
|
'bg-blue-100 text-blue-800': booking.status === 'diproses',
|
|
'bg-green-100 text-green-800': booking.status === 'selesai'
|
|
}"
|
|
x-text="booking.status.charAt(0).toUpperCase() + booking.status.slice(1)">
|
|
</span>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full"
|
|
:class="{
|
|
'bg-red-100 text-red-800': booking.payment_status === 'unpaid',
|
|
'bg-green-100 text-green-800': booking.payment_status === 'paid'
|
|
}"
|
|
x-text="booking.payment_status === 'paid' ? 'Lunas' : 'Belum Lunas'">
|
|
</span>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
<button @click="showDetails(booking)" class="text-blue-600 hover:text-blue-900">Detail</button>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Detail Modal -->
|
|
<div x-show="selectedBooking"
|
|
class="fixed inset-0 bg-black bg-opacity-50 z-50 overflow-y-auto"
|
|
x-cloak>
|
|
<div class="flex items-center justify-center min-h-screen p-4">
|
|
<div class="bg-white rounded-lg w-full max-w-2xl relative shadow-xl"
|
|
@click.away="selectedBooking = null">
|
|
<!-- Header - Fixed at top -->
|
|
<div class="sticky top-0 bg-white px-4 py-3 border-b z-10">
|
|
<div class="flex justify-between items-start">
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-gray-900">Detail Pesanan #<span x-text="selectedBooking?.id"></span></h3>
|
|
<p class="mt-1 text-sm text-gray-500">Dibuat pada <span x-text="formatDate(selectedBooking?.created_at)"></span></p>
|
|
</div>
|
|
<button @click="selectedBooking = null" class="text-gray-400 hover:text-gray-500 p-1 hover:bg-gray-100 rounded-full transition-colors">
|
|
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scrollable Content -->
|
|
<div class="px-4 py-3 space-y-4">
|
|
<!-- Status Bar -->
|
|
<div class="flex items-center justify-between bg-gray-50 p-3 rounded-lg">
|
|
<div>
|
|
<span class="px-2.5 py-0.5 text-xs font-semibold rounded-full"
|
|
:class="{
|
|
'bg-yellow-100 text-yellow-800': selectedBooking?.status === 'reservasi',
|
|
'bg-blue-100 text-blue-800': selectedBooking?.status === 'diproses',
|
|
'bg-green-100 text-green-800': selectedBooking?.status === 'selesai'
|
|
}"
|
|
x-text="selectedBooking?.status.charAt(0).toUpperCase() + selectedBooking?.status.slice(1)">
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span class="px-2.5 py-0.5 text-xs font-semibold rounded-full"
|
|
:class="{
|
|
'bg-red-100 text-red-800': selectedBooking?.payment_status === 'unpaid',
|
|
'bg-green-100 text-green-800': selectedBooking?.payment_status === 'paid'
|
|
}"
|
|
x-text="selectedBooking?.payment_status === 'paid' ? 'Lunas' : 'Belum Lunas'">
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Customer Info -->
|
|
<div class="bg-white rounded-lg border border-gray-200">
|
|
<div class="px-3 py-2 border-b border-gray-200 bg-gray-50">
|
|
<h4 class="text-sm font-semibold text-gray-900">Informasi Pelanggan</h4>
|
|
</div>
|
|
<div class="p-3">
|
|
<div class="space-y-3">
|
|
<div>
|
|
<p class="text-xs font-medium text-gray-500">Nama</p>
|
|
<p class="mt-0.5 text-sm text-gray-900" x-text="selectedBooking?.customer.name"></p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-medium text-gray-500">Telepon</p>
|
|
<p class="mt-0.5 text-sm text-gray-900" x-text="selectedBooking?.customer.phone_number"></p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-medium text-gray-500">Alamat</p>
|
|
<p class="mt-0.5 text-sm text-gray-900 break-words" x-text="selectedBooking?.customer.address"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Order Details -->
|
|
<div class="bg-white rounded-lg border border-gray-200">
|
|
<div class="px-3 py-2 border-b border-gray-200 bg-gray-50">
|
|
<h4 class="text-sm font-semibold text-gray-900">Detail Pesanan</h4>
|
|
</div>
|
|
<div class="p-3">
|
|
<div class="space-y-3">
|
|
<div>
|
|
<p class="text-xs font-medium text-gray-500">Jenis Layanan</p>
|
|
<p class="mt-0.5 text-sm text-gray-900" x-text="selectedBooking?.service_type"></p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-medium text-gray-500">Kategori</p>
|
|
<p class="mt-0.5 text-sm text-gray-900" x-text="selectedBooking?.category"></p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-medium text-gray-500">Tanggal Appointment</p>
|
|
<p class="mt-0.5 text-sm text-gray-900" x-text="formatDate(selectedBooking?.appointment_date)"></p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-medium text-gray-500">Jam</p>
|
|
<p class="mt-0.5 text-sm text-gray-900" x-text="selectedBooking?.appointment_time"></p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-medium text-gray-500">Catatan</p>
|
|
<p class="mt-0.5 text-sm text-gray-900 break-words" x-text="selectedBooking?.notes || '-'"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Design Photo -->
|
|
<div x-show="selectedBooking?.design_photo" class="bg-white rounded-lg border border-gray-200">
|
|
<div class="px-3 py-2 border-b border-gray-200 bg-gray-50">
|
|
<h4 class="text-sm font-semibold text-gray-900">Foto Design</h4>
|
|
</div>
|
|
<div class="p-3">
|
|
<img :src="window.assetUrl('storage/' + selectedBooking?.design_photo)"
|
|
class="max-h-60 w-full rounded-lg object-contain bg-gray-50"
|
|
alt="Design Photo">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Completion Info -->
|
|
<template x-if="selectedBooking?.status === 'selesai'">
|
|
<div class="bg-white rounded-lg border border-gray-200">
|
|
<div class="px-3 py-2 border-b border-gray-200 bg-gray-50">
|
|
<h4 class="text-sm font-semibold text-gray-900">Informasi Penyelesaian</h4>
|
|
</div>
|
|
<div class="p-3 space-y-4">
|
|
<div x-show="selectedBooking?.completion_photo">
|
|
<p class="text-xs font-medium text-gray-500 mb-2">Foto Hasil</p>
|
|
<img :src="window.assetUrl('storage/' + selectedBooking?.completion_photo)"
|
|
class="max-h-60 w-full rounded-lg object-contain bg-gray-50"
|
|
alt="Completion Photo">
|
|
</div>
|
|
<div x-show="selectedBooking?.completion_notes">
|
|
<p class="text-xs font-medium text-gray-500">Catatan Penyelesaian</p>
|
|
<p class="mt-0.5 text-sm text-gray-900 break-words" x-text="selectedBooking?.completion_notes"></p>
|
|
</div>
|
|
<div x-show="selectedBooking?.pickup_date">
|
|
<p class="text-xs font-medium text-gray-500">Tanggal Pengambilan</p>
|
|
<p class="mt-0.5 text-sm text-gray-900" x-text="formatDate(selectedBooking?.pickup_date)"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endsection
|
|
|
|
@push('scripts')
|
|
<script>
|
|
document.addEventListener('alpine:init', () => {
|
|
Alpine.data('bookingsData', () => ({
|
|
bookings: [],
|
|
filteredBookings: [],
|
|
selectedBooking: null,
|
|
searchQuery: '',
|
|
selectedStatus: '',
|
|
|
|
init() {
|
|
this.fetchBookings();
|
|
},
|
|
|
|
async fetchBookings() {
|
|
try {
|
|
const token = '{{ session('api_token') }}';
|
|
const tokenType = '{{ session('token_type') }}';
|
|
|
|
const response = await fetch(window.apiUrl('bookings'), {
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'Authorization': `${tokenType} ${token}`
|
|
}
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.status === 'success') {
|
|
this.bookings = result.data.data;
|
|
this.filteredBookings = this.bookings;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching bookings:', error);
|
|
}
|
|
},
|
|
|
|
filterBookings() {
|
|
this.filteredBookings = this.bookings.filter(booking => {
|
|
const matchesSearch = this.searchQuery === '' ||
|
|
booking.customer.name.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
|
|
booking.tailor.name.toLowerCase().includes(this.searchQuery.toLowerCase());
|
|
|
|
const matchesStatus = this.selectedStatus === '' ||
|
|
booking.status === this.selectedStatus;
|
|
|
|
return matchesSearch && matchesStatus;
|
|
});
|
|
},
|
|
|
|
showDetails(booking) {
|
|
this.selectedBooking = booking;
|
|
},
|
|
|
|
formatDate(dateString) {
|
|
if (!dateString) return '-';
|
|
const options = { year: 'numeric', month: 'long', day: 'numeric' };
|
|
return new Date(dateString).toLocaleDateString('id-ID', options);
|
|
}
|
|
}));
|
|
});
|
|
</script>
|
|
@endpush
|