initial commit
This commit is contained in:
252
resources/views/schedule/index.blade.php
Normal file
252
resources/views/schedule/index.blade.php
Normal file
@@ -0,0 +1,252 @@
|
||||
@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">
|
||||
@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">
|
||||
<option value="">— None —</option>
|
||||
@foreach($clients as $client)
|
||||
<option value="{{ $client->id }}">{{ $client->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</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>
|
||||
<label class="text-xs font-semibold text-gray-600">Color</label>
|
||||
<div class="flex gap-2 mt-1 flex-wrap">
|
||||
@foreach(['#FFB7C5','#B5EAD7','#FDFD96','#E8D5F5','#FFD5B8','#AEC6CF'] as $hex)
|
||||
<label class="cursor-pointer">
|
||||
<input type="radio" name="color" value="{{ $hex }}" class="sr-only peer" {{ $loop->first ? 'checked' : '' }}>
|
||||
<span class="w-7 h-7 rounded-full block ring-2 ring-transparent peer-checked:ring-gray-500 peer-checked:ring-offset-1 transition-all"
|
||||
style="background:{{ $hex }};"></span>
|
||||
</label>
|
||||
@endforeach
|
||||
</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>
|
||||
<div class="sm:col-span-2">
|
||||
<button type="submit" class="btn-sakura w-full">🐾 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">
|
||||
<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>` : ''}
|
||||
`;
|
||||
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');
|
||||
},
|
||||
});
|
||||
|
||||
calendar.render();
|
||||
|
||||
// Close modal on backdrop tap
|
||||
document.getElementById('event-modal').addEventListener('click', function(e) {
|
||||
if (e.target === this) this.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Combine date + time inputs into hidden datetime fields before submit
|
||||
document.getElementById('new-event-form').addEventListener('submit', function(e) {
|
||||
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) {
|
||||
e.preventDefault();
|
||||
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;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endsection
|
||||
Reference in New Issue
Block a user