246 lines
14 KiB
PHP
246 lines
14 KiB
PHP
@extends('layouts.app')
|
||
|
||
@section('title', 'Schedule')
|
||
@section('page-title', 'Schedule & Calendar 📅')
|
||
@section('page-subtitle', 'Tap a date or event to manage it')
|
||
|
||
@section('content')
|
||
|
||
{{-- ── Add Event Button ─────────────────────────────────────── --}}
|
||
<div class="flex items-center justify-between mb-4">
|
||
<a href="{{ route('schedule.create') }}" class="btn-sakura text-sm px-5 py-2">+ Add Event</a>
|
||
</div>
|
||
|
||
{{-- ── Upcoming Alert Strip ─────────────────────────────────── --}}
|
||
@php
|
||
$startOfMonth = now()->startOfMonth();
|
||
$endOfMonth = now()->endOfMonth();
|
||
$upcomingList = $events->filter(function($e) use ($startOfMonth, $endOfMonth) {
|
||
$start = $e->start_time;
|
||
$end = $e->end_time ?: $e->start_time;
|
||
return $start->lte($endOfMonth) && $end->gte($startOfMonth);
|
||
});
|
||
@endphp
|
||
@if($upcomingList->isNotEmpty())
|
||
<div class="flex gap-2 overflow-x-auto pb-2 mb-4 -mx-4 px-4 lg:mx-0 lg:px-0">
|
||
@foreach($upcomingList as $event)
|
||
<div class="shrink-0 flex items-center gap-2 bg-white rounded-2xl border border-pink-100 px-3 py-2 shadow-sm cursor-pointer hover:border-pink-300 transition-colors"
|
||
onclick="openAlertEventModal({{ json_encode(['id' => $event->id, 'title' => $event->title, 'type' => $event->event_type, 'status' => $event->status->value, 'client' => $event->client?->name, 'start' => $event->start_time->format('d M Y H:i'), 'end' => $event->end_time?->format('d M Y H:i'), 'location' => $event->location, 'description' => $event->description, 'notes' => $event->notes, 'has_contract' => $event->has_contract, 'has_content' => $event->has_content]) }})">
|
||
<div class="w-1.5 h-8 rounded-full shrink-0" style="background:{{ $event->color }}"></div>
|
||
<div>
|
||
<div class="text-xs font-semibold text-gray-700 whitespace-nowrap">{{ $event->title }}</div>
|
||
<div class="text-xs text-gray-400">{{ $event->start_time->format('H:i') }}</div>
|
||
</div>
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
@endif
|
||
|
||
{{-- ── Calendar ─────────────────────────────────────────────── --}}
|
||
<div class="cat-card p-3 lg:p-5">
|
||
<div id="calendar"></div>
|
||
</div>
|
||
|
||
{{-- ── Event Detail Modal ───────────────────────────────────── --}}
|
||
<div id="event-modal" class="fixed inset-0 z-50 flex items-end sm:items-center justify-center bg-black/40 hidden">
|
||
<div class="cat-card p-5 w-full sm:max-w-md rounded-b-none sm:rounded-3xl shadow-xl mx-0 sm:mx-4">
|
||
<div class="w-10 h-1 bg-gray-200 rounded-full mx-auto mb-4 sm:hidden"></div>
|
||
<div class="flex items-center justify-between mb-3">
|
||
<h3 class="font-bold text-gray-800 text-base" id="modal-title">Event</h3>
|
||
<button onclick="document.getElementById('event-modal').classList.add('hidden')"
|
||
class="text-gray-400 hover:text-gray-600 text-xl leading-none">✕</button>
|
||
</div>
|
||
<div id="modal-body" class="text-sm text-gray-600 space-y-2 mb-4"></div>
|
||
<div id="modal-actions" class="flex gap-2"></div>
|
||
</div>
|
||
</div>
|
||
|
||
@push('scripts')
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.11/index.global.min.css">
|
||
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.11/index.global.min.js"></script>
|
||
|
||
<style>
|
||
/* ── FullCalendar mobile overrides ── */
|
||
.fc .fc-toolbar { flex-wrap: wrap; gap: 8px; }
|
||
.fc .fc-toolbar-title { font-size: 1rem; font-weight: 700; }
|
||
.fc .fc-button {
|
||
background: #FFB7C5 !important;
|
||
border-color: #FFB7C5 !important;
|
||
border-radius: 999px !important;
|
||
font-size: 0.75rem !important;
|
||
padding: 4px 12px !important;
|
||
box-shadow: none !important;
|
||
}
|
||
.fc .fc-button:hover { background: #FF8FAD !important; border-color: #FF8FAD !important; }
|
||
.fc .fc-button-active { background: #FF8FAD !important; border-color: #FF8FAD !important; }
|
||
.fc .fc-daygrid-event { border-radius: 8px !important; font-size: 0.7rem; }
|
||
.fc .fc-list-event:hover td { background: #FFF0F5 !important; }
|
||
.fc .fc-list-day-cushion { background: #FFF8F0 !important; }
|
||
@media (max-width: 639px) {
|
||
.fc .fc-toolbar { gap: 6px; }
|
||
.fc .fc-toolbar-title { font-size: 0.9rem; }
|
||
.fc .fc-button { padding: 3px 8px !important; font-size: 0.7rem !important; }
|
||
}
|
||
</style>
|
||
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
const isMobile = window.innerWidth < 640;
|
||
const calendar = new FullCalendar.Calendar(document.getElementById('calendar'), {
|
||
initialView: isMobile ? 'listWeek' : 'dayGridMonth',
|
||
headerToolbar: {
|
||
left: 'prev,next today',
|
||
center: 'title',
|
||
right: isMobile ? 'listWeek,timeGridDay' : 'dayGridMonth,timeGridWeek,listWeek',
|
||
},
|
||
editable: !isMobile,
|
||
selectable: true,
|
||
events: @json($calendarEvents),
|
||
height: 'auto',
|
||
windowResize(view) {
|
||
const mob = window.innerWidth < 640;
|
||
if (mob && calendar.view.type !== 'listWeek' && calendar.view.type !== 'timeGridDay') {
|
||
calendar.changeView('listWeek');
|
||
}
|
||
},
|
||
|
||
eventDrop(info) {
|
||
fetch(`/schedule/${info.event.id}/dates`, {
|
||
method: 'PATCH',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||
},
|
||
body: JSON.stringify({
|
||
start_time: info.event.startStr ? info.event.startStr.substring(0, 19) : null,
|
||
end_time: info.event.endStr ? info.event.endStr.substring(0, 19) : null,
|
||
}),
|
||
}).then(r => r.ok ? window.KolManager.notify('Event rescheduled! 🐾') : info.revert());
|
||
},
|
||
|
||
select(info) {
|
||
// Pre-fill date from calendar click
|
||
const dateInput = document.getElementById('start_date');
|
||
if (dateInput) dateInput.value = info.startStr.substring(0, 10);
|
||
const endInput = document.getElementById('end_date');
|
||
if (endInput && info.endStr) endInput.value = info.endStr.substring(0, 10);
|
||
},
|
||
|
||
eventClick(info) {
|
||
const e = info.event, p = e.extendedProps;
|
||
document.getElementById('modal-title').textContent = e.title;
|
||
document.getElementById('modal-body').innerHTML = `
|
||
<div class="flex gap-2"><span class="text-gray-400 w-24 shrink-0">Type</span><span>${p.type}</span></div>
|
||
<div class="flex gap-2"><span class="text-gray-400 w-24 shrink-0">Status</span><span>${p.status}</span></div>
|
||
${p.client ? `<div class="flex gap-2"><span class="text-gray-400 w-24 shrink-0">Client</span><span>${p.client}</span></div>` : ''}
|
||
${p.location ? `<div class="flex gap-2"><span class="text-gray-400 w-24 shrink-0">Location</span><span>${p.location}</span></div>` : ''}
|
||
<div class="flex gap-2"><span class="text-gray-400 w-24 shrink-0">Start</span><span>${e.start?.toLocaleString()}</span></div>
|
||
${e.end ? `<div class="flex gap-2"><span class="text-gray-400 w-24 shrink-0">End</span><span>${e.end.toLocaleString()}</span></div>` : ''}
|
||
${p.description ? `<div class="flex gap-2"><span class="text-gray-400 w-24 shrink-0">Description</span><div class="flex-1 break-words whitespace-pre-wrap overflow-y-auto max-h-20 pr-1">${p.description}</div></div>` : ''}
|
||
${p.notes ? `<div class="flex gap-2"><span class="text-gray-400 w-24 shrink-0">Notes</span><div class="flex-1 break-words whitespace-pre-wrap overflow-y-auto max-h-20 pr-1">${p.notes}</div></div>` : ''}
|
||
${p.has_contract ? `<div class="flex gap-2"><span class="text-gray-400 w-24 shrink-0">Contract</span><span class="text-pink-500 font-semibold">Yes</span></div>` : ''}
|
||
${p.has_content ? `<div class="flex gap-2"><span class="text-gray-400 w-24 shrink-0">Content Bd</span><span class="text-pink-500 font-semibold">Yes</span></div>` : ''}
|
||
`;
|
||
const editUrl = `/schedule/${e.id}/edit`;
|
||
document.getElementById('modal-actions').innerHTML = `
|
||
<a href="${editUrl}" class="btn-sakura text-sm flex-1 text-center py-2">Edit</a>
|
||
<form method="POST" action="/schedule/${e.id}" onsubmit="return confirm('Delete?')" class="flex-1">
|
||
<input type="hidden" name="_method" value="DELETE">
|
||
<input type="hidden" name="_token" value="${document.querySelector('meta[name=csrf-token]').content}">
|
||
<button class="w-full py-2 rounded-full border border-red-200 text-red-400 text-sm hover:bg-red-50">Delete</button>
|
||
</form>
|
||
`;
|
||
document.getElementById('event-modal').classList.remove('hidden');
|
||
},
|
||
});
|
||
|
||
calendar.render();
|
||
|
||
// Close modal on backdrop tap
|
||
document.getElementById('event-modal').addEventListener('click', function(e) {
|
||
if (e.target === this) this.classList.add('hidden');
|
||
});
|
||
|
||
// Note: datetime hidden fields are populated in contractCheck's submitIfClear()
|
||
// since @submit.prevent is used on the form (form.submit() bypasses native submit event)
|
||
});
|
||
|
||
function openAlertEventModal(e) {
|
||
document.getElementById('modal-title').textContent = e.title;
|
||
const startStr = e.start ? e.start : '—';
|
||
const endStr = e.end ? e.end : null;
|
||
document.getElementById('modal-body').innerHTML = `
|
||
<div class="flex gap-2"><span class="text-gray-400 w-24 shrink-0">Type</span><span>${e.type}</span></div>
|
||
<div class="flex gap-2"><span class="text-gray-400 w-24 shrink-0">Status</span><span>${e.status}</span></div>
|
||
${e.client ? `<div class="flex gap-2"><span class="text-gray-400 w-24 shrink-0">Client</span><span>${e.client}</span></div>` : ''}
|
||
${e.location ? `<div class="flex gap-2"><span class="text-gray-400 w-24 shrink-0">Location</span><span>${e.location}</span></div>` : ''}
|
||
<div class="flex gap-2"><span class="text-gray-400 w-24 shrink-0">Start</span><span>${startStr}</span></div>
|
||
${endStr ? `<div class="flex gap-2"><span class="text-gray-400 w-24 shrink-0">End</span><span>${endStr}</span></div>` : ''}
|
||
${e.description ? `<div class="flex gap-2"><span class="text-gray-400 w-24 shrink-0">Description</span><div class="flex-1 break-words whitespace-pre-wrap overflow-y-auto max-h-20 pr-1">${e.description}</div></div>` : ''}
|
||
${e.notes ? `<div class="flex gap-2"><span class="text-gray-400 w-24 shrink-0">Notes</span><div class="flex-1 break-words whitespace-pre-wrap overflow-y-auto max-h-20 pr-1">${e.notes}</div></div>` : ''}
|
||
${e.has_contract ? `<div class="flex gap-2"><span class="text-gray-400 w-24 shrink-0">Contract</span><span class="text-pink-500 font-semibold">Yes</span></div>` : ''}
|
||
${e.has_content ? `<div class="flex gap-2"><span class="text-gray-400 w-24 shrink-0">Content Bd</span><span class="text-pink-500 font-semibold">Yes</span></div>` : ''}
|
||
`;
|
||
document.getElementById('modal-actions').innerHTML = `
|
||
<a href="/schedule/${e.id}/edit" class="btn-sakura text-sm flex-1 text-center py-2">Edit</a>
|
||
<form method="POST" action="/schedule/${e.id}" onsubmit="return confirm('Delete?')" class="flex-1">
|
||
<input type="hidden" name="_method" value="DELETE">
|
||
<input type="hidden" name="_token" value="${document.querySelector('meta[name=csrf-token]').content}">
|
||
<button class="w-full py-2 rounded-full border border-red-200 text-red-400 text-sm hover:bg-red-50">Delete</button>
|
||
</form>
|
||
`;
|
||
document.getElementById('event-modal').classList.remove('hidden');
|
||
}
|
||
</script>
|
||
|
||
{{-- Share scripts disabled --}}
|
||
|
||
<script>
|
||
function contractCheck(checkUrl) {
|
||
return {
|
||
categoryId: '',
|
||
clientId: '',
|
||
contractWarning: false,
|
||
contractMessage: '',
|
||
async checkContract() {
|
||
if (!this.categoryId) { this.contractWarning = false; return; }
|
||
const params = new URLSearchParams({ category_id: this.categoryId, client_id: this.clientId || '' });
|
||
const res = await fetch(checkUrl + '?' + params);
|
||
const data = await res.json();
|
||
if (data) {
|
||
this.contractWarning = true;
|
||
this.contractMessage = `"${data.title}" — ${data.client_name} holds exclusive rights in the ${data.category} category until ${data.end_date} (${data.days_left} days left).`;
|
||
} else {
|
||
this.contractWarning = false;
|
||
this.contractMessage = '';
|
||
}
|
||
},
|
||
submitIfClear(form) {
|
||
if (this.contractWarning) {
|
||
if (!confirm('⚠️ An exclusive contract is active for this category.\n\nAre you sure you want to proceed?')) return;
|
||
}
|
||
// Combine date + time inputs into the hidden datetime fields
|
||
// (form.submit() bypasses native 'submit' event, so we do it here)
|
||
const startDate = document.getElementById('start_date').value;
|
||
const startTime = document.getElementById('start_time_input').value || '00:00';
|
||
const endDate = document.getElementById('end_date').value;
|
||
const endTime = document.getElementById('end_time_input').value || '00:00';
|
||
|
||
if (!startDate) {
|
||
alert('Please select a start date.');
|
||
return;
|
||
}
|
||
|
||
document.getElementById('start_time_hidden').value = startDate + 'T' + startTime;
|
||
if (endDate) {
|
||
document.getElementById('end_time_hidden').value = endDate + 'T' + endTime;
|
||
}
|
||
|
||
form.submit();
|
||
}
|
||
};
|
||
}
|
||
</script>
|
||
@endpush
|
||
@endsection
|