364 lines
18 KiB
PHP
364 lines
18 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 Form (always accessible, collapsible) ─────── --}}
|
||
<div class="cat-card p-4 mb-4" x-data="{ open: false }">
|
||
<div class="flex items-center justify-between">
|
||
<h3 class="text-sm font-bold text-gray-700 flex items-center gap-2">➕ New Event</h3>
|
||
<button class="btn-sakura text-xs px-4 py-1.5" @click="open = !open"
|
||
x-text="open ? '✕ Close' : '+ Add Event'"></button>
|
||
</div>
|
||
|
||
<form x-show="open" x-transition action="{{ route('schedule.store') }}" method="POST"
|
||
class="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-3"
|
||
id="new-event-form"
|
||
x-data="contractCheck('{{ route('contracts.check') }}')"
|
||
@submit.prevent="submitIfClear($el)">
|
||
@csrf
|
||
{{-- Hidden combined datetime fields --}}
|
||
<input type="hidden" name="start_time" id="start_time_hidden">
|
||
<input type="hidden" name="end_time" id="end_time_hidden">
|
||
|
||
<div class="sm:col-span-2">
|
||
<label class="text-xs font-semibold text-gray-600">Title <span class="text-pink-400">*</span></label>
|
||
<input type="text" name="title" class="input-soft mt-1" placeholder="Event title" required>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs font-semibold text-gray-600">Type</label>
|
||
<select name="event_type" class="input-soft mt-1">
|
||
<option value="task">📋 Task</option>
|
||
<option value="meeting">🤝 Meeting</option>
|
||
<option value="deadline">⏰ Deadline</option>
|
||
<option value="posting">📸 Posting</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs font-semibold text-gray-600">Client</label>
|
||
<select name="client_id" class="input-soft mt-1" x-model="clientId" @change="checkContract()">
|
||
<option value="">— None —</option>
|
||
@foreach($clients as $client)
|
||
<option value="{{ $client->id }}">{{ $client->name }}</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs font-semibold text-gray-600">Category</label>
|
||
<select name="category_id" class="input-soft mt-1" x-model="categoryId" @change="checkContract()">
|
||
<option value="">— None —</option>
|
||
@foreach($categoryTree as $parent)
|
||
<option value="{{ $parent->id }}">{{ $parent->icon }} {{ $parent->name }}</option>
|
||
@foreach($parent->children as $child)
|
||
<option value="{{ $child->id }}"> ↳ {{ $child->name }}</option>
|
||
@endforeach
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
|
||
{{-- Contract warning --}}
|
||
<div x-show="contractWarning" x-transition class="sm:col-span-2 rounded-2xl bg-amber-50 border border-amber-200 px-4 py-3 text-sm" style="display:none">
|
||
<div class="font-semibold text-amber-700 mb-1">⚠️ Exclusive Contract Active</div>
|
||
<p class="text-amber-600 text-xs" x-text="contractMessage"></p>
|
||
</div>
|
||
|
||
{{-- Start: date + time side by side --}}
|
||
<div>
|
||
<label class="text-xs font-semibold text-gray-600">Start Date <span class="text-pink-400">*</span></label>
|
||
<input type="date" id="start_date" class="input-soft mt-1" required>
|
||
</div>
|
||
<div>
|
||
<label class="text-xs font-semibold text-gray-600">Start Time</label>
|
||
<input type="time" id="start_time_input" class="input-soft mt-1" value="09:00">
|
||
</div>
|
||
|
||
{{-- End: date + time side by side --}}
|
||
<div>
|
||
<label class="text-xs font-semibold text-gray-600">End Date</label>
|
||
<input type="date" id="end_date" class="input-soft mt-1">
|
||
</div>
|
||
<div>
|
||
<label class="text-xs font-semibold text-gray-600">End Time</label>
|
||
<input type="time" id="end_time_input" class="input-soft mt-1" value="18:00">
|
||
</div>
|
||
|
||
<div x-data="{ color: '#FFB7C5' }">
|
||
<label class="text-xs font-semibold text-gray-600">Color</label>
|
||
<input type="hidden" name="color" :value="color">
|
||
<div class="flex items-center gap-3 mt-1">
|
||
<input type="color" x-model="color"
|
||
class="w-9 h-9 rounded-xl cursor-pointer border-2 border-pink-100 p-0.5 shrink-0">
|
||
<div class="flex gap-2 flex-wrap">
|
||
@foreach(['#FFB7C5','#B5EAD7','#FDFD96','#E8D5F5','#FFD5B8','#AEC6CF'] as $hex)
|
||
<button type="button"
|
||
class="w-7 h-7 rounded-full ring-2 ring-transparent hover:ring-gray-400 transition-all"
|
||
style="background:{{ $hex }}"
|
||
@click="color = '{{ $hex }}'"></button>
|
||
@endforeach
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="flex items-center gap-2 mt-1">
|
||
<input type="checkbox" name="is_priority" value="1" id="priority" class="w-4 h-4 accent-pink-400">
|
||
<label for="priority" class="text-xs text-gray-600 cursor-pointer">⭐ Priority</label>
|
||
</div>
|
||
<div class="sm:col-span-2">
|
||
<label class="text-xs font-semibold text-gray-600">Notes</label>
|
||
<textarea name="notes" rows="2" class="input-soft mt-1 resize-none text-sm"></textarea>
|
||
</div>
|
||
|
||
{{-- Active Contract checkbox --}}
|
||
<div class="sm:col-span-2" x-data="{ createContract: false }">
|
||
<div class="flex items-center gap-2">
|
||
<input type="checkbox" name="create_brand_contract" value="1" id="new_create_contract"
|
||
class="w-4 h-4 accent-pink-400" x-model="createContract">
|
||
<label for="new_create_contract" class="text-xs text-gray-600 cursor-pointer">
|
||
📄 Also create a Brand Contract for this event
|
||
</label>
|
||
</div>
|
||
<div x-show="createContract" x-transition class="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-3 bg-pink-50 rounded-2xl p-3">
|
||
<div class="sm:col-span-2">
|
||
<label class="text-xs font-semibold text-gray-600">Contract Title</label>
|
||
<input type="text" name="contract_title" class="input-soft mt-1 text-xs"
|
||
placeholder="e.g. Exclusive Posting Contract — BrandX">
|
||
</div>
|
||
<div>
|
||
<label class="text-xs font-semibold text-gray-600">Contract Start</label>
|
||
<input type="date" name="contract_start_date" class="input-soft mt-1 text-xs">
|
||
</div>
|
||
<div>
|
||
<label class="text-xs font-semibold text-gray-600">Contract End</label>
|
||
<input type="date" name="contract_end_date" class="input-soft mt-1 text-xs">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sm:col-span-2">
|
||
<button type="submit" class="btn-sakura w-full"
|
||
:class="{ 'opacity-50 cursor-not-allowed': contractWarning }">
|
||
🐾 Schedule It!
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
{{-- ── Upcoming Alert Strip ─────────────────────────────────── --}}
|
||
@php
|
||
$upcomingList = $events->filter(fn($e) => $e->start_time->isFuture() && $e->start_time->diffInHours(now()) <= 24);
|
||
@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->take(6) 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->toIso8601String(), 'end' => $event->end_time?->toIso8601String()]) }})">
|
||
<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,
|
||
end_time: info.event.endStr || 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-16 shrink-0">Type</span><span>${p.type}</span></div>
|
||
<div class="flex gap-2"><span class="text-gray-400 w-16 shrink-0">Status</span><span>${p.status}</span></div>
|
||
${p.client ? `<div class="flex gap-2"><span class="text-gray-400 w-16 shrink-0">Client</span><span>${p.client}</span></div>` : ''}
|
||
<div class="flex gap-2"><span class="text-gray-400 w-16 shrink-0">Start</span><span>${e.start?.toLocaleString()}</span></div>
|
||
${e.end ? `<div class="flex gap-2"><span class="text-gray-400 w-16 shrink-0">End</span><span>${e.end.toLocaleString()}</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 ? new Date(e.start).toLocaleString() : '—';
|
||
const endStr = e.end ? new Date(e.end).toLocaleString() : null;
|
||
document.getElementById('modal-body').innerHTML = `
|
||
<div class="flex gap-2"><span class="text-gray-400 w-16 shrink-0">Type</span><span>${e.type}</span></div>
|
||
<div class="flex gap-2"><span class="text-gray-400 w-16 shrink-0">Status</span><span>${e.status}</span></div>
|
||
${e.client ? `<div class="flex gap-2"><span class="text-gray-400 w-16 shrink-0">Client</span><span>${e.client}</span></div>` : ''}
|
||
<div class="flex gap-2"><span class="text-gray-400 w-16 shrink-0">Start</span><span>${startStr}</span></div>
|
||
${endStr ? `<div class="flex gap-2"><span class="text-gray-400 w-16 shrink-0">End</span><span>${endStr}</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
|