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.
This commit is contained in:
I-Fun
2026-04-02 23:24:38 +07:00
parent 5748b163b6
commit 1b90f730a6
25 changed files with 1524 additions and 390 deletions

View File

@@ -0,0 +1,323 @@
@extends('layouts.app')
@section('title', 'Add Event')
@section('page-title', 'Add Event 📅')
@section('page-subtitle', 'Schedule a new task, meeting, or posting')
@section('content')
<div class="max-w-2xl mx-auto">
<div class="flex items-center gap-3 mb-5">
<a href="{{ route('schedule.index') }}"
class="text-xs px-3 py-2 rounded-full border border-pink-200 text-gray-500 hover:bg-pink-50">
← Back to Schedule
</a>
</div>
<div class="cat-card p-4 lg:p-8">
<form action="{{ route('schedule.store') }}" method="POST" id="create-event-form"
x-data="contractCheck('{{ route('contracts.check') }}')"
class="space-y-5">
@csrf
<input type="hidden" name="start_time" id="start_time_hidden">
<input type="hidden" name="end_time" id="end_time_hidden">
{{-- 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">
{{-- 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 x-data="{
isNew: false,
saving: false,
newName: '', newEmail: '', newPhone: '', newCompany: '',
errMsg: '',
onClientChange(e) {
this.isNew = (e.target.value === '__new__');
if (!this.isNew) {
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);
const res = await fetch('{{ route('clients.quick-store') }}', { method:'POST', body: fd });
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.';
}
this.saving = false;
}
}">
<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>
{{-- Quick New Client mini-form --}}
<div x-show="isNew" x-transition
class="mt-3 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; $el.closest('div').querySelector('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>
</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" class="input-soft" x-model="categoryId" @change="checkContract()">
<option value="">— None —</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' : '' }}>
&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 --}}
<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>
</div>
<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>
{{-- 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>
<label class="block text-sm font-semibold text-gray-700 mb-1">End Time</label>
<input type="time" id="end_time_input" class="input-soft" value="18:00">
</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>
{{-- Description --}}
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">Description</label>
<textarea name="description" rows="2" class="input-soft resize-none"
placeholder="Brief description…">{{ old('description') }}</textarea>
</div>
{{-- Color --}}
<div x-data="{ color: '{{ old('color', '#FFB7C5') }}' }">
<label class="block text-sm font-semibold text-gray-700 mb-2">Color</label>
<input type="hidden" name="color" :value="color">
<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 }">
🐾 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>
@push('scripts')
<script>
document.getElementById('create-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

View File

@@ -137,14 +137,6 @@
<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 class="flex gap-2 flex-wrap">
@foreach(['#FFB7C5','#B5EAD7','#FDFD96','#E8D5F5','#FFD5B8','#AEC6CF'] as $hex)
<button type="button"
class="w-8 h-8 rounded-full ring-2 ring-transparent hover:ring-gray-400 transition-all"
style="background:{{ $hex }}"
@click="color = '{{ $hex }}'"></button>
@endforeach
</div>
</div>
</div>

View File

@@ -6,143 +6,9 @@
@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>
{{-- ── 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 ─────────────────────────────────── --}}