Files
Kol-Manager/resources/views/schedule/index.blade.php
I-Fun 1b90f730a6 feat: add notes management feature with CRUD operations
- Created Note model and controller for handling notes.
- Added routes for notes CRUD operations.
- Implemented notes index view with search and pinning functionality.
- Created migration for notes table with necessary fields.
- Developed event creation form with client and contract options.
2026-04-02 23:24:38 +07:00

230 lines
12 KiB
PHP
Raw Permalink 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 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
$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