feat: implement Alpine.js modal for content details and migrate to SortableJS for kanban board functionality
This commit is contained in:
@@ -70,7 +70,15 @@ class EventController extends Controller
|
|||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return view('schedule.index', compact('events', 'clients', 'calendarEvents', 'categoryTree'));
|
$holidays = \Illuminate\Support\Facades\Cache::remember('indonesian_holidays', 86400, function() {
|
||||||
|
$url = "https://libur.deno.dev/api";
|
||||||
|
$response = @file_get_contents($url);
|
||||||
|
if ($response === false) return [];
|
||||||
|
$data = json_decode($response, true);
|
||||||
|
return is_array($data) ? $data : [];
|
||||||
|
});
|
||||||
|
|
||||||
|
return view('schedule.index', compact('events', 'clients', 'calendarEvents', 'categoryTree', 'holidays'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(Request $request): RedirectResponse
|
public function store(Request $request): RedirectResponse
|
||||||
|
|||||||
@@ -50,16 +50,28 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Sortable items container --}}
|
{{-- Sortable items container --}}
|
||||||
<div class="flex flex-col gap-2 flex-1 min-h-12" x-sort:group="kanban"
|
<div class="flex flex-col gap-2 flex-1 min-h-12 kanban-column"
|
||||||
@sort="moved($event, '{{ $status->value }}')" data-col-status="{{ $status->value }}">
|
data-col-status="{{ $status->value }}">
|
||||||
|
|
||||||
@foreach($col['items'] as $brief)
|
@foreach($col['items'] as $brief)
|
||||||
<div class="bg-white rounded-2xl p-3 shadow-sm border border-pink-50 select-none
|
<div class="bg-white rounded-2xl p-3 shadow-sm border border-pink-50 select-none
|
||||||
cursor-grab active:cursor-grabbing hover:shadow-md transition-shadow"
|
cursor-grab active:cursor-grabbing hover:shadow-md transition-shadow kanban-item"
|
||||||
x-sort:item="{{ $brief->id }}" data-id="{{ $brief->id }}">
|
data-id="{{ $brief->id }}">
|
||||||
<div class="flex items-start gap-2 mb-1.5">
|
<div class="flex items-start gap-2 mb-1.5">
|
||||||
<span class="text-sm shrink-0">{{ $brief->platform_icon }}</span>
|
<span class="text-sm shrink-0">{{ $brief->platform_icon }}</span>
|
||||||
<a href="{{ route('content.show', $brief) }}"
|
<a href="#"
|
||||||
|
data-brief="{{ json_encode([
|
||||||
|
'id' => $brief->id,
|
||||||
|
'title' => $brief->title,
|
||||||
|
'platform' => $brief->platform_icon . ' ' . ucfirst($brief->platform),
|
||||||
|
'status' => $brief->status->label(),
|
||||||
|
'client' => $brief->client?->name,
|
||||||
|
'target_date' => $brief->target_date?->format('d M Y'),
|
||||||
|
'hook' => $brief->hook,
|
||||||
|
'body' => $brief->body,
|
||||||
|
'notes' => $brief->notes
|
||||||
|
]) }}"
|
||||||
|
@click.prevent="openModal(JSON.parse($el.dataset.brief))"
|
||||||
class="font-semibold text-gray-800 text-xs leading-tight hover:text-pink-500 line-clamp-2 flex-1">
|
class="font-semibold text-gray-800 text-xs leading-tight hover:text-pink-500 line-clamp-2 flex-1">
|
||||||
{{ $brief->title }}
|
{{ $brief->title }}
|
||||||
</a>
|
</a>
|
||||||
@@ -89,24 +101,106 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
||||||
|
{{-- Content Detail Request Modal --}}
|
||||||
|
<div x-show="modalOpen" class="fixed inset-0 z-[60] flex items-center justify-center bg-black/40 backdrop-blur-sm" style="display: none;" x-transition>
|
||||||
|
<div @click.outside="modalOpen = false" class="bg-white p-5 w-full max-w-lg rounded-3xl shadow-xl mx-4 max-h-[90vh] flex flex-col relative"
|
||||||
|
x-transition:enter="transition ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0 translate-y-4"
|
||||||
|
x-transition:enter-end="opacity-100 translate-y-0"
|
||||||
|
x-transition:leave="transition ease-in duration-200"
|
||||||
|
x-transition:leave-start="opacity-100 translate-y-0"
|
||||||
|
x-transition:leave-end="opacity-0 translate-y-4">
|
||||||
|
<div class="flex items-center justify-between mb-4 border-b border-pink-50 pb-3 shrink-0">
|
||||||
|
<h3 class="font-extrabold text-gray-800 text-xl truncate pr-4" x-text="activeBrief?.title"></h3>
|
||||||
|
<button @click="modalOpen = false" class="text-gray-400 hover:text-pink-500 hover:bg-pink-50 w-8 h-8 rounded-full flex items-center justify-center transition-colors">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-sm text-gray-600 space-y-4 flex-1 overflow-y-auto pr-2 custom-scrollbar">
|
||||||
|
<div class="flex gap-3"><span class="text-gray-400 w-20 shrink-0 font-medium">Platform</span><span class="font-bold text-gray-800" x-text="activeBrief?.platform"></span></div>
|
||||||
|
<div class="flex gap-3"><span class="text-gray-400 w-20 shrink-0 font-medium">Status</span><span class="font-bold badge bg-pink-100 text-pink-700 rounded-full px-2 py-0.5" x-text="activeBrief?.status"></span></div>
|
||||||
|
<template x-if="activeBrief?.client">
|
||||||
|
<div class="flex gap-3 items-center"><span class="text-gray-400 w-20 shrink-0 font-medium">Client</span><span class="font-semibold text-pink-500" x-text="activeBrief?.client"></span></div>
|
||||||
|
</template>
|
||||||
|
<template x-if="activeBrief?.target_date">
|
||||||
|
<div class="flex gap-3"><span class="text-gray-400 w-20 shrink-0 font-medium">Target</span><span class="font-medium" x-text="activeBrief?.target_date"></span></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="activeBrief?.hook">
|
||||||
|
<div class="flex flex-col gap-1 mt-4">
|
||||||
|
<span class="text-gray-400 font-medium">🎣 Hook</span>
|
||||||
|
<div class="whitespace-pre-wrap bg-pink-50/50 border border-pink-100 p-3 rounded-2xl text-gray-700 leading-relaxed" x-text="activeBrief?.hook"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template x-if="activeBrief?.body">
|
||||||
|
<div class="flex flex-col gap-1 mt-4">
|
||||||
|
<span class="text-gray-400 font-medium">📖 Body</span>
|
||||||
|
<div class="whitespace-pre-wrap bg-gray-50 border border-gray-100 p-3 rounded-2xl text-gray-700 leading-relaxed" x-text="activeBrief?.body"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template x-if="activeBrief?.notes">
|
||||||
|
<div class="flex flex-col gap-1 mt-4">
|
||||||
|
<span class="text-gray-400 font-medium">📝 Notes</span>
|
||||||
|
<div class="whitespace-pre-wrap bg-yellow-50/50 border border-yellow-100 p-3 rounded-2xl text-gray-700 leading-relaxed" x-text="activeBrief?.notes"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-3 mt-5 pt-4 border-t border-pink-50 shrink-0">
|
||||||
|
<a :href="`/content/${activeBrief?.id}/edit`" class="btn-sakura text-sm flex-1 text-center py-2.5">Edit Brief</a>
|
||||||
|
<a :href="`/content/${activeBrief?.id}`" class="py-2.5 rounded-full border border-pink-200 text-pink-500 text-sm hover:bg-pink-50 flex-1 text-center font-bold bg-white transition-colors">Details Page</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@push('scripts')
|
@push('scripts')
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
function kanban() {
|
function kanban() {
|
||||||
return {
|
return {
|
||||||
moved(event, targetStatus) {
|
modalOpen: false,
|
||||||
const id = event.item?.dataset?.id;
|
activeBrief: null,
|
||||||
if (!id) return;
|
init() {
|
||||||
fetch(`/content/${id}/status`, {
|
// Start Sortable once script loads
|
||||||
method: 'PATCH',
|
const initSort = () => {
|
||||||
headers: {
|
if (typeof Sortable !== 'undefined') {
|
||||||
'Content-Type': 'application/json',
|
document.querySelectorAll('.kanban-column').forEach(el => {
|
||||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
Sortable.create(el, {
|
||||||
},
|
group: 'kanban-board',
|
||||||
body: JSON.stringify({ status: targetStatus }),
|
animation: 150,
|
||||||
}).then(r => { if (r.ok) window.KolManager.notify('Status updated! 🎬'); });
|
onEnd: (evt) => {
|
||||||
|
const id = evt.item.dataset.id;
|
||||||
|
const targetCol = evt.to.dataset.colStatus;
|
||||||
|
if (!id || !targetCol) return;
|
||||||
|
|
||||||
|
// If dropped in same column identically, skip fetch
|
||||||
|
if (evt.from === evt.to && evt.oldIndex === evt.newIndex) return;
|
||||||
|
|
||||||
|
fetch(`/content/${id}/status`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ status: targetCol }),
|
||||||
|
}).then(r => {
|
||||||
|
if (r.ok) window.KolManager.notify('Status updated! 🎬');
|
||||||
|
else window.KolManager.notify('❌ Failed to update status');
|
||||||
|
}).catch(() => window.KolManager.notify('❌ Network error!'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setTimeout(initSort, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
initSort();
|
||||||
},
|
},
|
||||||
|
openModal(brief) {
|
||||||
|
this.activeBrief = brief;
|
||||||
|
this.modalOpen = true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -5,437 +5,449 @@
|
|||||||
@section('page-subtitle', 'Schedule a new task, meeting, or posting')
|
@section('page-subtitle', 'Schedule a new task, meeting, or posting')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="max-w-2xl mx-auto">
|
<div class="max-w-2xl mx-auto">
|
||||||
<div class="flex items-center gap-3 mb-5">
|
<div class="flex items-center gap-3 mb-5">
|
||||||
<a href="{{ route('schedule.index') }}"
|
<a href="{{ route('schedule.index') }}" class="btn-sakura text-sm px-5 py-2">
|
||||||
class="text-xs px-3 py-2 rounded-full border border-pink-200 text-gray-500 hover:bg-pink-50">
|
← Back to Schedule
|
||||||
← Back to Schedule
|
</a>
|
||||||
</a>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="cat-card p-4 lg:p-8">
|
<div class="cat-card p-4 lg:p-8">
|
||||||
<form action="{{ route('schedule.store') }}" method="POST" id="create-event-form"
|
<form action="{{ route('schedule.store') }}" method="POST" id="create-event-form"
|
||||||
x-data="contractCheck('{{ route('contracts.check', [], false) }}')"
|
x-data="contractCheck('{{ route('contracts.check', [], false) }}')" class="space-y-5">
|
||||||
class="space-y-5">
|
@csrf
|
||||||
@csrf
|
<input type="hidden" name="start_time" id="start_time_hidden">
|
||||||
<input type="hidden" name="start_time" id="start_time_hidden">
|
<input type="hidden" name="end_time" id="end_time_hidden">
|
||||||
<input type="hidden" name="end_time" id="end_time_hidden">
|
|
||||||
|
|
||||||
{{-- Title --}}
|
{{-- Title --}}
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-1">
|
|
||||||
Title <span class="text-pink-400">*</span>
|
|
||||||
</label>
|
|
||||||
<input type="text" name="title" value="{{ old('title') }}"
|
|
||||||
class="input-soft" placeholder="Event title" required>
|
|
||||||
@error('title') <p class="text-xs text-red-500 mt-1">{{ $message }}</p> @enderror
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4" x-data="{
|
|
||||||
isNew: false,
|
|
||||||
saving: false,
|
|
||||||
newName: '', newEmail: '', newPhone: '', newCompany: '',
|
|
||||||
errMsg: '',
|
|
||||||
isNewCat: false,
|
|
||||||
savingCat: false,
|
|
||||||
newCatName: '',
|
|
||||||
catErrMsg: '',
|
|
||||||
onClientChange(e) {
|
|
||||||
this.isNew = (e.target.value === '__new__');
|
|
||||||
if (!this.isNew) {
|
|
||||||
this.clientId = e.target.value;
|
|
||||||
$dispatch('contract-check');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async saveNewClient() {
|
|
||||||
if (!this.newName.trim()) { this.errMsg = 'Name is required.'; return; }
|
|
||||||
this.saving = true; this.errMsg = '';
|
|
||||||
const fd = new FormData();
|
|
||||||
fd.append('name', this.newName);
|
|
||||||
fd.append('email', this.newEmail);
|
|
||||||
fd.append('phone', this.newPhone);
|
|
||||||
fd.append('company', this.newCompany);
|
|
||||||
fd.append('_token', document.querySelector('meta[name=csrf-token]').content);
|
|
||||||
try {
|
|
||||||
const res = await fetch('{{ route('clients.quick-store', [], false) }}', {
|
|
||||||
method:'POST',
|
|
||||||
body: fd,
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'X-Requested-With': 'XMLHttpRequest'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (res.ok) {
|
|
||||||
// Add new option & select it
|
|
||||||
const sel = document.getElementById('client-select');
|
|
||||||
const opt = new Option(data.name, data.id, true, true);
|
|
||||||
sel.add(opt, sel.options[1]);
|
|
||||||
sel.value = data.id;
|
|
||||||
sel.dispatchEvent(new Event('change'));
|
|
||||||
this.isNew = false;
|
|
||||||
this.newName = this.newEmail = this.newPhone = this.newCompany = '';
|
|
||||||
} else {
|
|
||||||
this.errMsg = data.message || 'Could not create client.';
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.errMsg = 'An unexpected error occurred.';
|
|
||||||
} finally {
|
|
||||||
this.saving = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onCategoryChange(e) {
|
|
||||||
this.isNewCat = (e.target.value === '__new__');
|
|
||||||
if (!this.isNewCat) {
|
|
||||||
this.categoryId = e.target.value;
|
|
||||||
this.checkContract();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async saveNewCategory() {
|
|
||||||
if (!this.newCatName.trim()) { this.catErrMsg = 'Category name is required.'; return; }
|
|
||||||
this.savingCat = true; this.catErrMsg = '';
|
|
||||||
const fd = new FormData();
|
|
||||||
fd.append('name', this.newCatName);
|
|
||||||
fd.append('_token', document.querySelector('meta[name=csrf-token]').content);
|
|
||||||
try {
|
|
||||||
const res = await fetch('{{ route('client-categories.quick-store', [], false) }}', {
|
|
||||||
method:'POST',
|
|
||||||
body: fd,
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'X-Requested-With': 'XMLHttpRequest'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (res.ok) {
|
|
||||||
const sel = document.getElementById('category-select');
|
|
||||||
const opt = new Option(data.icon + ' ' + data.name, data.id, true, true);
|
|
||||||
sel.add(opt, sel.options[1]);
|
|
||||||
sel.value = data.id;
|
|
||||||
sel.dispatchEvent(new Event('change'));
|
|
||||||
this.isNewCat = false;
|
|
||||||
this.newCatName = '';
|
|
||||||
} else {
|
|
||||||
this.catErrMsg = data.message || 'Could not create category.';
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.catErrMsg = 'An unexpected error occurred.';
|
|
||||||
} finally {
|
|
||||||
this.savingCat = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}">
|
|
||||||
{{-- Type --}}
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-1">Type</label>
|
|
||||||
<select name="event_type" class="input-soft">
|
|
||||||
@foreach(['task' => '📋 Task', 'meeting' => '🤝 Meeting', 'deadline' => '⏰ Deadline', 'posting' => '📸 Posting'] as $val => $label)
|
|
||||||
<option value="{{ $val }}" {{ old('event_type') === $val ? 'selected' : '' }}>
|
|
||||||
{{ $label }}
|
|
||||||
</option>
|
|
||||||
@endforeach
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Client --}}
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-1">Client</label>
|
|
||||||
<select name="client_id" id="client-select" class="input-soft"
|
|
||||||
x-model="clientId"
|
|
||||||
@change="onClientChange($event); if($event.target.value !== '__new__') checkContract()">
|
|
||||||
<option value="">— None —</option>
|
|
||||||
<option value="__new__" class="font-semibold text-pink-600">+ Create New Client…</option>
|
|
||||||
@foreach($clients as $client)
|
|
||||||
<option value="{{ $client->id }}" {{ old('client_id') == $client->id ? 'selected' : '' }}>
|
|
||||||
{{ $client->name }}
|
|
||||||
</option>
|
|
||||||
@endforeach
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Quick New Client mini-form --}}
|
|
||||||
<div x-show="isNew" x-transition style="display: none;"
|
|
||||||
class="sm:col-span-2 bg-purple-50 rounded-2xl p-4 space-y-3 border border-purple-100">
|
|
||||||
<p class="text-xs font-bold text-purple-700">🐱 New Client Basic Info</p>
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Name <span class="text-pink-400">*</span></label>
|
|
||||||
<input type="text" x-model="newName" class="input-soft text-sm" placeholder="Client / brand name">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Company</label>
|
|
||||||
<input type="text" x-model="newCompany" class="input-soft text-sm" placeholder="Company name">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Email</label>
|
|
||||||
<input type="email" x-model="newEmail" class="input-soft text-sm" placeholder="contact@brand.com">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Phone</label>
|
|
||||||
<input type="text" x-model="newPhone" class="input-soft text-sm" placeholder="+62 …">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p x-show="errMsg" x-text="errMsg" class="text-xs text-red-500"></p>
|
|
||||||
<div class="flex gap-2 pt-1">
|
|
||||||
<button type="button" @click="saveNewClient()" :disabled="saving"
|
|
||||||
class="btn-sakura text-sm px-4 py-2" x-text="saving ? 'Saving…' : '✓ Add & Select'"></button>
|
|
||||||
<button type="button" @click="isNew = false; document.getElementById('client-select').value = ''"
|
|
||||||
class="text-sm px-4 py-2 rounded-full border border-pink-200 text-gray-500 hover:bg-pink-50">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p class="text-xs text-purple-400">Status will be set to Active automatically.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Category --}}
|
|
||||||
<div class="sm:col-span-2">
|
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-1">Category</label>
|
|
||||||
<select name="category_id" id="category-select" class="input-soft" x-model="categoryId" @change="onCategoryChange($event)">
|
|
||||||
<option value="">— None —</option>
|
|
||||||
<option value="__new__" class="font-semibold text-pink-600">+ Create New Category…</option>
|
|
||||||
@foreach($categoryTree as $parent)
|
|
||||||
<option value="{{ $parent->id }}" {{ old('category_id') == $parent->id ? 'selected' : '' }}>
|
|
||||||
{{ $parent->icon }} {{ $parent->name }}
|
|
||||||
</option>
|
|
||||||
@foreach($parent->children as $child)
|
|
||||||
<option value="{{ $child->id }}" {{ old('category_id') == $child->id ? 'selected' : '' }}>
|
|
||||||
↳ {{ $child->name }}
|
|
||||||
</option>
|
|
||||||
@endforeach
|
|
||||||
@endforeach
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Quick New Category mini-form --}}
|
|
||||||
<div x-show="isNewCat" x-transition style="display: none;"
|
|
||||||
class="sm:col-span-2 bg-pink-50 rounded-2xl p-4 space-y-3 border border-pink-100">
|
|
||||||
<p class="text-xs font-bold text-pink-700">🏷️ New Category</p>
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Category Name <span class="text-pink-400">*</span></label>
|
|
||||||
<input type="text" x-model="newCatName" class="input-soft text-sm" placeholder="e.g. Sponsored Post">
|
|
||||||
</div>
|
|
||||||
<p x-show="catErrMsg" x-text="catErrMsg" class="text-xs text-red-500"></p>
|
|
||||||
<div class="flex gap-2 pt-1">
|
|
||||||
<button type="button" @click="saveNewCategory()" :disabled="savingCat"
|
|
||||||
class="btn-sakura text-sm px-4 py-2" x-text="savingCat ? 'Saving…' : '✓ Add & Select'"></button>
|
|
||||||
<button type="button" @click="isNewCat = false; document.getElementById('category-select').value = ''"
|
|
||||||
class="text-sm px-4 py-2 rounded-full border border-pink-200 text-gray-500 hover:bg-pink-50">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</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 --}}
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-1">
|
<label class="block text-sm font-semibold text-gray-700 mb-1">
|
||||||
Start Date <span class="text-pink-400">*</span>
|
Title <span class="text-pink-400">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input type="date" id="start_date" class="input-soft"
|
<input type="text" name="title" value="{{ old('title') }}" class="input-soft" placeholder="Event title"
|
||||||
value="{{ old('_start_date') }}" required @change="checkContract()">
|
required>
|
||||||
</div>
|
@error('title') <p class="text-xs text-red-500 mt-1">{{ $message }}</p> @enderror
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-1">Start Time</label>
|
|
||||||
<input type="time" id="start_time_input" class="input-soft" value="09:00">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- End Date --}}
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4" x-data="{
|
||||||
<div>
|
isNew: false,
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-1">End Date</label>
|
saving: false,
|
||||||
<input type="date" id="end_date" class="input-soft" value="{{ old('_end_date') }}">
|
newName: '', newEmail: '', newPhone: '', newCompany: '',
|
||||||
</div>
|
errMsg: '',
|
||||||
<div>
|
isNewCat: false,
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-1">End Time</label>
|
savingCat: false,
|
||||||
<input type="time" id="end_time_input" class="input-soft" value="18:00">
|
newCatName: '',
|
||||||
</div>
|
catErrMsg: '',
|
||||||
|
onClientChange(e) {
|
||||||
{{-- Location --}}
|
this.isNew = (e.target.value === '__new__');
|
||||||
<div class="sm:col-span-2">
|
if (!this.isNew) {
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-1">Location</label>
|
this.clientId = e.target.value;
|
||||||
<input type="text" name="location" value="{{ old('location') }}"
|
$dispatch('contract-check');
|
||||||
class="input-soft" placeholder="Online / venue address">
|
}
|
||||||
</div>
|
},
|
||||||
</div>
|
async saveNewClient() {
|
||||||
|
if (!this.newName.trim()) { this.errMsg = 'Name is required.'; return; }
|
||||||
{{-- Description --}}
|
this.saving = true; this.errMsg = '';
|
||||||
<div>
|
const fd = new FormData();
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-1">Description</label>
|
fd.append('name', this.newName);
|
||||||
<textarea name="description" rows="2" class="input-soft resize-none"
|
fd.append('email', this.newEmail);
|
||||||
placeholder="Brief description…">{{ old('description') }}</textarea>
|
fd.append('phone', this.newPhone);
|
||||||
</div>
|
fd.append('company', this.newCompany);
|
||||||
|
fd.append('_token', document.querySelector('meta[name=csrf-token]').content);
|
||||||
{{-- Color --}}
|
try {
|
||||||
<div x-data="{ color: '{{ old('color', '#FFB7C5') }}' }">
|
const res = await fetch('{{ route('clients.quick-store', [], false) }}', {
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Color</label>
|
method:'POST',
|
||||||
<input type="hidden" name="color" :value="color">
|
body: fd,
|
||||||
<div class="flex items-center gap-3">
|
headers: {
|
||||||
<input type="color" x-model="color"
|
'Accept': 'application/json',
|
||||||
class="w-10 h-10 rounded-xl cursor-pointer border-2 border-pink-100 p-0.5 shrink-0">
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
</div>
|
});
|
||||||
</div>
|
const data = await res.json();
|
||||||
|
if (res.ok) {
|
||||||
{{-- Priority --}}
|
// Add new option & select it
|
||||||
<div class="flex items-center gap-2">
|
const sel = document.getElementById('client-select');
|
||||||
<input type="checkbox" name="is_priority" value="1" id="priority"
|
const opt = new Option(data.name, data.id, true, true);
|
||||||
class="w-4 h-4 accent-pink-400" {{ old('is_priority') ? 'checked' : '' }}>
|
sel.add(opt, sel.options[1]);
|
||||||
<label for="priority" class="text-sm text-gray-600 cursor-pointer">⭐ Mark as Priority</label>
|
sel.value = data.id;
|
||||||
</div>
|
sel.dispatchEvent(new Event('change'));
|
||||||
|
this.isNew = false;
|
||||||
{{-- Notes --}}
|
this.newName = this.newEmail = this.newPhone = this.newCompany = '';
|
||||||
<div>
|
} else {
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-1">Notes</label>
|
this.errMsg = data.message || 'Could not create client.';
|
||||||
<textarea name="notes" rows="3" class="input-soft resize-none"
|
}
|
||||||
placeholder="Additional notes…">{{ old('notes') }}</textarea>
|
} catch (err) {
|
||||||
</div>
|
this.errMsg = 'An unexpected error occurred.';
|
||||||
|
} finally {
|
||||||
{{-- ── Add to Content Board ──────────────────────────────── --}}
|
this.saving = false;
|
||||||
<div x-data="{ addToBoard: {{ old('add_to_content_board') ? 'true' : 'false' }} }">
|
}
|
||||||
<div class="flex items-center gap-2">
|
},
|
||||||
<input type="checkbox" name="add_to_content_board" value="1" id="add_to_content_board"
|
onCategoryChange(e) {
|
||||||
class="w-4 h-4 accent-pink-400" x-model="addToBoard">
|
this.isNewCat = (e.target.value === '__new__');
|
||||||
<label for="add_to_content_board" class="text-sm text-gray-600 cursor-pointer">
|
if (!this.isNewCat) {
|
||||||
🗂️ Also add to Content Board as a Drafting card
|
this.categoryId = e.target.value;
|
||||||
</label>
|
this.checkContract();
|
||||||
</div>
|
}
|
||||||
<div x-show="addToBoard" x-transition
|
},
|
||||||
class="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-3 bg-purple-50 rounded-2xl p-4">
|
async saveNewCategory() {
|
||||||
|
if (!this.newCatName.trim()) { this.catErrMsg = 'Category name is required.'; return; }
|
||||||
|
this.savingCat = true; this.catErrMsg = '';
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('name', this.newCatName);
|
||||||
|
fd.append('_token', document.querySelector('meta[name=csrf-token]').content);
|
||||||
|
try {
|
||||||
|
const res = await fetch('{{ route('client-categories.quick-store', [], false) }}', {
|
||||||
|
method:'POST',
|
||||||
|
body: fd,
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (res.ok) {
|
||||||
|
const sel = document.getElementById('category-select');
|
||||||
|
const opt = new Option(data.icon + ' ' + data.name, data.id, true, true);
|
||||||
|
sel.add(opt, sel.options[1]);
|
||||||
|
sel.value = data.id;
|
||||||
|
sel.dispatchEvent(new Event('change'));
|
||||||
|
this.isNewCat = false;
|
||||||
|
this.newCatName = '';
|
||||||
|
} else {
|
||||||
|
this.catErrMsg = data.message || 'Could not create category.';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.catErrMsg = 'An unexpected error occurred.';
|
||||||
|
} finally {
|
||||||
|
this.savingCat = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}">
|
||||||
|
{{-- Type --}}
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Platform</label>
|
<label class="block text-sm font-semibold text-gray-700 mb-1">Type</label>
|
||||||
<select name="content_platform" class="input-soft text-sm">
|
<select name="event_type" class="input-soft">
|
||||||
<option value="instagram" {{ old('content_platform') === 'instagram' ? 'selected' : '' }}>📸 Instagram</option>
|
@foreach(['task' => '📋 Task', 'meeting' => '🤝 Meeting', 'deadline' => '⏰ Deadline', 'posting' => '📸 Posting'] as $val => $label)
|
||||||
<option value="tiktok" {{ old('content_platform') === 'tiktok' ? 'selected' : '' }}>🎵 TikTok</option>
|
<option value="{{ $val }}" {{ old('event_type') === $val ? 'selected' : '' }}>
|
||||||
<option value="youtube" {{ old('content_platform') === 'youtube' ? 'selected' : '' }}>▶️ YouTube</option>
|
{{ $label }}
|
||||||
<option value="other" {{ old('content_platform') === 'other' ? 'selected' : '' }}>📱 Other</option>
|
</option>
|
||||||
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- Client --}}
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Content Type</label>
|
<label class="block text-sm font-semibold text-gray-700 mb-1">Client</label>
|
||||||
<select name="content_type" class="input-soft text-sm">
|
<select name="client_id" id="client-select" class="input-soft" x-model="clientId"
|
||||||
<option value="post" {{ old('content_type') === 'post' ? 'selected' : '' }}>Post / Feed</option>
|
@change="onClientChange($event); if($event.target.value !== '__new__') checkContract()">
|
||||||
<option value="reel" {{ old('content_type') === 'reel' ? 'selected' : '' }}>Reel / Short</option>
|
<option value="">— None —</option>
|
||||||
<option value="story" {{ old('content_type') === 'story' ? 'selected' : '' }}>Story</option>
|
<option value="__new__" class="font-semibold text-pink-600">+ Create New Client…</option>
|
||||||
<option value="video" {{ old('content_type') === 'video' ? 'selected' : '' }}>Long Video</option>
|
@foreach($clients as $client)
|
||||||
|
<option value="{{ $client->id }}" {{ old('client_id') == $client->id ? 'selected' : '' }}>
|
||||||
|
{{ $client->name }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<p class="sm:col-span-2 text-xs text-purple-500">
|
|
||||||
A Drafting card will be created in your Content Board with this event's title and date.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- ── Brand Contract ────────────────────────────────────── --}}
|
{{-- Quick New Client mini-form --}}
|
||||||
<div x-data="{ createContract: {{ old('create_brand_contract') ? 'true' : 'false' }} }">
|
<div x-show="isNew" x-transition style="display: none;"
|
||||||
<div class="flex items-center gap-2">
|
class="sm:col-span-2 bg-purple-50 rounded-2xl p-4 space-y-3 border border-purple-100">
|
||||||
<input type="checkbox" name="create_brand_contract" value="1" id="create_contract"
|
<p class="text-xs font-bold text-purple-700">🐱 New Client Basic Info</p>
|
||||||
class="w-4 h-4 accent-pink-400" x-model="createContract">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
<label for="create_contract" class="text-sm text-gray-600 cursor-pointer">
|
<div>
|
||||||
📄 Also create a Brand Contract for this event
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Name <span
|
||||||
</label>
|
class="text-pink-400">*</span></label>
|
||||||
</div>
|
<input type="text" x-model="newName" class="input-soft text-sm"
|
||||||
<div x-show="createContract" x-transition
|
placeholder="Client / brand name">
|
||||||
class="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-3 bg-pink-50 rounded-2xl p-4">
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Company</label>
|
||||||
|
<input type="text" x-model="newCompany" class="input-soft text-sm"
|
||||||
|
placeholder="Company name">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Email</label>
|
||||||
|
<input type="email" x-model="newEmail" class="input-soft text-sm"
|
||||||
|
placeholder="contact@brand.com">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Phone</label>
|
||||||
|
<input type="text" x-model="newPhone" class="input-soft text-sm" placeholder="+62 …">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p x-show="errMsg" x-text="errMsg" class="text-xs text-red-500"></p>
|
||||||
|
<div class="flex gap-2 pt-1">
|
||||||
|
<button type="button" @click="saveNewClient()" :disabled="saving"
|
||||||
|
class="btn-sakura text-sm px-4 py-2"
|
||||||
|
x-text="saving ? 'Saving…' : '✓ Add & Select'"></button>
|
||||||
|
<button type="button"
|
||||||
|
@click="isNew = false; document.getElementById('client-select').value = ''"
|
||||||
|
class="text-sm px-4 py-2 rounded-full border border-pink-200 text-gray-500 hover:bg-pink-50">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-purple-400">Status will be set to Active automatically.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Category --}}
|
||||||
<div class="sm:col-span-2">
|
<div class="sm:col-span-2">
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Contract Title</label>
|
<label class="block text-sm font-semibold text-gray-700 mb-1">Category</label>
|
||||||
<input type="text" name="contract_title" class="input-soft text-sm"
|
<select name="category_id" id="category-select" class="input-soft" x-model="categoryId"
|
||||||
placeholder="e.g. Exclusive Posting Contract — BrandX"
|
@change="onCategoryChange($event)">
|
||||||
value="{{ old('contract_title') }}">
|
<option value="">— None —</option>
|
||||||
|
<option value="__new__" class="font-semibold text-pink-600">+ Create New Category…</option>
|
||||||
|
@foreach($categoryTree as $parent)
|
||||||
|
<option value="{{ $parent->id }}" {{ old('category_id') == $parent->id ? 'selected' : '' }}>
|
||||||
|
{{ $parent->icon }} {{ $parent->name }}
|
||||||
|
</option>
|
||||||
|
@foreach($parent->children as $child)
|
||||||
|
<option value="{{ $child->id }}" {{ old('category_id') == $child->id ? 'selected' : '' }}>
|
||||||
|
↳ {{ $child->name }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Quick New Category mini-form --}}
|
||||||
|
<div x-show="isNewCat" x-transition style="display: none;"
|
||||||
|
class="sm:col-span-2 bg-pink-50 rounded-2xl p-4 space-y-3 border border-pink-100">
|
||||||
|
<p class="text-xs font-bold text-pink-700">🏷️ New Category</p>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Category Name <span
|
||||||
|
class="text-pink-400">*</span></label>
|
||||||
|
<input type="text" x-model="newCatName" class="input-soft text-sm"
|
||||||
|
placeholder="e.g. Sponsored Post">
|
||||||
|
</div>
|
||||||
|
<p x-show="catErrMsg" x-text="catErrMsg" class="text-xs text-red-500"></p>
|
||||||
|
<div class="flex gap-2 pt-1">
|
||||||
|
<button type="button" @click="saveNewCategory()" :disabled="savingCat"
|
||||||
|
class="btn-sakura text-sm px-4 py-2"
|
||||||
|
x-text="savingCat ? 'Saving…' : '✓ Add & Select'"></button>
|
||||||
|
<button type="button"
|
||||||
|
@click="isNewCat = false; document.getElementById('category-select').value = ''"
|
||||||
|
class="text-sm px-4 py-2 rounded-full border border-pink-200 text-gray-500 hover:bg-pink-50">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</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 --}}
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-1">
|
||||||
|
Start Date <span class="text-pink-400">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="date" id="start_date" class="input-soft" value="{{ old('_start_date') }}" required
|
||||||
|
@change="checkContract()">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Contract Start</label>
|
<label class="block text-sm font-semibold text-gray-700 mb-1">Start Time</label>
|
||||||
<input type="date" name="contract_start_date" class="input-soft text-sm"
|
<input type="time" id="start_time_input" class="input-soft" value="09:00">
|
||||||
value="{{ old('contract_start_date') }}">
|
</div>
|
||||||
|
|
||||||
|
{{-- End Date --}}
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-1">End Date</label>
|
||||||
|
<input type="date" id="end_date" class="input-soft" value="{{ old('_end_date') }}">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-semibold text-gray-600 mb-1">Contract End</label>
|
<label class="block text-sm font-semibold text-gray-700 mb-1">End Time</label>
|
||||||
<input type="date" name="contract_end_date" class="input-soft text-sm"
|
<input type="time" id="end_time_input" class="input-soft" value="18:00">
|
||||||
value="{{ old('contract_end_date') }}">
|
</div>
|
||||||
|
|
||||||
|
{{-- Location --}}
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-1">Location</label>
|
||||||
|
<input type="text" name="location" value="{{ old('location') }}" class="input-soft"
|
||||||
|
placeholder="Online / venue address">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col sm:flex-row gap-3 pt-1">
|
{{-- Description --}}
|
||||||
<button type="submit" class="btn-sakura flex-1 py-3"
|
<div>
|
||||||
:class="{ 'opacity-50 cursor-not-allowed': contractWarning }"
|
<label class="block text-sm font-semibold text-gray-700 mb-1">Description</label>
|
||||||
:disabled="contractWarning">
|
<textarea name="description" rows="2" class="input-soft resize-none"
|
||||||
🐾 Schedule It!
|
placeholder="Brief description…">{{ old('description') }}</textarea>
|
||||||
</button>
|
</div>
|
||||||
<a href="{{ route('schedule.index') }}"
|
|
||||||
class="flex-1 py-3 text-center rounded-full border border-pink-200 text-gray-600 hover:bg-pink-50 font-semibold text-sm">
|
{{-- Color --}}
|
||||||
Cancel
|
<div x-data="{ color: '{{ old('color', '#FFB7C5') }}' }">
|
||||||
</a>
|
<label class="block text-sm font-semibold text-gray-700 mb-2">Color</label>
|
||||||
</div>
|
<input type="hidden" name="color" :value="color">
|
||||||
</form>
|
<div class="flex items-center gap-3">
|
||||||
|
<input type="color" x-model="color"
|
||||||
|
class="w-10 h-10 rounded-xl cursor-pointer border-2 border-pink-100 p-0.5 shrink-0">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Priority --}}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input type="checkbox" name="is_priority" value="1" id="priority" class="w-4 h-4 accent-pink-400" {{ old('is_priority') ? 'checked' : '' }}>
|
||||||
|
<label for="priority" class="text-sm text-gray-600 cursor-pointer">⭐ Mark as Priority</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Notes --}}
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-1">Notes</label>
|
||||||
|
<textarea name="notes" rows="3" class="input-soft resize-none"
|
||||||
|
placeholder="Additional notes…">{{ old('notes') }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- ── Add to Content Board ──────────────────────────────── --}}
|
||||||
|
<div x-data="{ addToBoard: {{ old('add_to_content_board') ? 'true' : 'false' }} }">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input type="checkbox" name="add_to_content_board" value="1" id="add_to_content_board"
|
||||||
|
class="w-4 h-4 accent-pink-400" x-model="addToBoard">
|
||||||
|
<label for="add_to_content_board" class="text-sm text-gray-600 cursor-pointer">
|
||||||
|
🗂️ Also add to Content Board as a Drafting card
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div x-show="addToBoard" x-transition
|
||||||
|
class="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-3 bg-purple-50 rounded-2xl p-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Platform</label>
|
||||||
|
<select name="content_platform" class="input-soft text-sm">
|
||||||
|
<option value="instagram" {{ old('content_platform') === 'instagram' ? 'selected' : '' }}>📸
|
||||||
|
Instagram</option>
|
||||||
|
<option value="tiktok" {{ old('content_platform') === 'tiktok' ? 'selected' : '' }}>🎵 TikTok
|
||||||
|
</option>
|
||||||
|
<option value="youtube" {{ old('content_platform') === 'youtube' ? 'selected' : '' }}>▶️
|
||||||
|
YouTube</option>
|
||||||
|
<option value="other" {{ old('content_platform') === 'other' ? 'selected' : '' }}>📱 Other
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Content Type</label>
|
||||||
|
<select name="content_type" class="input-soft text-sm">
|
||||||
|
<option value="post" {{ old('content_type') === 'post' ? 'selected' : '' }}>Post / Feed
|
||||||
|
</option>
|
||||||
|
<option value="reel" {{ old('content_type') === 'reel' ? 'selected' : '' }}>Reel / Short
|
||||||
|
</option>
|
||||||
|
<option value="story" {{ old('content_type') === 'story' ? 'selected' : '' }}>Story</option>
|
||||||
|
<option value="video" {{ old('content_type') === 'video' ? 'selected' : '' }}>Long Video
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<p class="sm:col-span-2 text-xs text-purple-500">
|
||||||
|
A Drafting card will be created in your Content Board with this event's title and date.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- ── Brand Contract ────────────────────────────────────── --}}
|
||||||
|
<div x-data="{ createContract: {{ old('create_brand_contract') ? 'true' : 'false' }} }">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input type="checkbox" name="create_brand_contract" value="1" id="create_contract"
|
||||||
|
class="w-4 h-4 accent-pink-400" x-model="createContract">
|
||||||
|
<label for="create_contract" class="text-sm 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-4">
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Contract Title</label>
|
||||||
|
<input type="text" name="contract_title" class="input-soft text-sm"
|
||||||
|
placeholder="e.g. Exclusive Posting Contract — BrandX" value="{{ old('contract_title') }}">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Contract Start</label>
|
||||||
|
<input type="date" name="contract_start_date" class="input-soft text-sm"
|
||||||
|
value="{{ old('contract_start_date') }}">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Contract End</label>
|
||||||
|
<input type="date" name="contract_end_date" class="input-soft text-sm"
|
||||||
|
value="{{ old('contract_end_date') }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col sm:flex-row gap-3 pt-1">
|
||||||
|
<button type="submit" class="btn-sakura flex-1 py-3"
|
||||||
|
:class="{ 'opacity-50 cursor-not-allowed': contractWarning }" :disabled="contractWarning">
|
||||||
|
🐾 Schedule It!
|
||||||
|
</button>
|
||||||
|
<a href="{{ route('schedule.index') }}"
|
||||||
|
class="flex-1 py-3 text-center rounded-full border border-pink-200 text-gray-600 hover:bg-pink-50 font-semibold text-sm">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
@push('scripts')
|
@push('scripts')
|
||||||
<script>
|
<script>
|
||||||
function contractCheck(checkUrl) {
|
function contractCheck(checkUrl) {
|
||||||
return {
|
return {
|
||||||
categoryId: '',
|
categoryId: '',
|
||||||
clientId: '',
|
clientId: '',
|
||||||
contractWarning: false,
|
contractWarning: false,
|
||||||
contractMessage: '',
|
contractMessage: '',
|
||||||
async checkContract() {
|
async checkContract() {
|
||||||
if (!this.categoryId) { this.contractWarning = false; return; }
|
if (!this.categoryId) { this.contractWarning = false; return; }
|
||||||
let dateVal = '';
|
let dateVal = '';
|
||||||
const startDateEl = document.getElementById('start_date');
|
const startDateEl = document.getElementById('start_date');
|
||||||
if (startDateEl && startDateEl.value) {
|
if (startDateEl && startDateEl.value) {
|
||||||
dateVal = startDateEl.value;
|
dateVal = startDateEl.value;
|
||||||
|
}
|
||||||
|
if (!dateVal) {
|
||||||
|
this.contractWarning = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
category_id: this.categoryId,
|
||||||
|
client_id: this.clientId || '',
|
||||||
|
date: dateVal
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const res = await fetch(checkUrl + '?' + params);
|
||||||
|
if (!res.ok) throw new Error('Network error');
|
||||||
|
const data = await res.json();
|
||||||
|
if (data && data.title) {
|
||||||
|
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 = '';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.contractWarning = false;
|
||||||
|
this.contractMessage = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (!dateVal) {
|
|
||||||
this.contractWarning = false;
|
document.getElementById('create-event-form').addEventListener('submit', function (e) {
|
||||||
return;
|
const startDate = document.getElementById('start_date').value;
|
||||||
}
|
const startTime = document.getElementById('start_time_input').value || '00:00';
|
||||||
const params = new URLSearchParams({
|
const endDate = document.getElementById('end_date').value;
|
||||||
category_id: this.categoryId,
|
const endTime = document.getElementById('end_time_input').value || '00:00';
|
||||||
client_id: this.clientId || '',
|
|
||||||
date: dateVal
|
if (!startDate) {
|
||||||
});
|
e.preventDefault();
|
||||||
try {
|
alert('Please select a start date.');
|
||||||
const res = await fetch(checkUrl + '?' + params);
|
return;
|
||||||
if (!res.ok) throw new Error('Network error');
|
|
||||||
const data = await res.json();
|
|
||||||
if (data && data.title) {
|
|
||||||
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 = '';
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
this.contractWarning = false;
|
|
||||||
this.contractMessage = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('create-event-form').addEventListener('submit', function (e) {
|
document.getElementById('start_time_hidden').value = startDate + 'T' + startTime;
|
||||||
const startDate = document.getElementById('start_date').value;
|
if (endDate) {
|
||||||
const startTime = document.getElementById('start_time_input').value || '00:00';
|
document.getElementById('end_time_hidden').value = endDate + 'T' + endTime;
|
||||||
const endDate = document.getElementById('end_date').value;
|
}
|
||||||
const endTime = document.getElementById('end_time_input').value || '00:00';
|
});
|
||||||
|
</script>
|
||||||
if (!startDate) {
|
@endpush
|
||||||
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
|
@endsection
|
||||||
@@ -104,6 +104,43 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
dayCellDidMount(info) {
|
||||||
|
const holidays = @json($holidays ?? []);
|
||||||
|
const holidayMap = {};
|
||||||
|
holidays.forEach(h => holidayMap[h.date] = h.name);
|
||||||
|
|
||||||
|
const year = info.date.getFullYear();
|
||||||
|
const month = String(info.date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(info.date.getDate()).padStart(2, '0');
|
||||||
|
const dateStr = `${year}-${month}-${day}`;
|
||||||
|
|
||||||
|
if (holidayMap[dateStr]) {
|
||||||
|
const frame = info.el.querySelector('.fc-daygrid-day-frame');
|
||||||
|
if (frame) {
|
||||||
|
// Update background color for the highlighted box
|
||||||
|
frame.style.backgroundColor = '#fff0f0'; // Light red highlighted box
|
||||||
|
frame.style.position = 'relative'; // Required for absolute positioning of text
|
||||||
|
|
||||||
|
// Add the holiday text to the top-left corner
|
||||||
|
const holidayEl = document.createElement('div');
|
||||||
|
holidayEl.textContent = holidayMap[dateStr];
|
||||||
|
holidayEl.style.position = 'absolute';
|
||||||
|
holidayEl.style.top = '4px';
|
||||||
|
holidayEl.style.left = '4px';
|
||||||
|
holidayEl.style.color = '#ef4444'; // Red text
|
||||||
|
holidayEl.style.fontSize = '0.70rem'; // Small text
|
||||||
|
holidayEl.style.fontWeight = '600';
|
||||||
|
holidayEl.style.whiteSpace = 'nowrap';
|
||||||
|
holidayEl.style.overflow = 'hidden';
|
||||||
|
holidayEl.style.textOverflow = 'ellipsis';
|
||||||
|
holidayEl.style.maxWidth = '75%'; // Prevent it from overlapping with the date number (usually on right)
|
||||||
|
holidayEl.style.zIndex = '1';
|
||||||
|
|
||||||
|
frame.appendChild(holidayEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
eventDrop(info) {
|
eventDrop(info) {
|
||||||
fetch(`/schedule/${info.event.id}/dates`, {
|
fetch(`/schedule/${info.event.id}/dates`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
|
|||||||
Reference in New Issue
Block a user