Files
Kol-Manager/resources/views/schedule/index.blade.php

364 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@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 }}">&nbsp;&nbsp;↳ {{ $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