feat: Add accent color to invoice settings and update related views; enhance event creation with brand contract option
This commit is contained in:
@@ -44,7 +44,6 @@ Thumbs.db
|
|||||||
# ── Docker config itself ─────────────────────
|
# ── Docker config itself ─────────────────────
|
||||||
Dockerfile
|
Dockerfile
|
||||||
docker-compose*.yml
|
docker-compose*.yml
|
||||||
docker/
|
|
||||||
|
|
||||||
# ── Claude / project meta ────────────────────
|
# ── Claude / project meta ────────────────────
|
||||||
.claude
|
.claude
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ RUN apk add --no-cache \
|
|||||||
nginx \
|
nginx \
|
||||||
supervisor \
|
supervisor \
|
||||||
sqlite \
|
sqlite \
|
||||||
|
sqlite-dev \
|
||||||
curl \
|
curl \
|
||||||
git \
|
git \
|
||||||
unzip \
|
unzip \
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ declare(strict_types=1);
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Enums\EventStatus;
|
use App\Enums\EventStatus;
|
||||||
|
use App\Models\BrandContract;
|
||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
use App\Models\ClientCategory;
|
use App\Models\ClientCategory;
|
||||||
use App\Models\Event;
|
use App\Models\Event;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class EventController extends Controller
|
class EventController extends Controller
|
||||||
@@ -63,12 +65,29 @@ class EventController extends Controller
|
|||||||
'is_all_day' => 'boolean',
|
'is_all_day' => 'boolean',
|
||||||
'color' => 'nullable|string|max:20',
|
'color' => 'nullable|string|max:20',
|
||||||
'notes' => 'nullable|string',
|
'notes' => 'nullable|string',
|
||||||
|
'create_brand_contract' => 'nullable|boolean',
|
||||||
|
'contract_title' => 'nullable|string|max:255',
|
||||||
|
'contract_start_date' => 'nullable|date',
|
||||||
|
'contract_end_date' => 'nullable|date|after_or_equal:contract_start_date',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$validated['is_priority'] = $request->boolean('is_priority');
|
$validated['is_priority'] = $request->boolean('is_priority');
|
||||||
$validated['is_all_day'] = $request->boolean('is_all_day');
|
$validated['is_all_day'] = $request->boolean('is_all_day');
|
||||||
|
|
||||||
Event::create($validated);
|
$event = Event::create(Arr::except($validated, [
|
||||||
|
'create_brand_contract', 'contract_title', 'contract_start_date', 'contract_end_date',
|
||||||
|
]));
|
||||||
|
|
||||||
|
if ($request->boolean('create_brand_contract') && $event->client_id) {
|
||||||
|
BrandContract::create([
|
||||||
|
'title' => $request->input('contract_title') ?: $event->title,
|
||||||
|
'client_id' => $event->client_id,
|
||||||
|
'category_id' => $event->category_id,
|
||||||
|
'start_date' => $request->input('contract_start_date') ?? $event->start_time->toDateString(),
|
||||||
|
'end_date' => $request->input('contract_end_date') ?? ($event->end_time?->toDateString() ?? $event->start_time->toDateString()),
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
return redirect()->route('schedule.index')
|
return redirect()->route('schedule.index')
|
||||||
->with('success', '🐾 Event scheduled!');
|
->with('success', '🐾 Event scheduled!');
|
||||||
@@ -98,10 +117,27 @@ class EventController extends Controller
|
|||||||
'status' => 'required|in:pending,in_progress,completed,cancelled',
|
'status' => 'required|in:pending,in_progress,completed,cancelled',
|
||||||
'color' => 'nullable|string|max:20',
|
'color' => 'nullable|string|max:20',
|
||||||
'notes' => 'nullable|string',
|
'notes' => 'nullable|string',
|
||||||
|
'create_brand_contract' => 'nullable|boolean',
|
||||||
|
'contract_title' => 'nullable|string|max:255',
|
||||||
|
'contract_start_date' => 'nullable|date',
|
||||||
|
'contract_end_date' => 'nullable|date|after_or_equal:contract_start_date',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$validated['is_priority'] = $request->boolean('is_priority');
|
$validated['is_priority'] = $request->boolean('is_priority');
|
||||||
$event->update($validated);
|
$event->update(Arr::except($validated, [
|
||||||
|
'create_brand_contract', 'contract_title', 'contract_start_date', 'contract_end_date',
|
||||||
|
]));
|
||||||
|
|
||||||
|
if ($request->boolean('create_brand_contract') && $event->client_id) {
|
||||||
|
BrandContract::create([
|
||||||
|
'title' => $request->input('contract_title') ?: $event->title,
|
||||||
|
'client_id' => $event->client_id,
|
||||||
|
'category_id' => $event->category_id,
|
||||||
|
'start_date' => $request->input('contract_start_date') ?? $event->start_time->toDateString(),
|
||||||
|
'end_date' => $request->input('contract_end_date') ?? ($event->end_time?->toDateString() ?? $event->start_time->toDateString()),
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
return redirect()->route('schedule.index')
|
return redirect()->route('schedule.index')
|
||||||
->with('success', '🐾 Event updated!');
|
->with('success', '🐾 Event updated!');
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class InvoiceSettingsController extends Controller
|
|||||||
'header_name' => 'nullable|string|max:100',
|
'header_name' => 'nullable|string|max:100',
|
||||||
'header_title' => 'nullable|string|max:150',
|
'header_title' => 'nullable|string|max:150',
|
||||||
'header_logo' => 'nullable|image|max:2048',
|
'header_logo' => 'nullable|image|max:2048',
|
||||||
|
'accent_color' => 'nullable|string|max:20',
|
||||||
'default_tax_rate' => 'nullable|numeric|min:0|max:100',
|
'default_tax_rate' => 'nullable|numeric|min:0|max:100',
|
||||||
'default_currency' => 'nullable|string|max:10',
|
'default_currency' => 'nullable|string|max:10',
|
||||||
'default_service_description'=> 'nullable|string|max:255',
|
'default_service_description'=> 'nullable|string|max:255',
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class InvoiceSettings extends Model
|
|||||||
'header_name',
|
'header_name',
|
||||||
'header_title',
|
'header_title',
|
||||||
'header_logo',
|
'header_logo',
|
||||||
|
'accent_color',
|
||||||
'default_tax_rate',
|
'default_tax_rate',
|
||||||
'default_currency',
|
'default_currency',
|
||||||
'default_service_description',
|
'default_service_description',
|
||||||
@@ -37,6 +38,7 @@ class InvoiceSettings extends Model
|
|||||||
return static::firstOrCreate([], [
|
return static::firstOrCreate([], [
|
||||||
'invoice_prefix' => 'INV',
|
'invoice_prefix' => 'INV',
|
||||||
'invoice_next_number' => 1,
|
'invoice_next_number' => 1,
|
||||||
|
'accent_color' => '#FFB7C5',
|
||||||
'default_tax_rate' => 11,
|
'default_tax_rate' => 11,
|
||||||
'default_currency' => 'IDR',
|
'default_currency' => 'IDR',
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -38,6 +38,13 @@ return [
|
|||||||
'report' => false,
|
'report' => false,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'private' => [
|
||||||
|
'driver' => 'local',
|
||||||
|
'root' => storage_path('app/private'),
|
||||||
|
'serve' => false,
|
||||||
|
'throw' => false,
|
||||||
|
],
|
||||||
|
|
||||||
'public' => [
|
'public' => [
|
||||||
'driver' => 'local',
|
'driver' => 'local',
|
||||||
'root' => storage_path('app/public'),
|
'root' => storage_path('app/public'),
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('invoice_settings', function (Blueprint $table) {
|
||||||
|
$table->string('accent_color', 20)->default('#FFB7C5')->after('header_logo');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('invoice_settings', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('accent_color');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -45,6 +45,10 @@ chmod -R 775 \
|
|||||||
echo "[migrate] Running database migrations…"
|
echo "[migrate] Running database migrations…"
|
||||||
php artisan migrate --force --no-interaction
|
php artisan migrate --force --no-interaction
|
||||||
|
|
||||||
|
# ── 5b. Create public storage symlink ────────
|
||||||
|
echo "[storage] Creating public storage symlink…"
|
||||||
|
php artisan storage:link --quiet 2>/dev/null || true
|
||||||
|
|
||||||
# ── 6. Cache config / routes / views ────────
|
# ── 6. Cache config / routes / views ────────
|
||||||
echo "[optimize] Caching configuration…"
|
echo "[optimize] Caching configuration…"
|
||||||
php artisan config:cache --quiet
|
php artisan config:cache --quiet
|
||||||
|
|||||||
@@ -38,11 +38,10 @@ stderr_logfile_maxbytes=0
|
|||||||
|
|
||||||
; ── Laravel Queue Worker ──────────────────────────────────────────────────
|
; ── Laravel Queue Worker ──────────────────────────────────────────────────
|
||||||
[program:queue]
|
[program:queue]
|
||||||
command=php /var/www/html/artisan queue:work \
|
command=php /var/www/html/artisan queue:work --sleep=3 --tries=3 --max-time=3600 --queue=default
|
||||||
--sleep=3 \
|
|
||||||
--tries=3 \
|
|
||||||
--max-time=3600 \
|
queue=default
|
||||||
--queue=default
|
|
||||||
directory=/var/www/html
|
directory=/var/www/html
|
||||||
user=www-data
|
user=www-data
|
||||||
autostart=true
|
autostart=true
|
||||||
|
|||||||
@@ -6,8 +6,30 @@
|
|||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
|
|
||||||
|
{{-- ── File Detail Popup Modal ──────────────────────────────── --}}
|
||||||
|
<div id="file-detail-modal"
|
||||||
|
class="fixed inset-0 z-50 flex items-end sm:items-center justify-center bg-black/40 hidden"
|
||||||
|
onclick="if(event.target===this)this.classList.add('hidden')">
|
||||||
|
<div class="cat-card p-5 w-full sm:max-w-sm 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="fdm-name">File</h3>
|
||||||
|
<button onclick="document.getElementById('file-detail-modal').classList.add('hidden')"
|
||||||
|
class="text-gray-400 hover:text-gray-600 text-xl leading-none">✕</button>
|
||||||
|
</div>
|
||||||
|
<div id="fdm-preview" class="mb-3 flex justify-center"></div>
|
||||||
|
<div class="text-sm text-gray-600 space-y-1.5" id="fdm-body"></div>
|
||||||
|
<div class="flex gap-2 mt-4">
|
||||||
|
<a id="fdm-view-btn" href="#" target="_blank"
|
||||||
|
class="btn-sakura flex-1 text-center py-2 text-sm">View / Download</a>
|
||||||
|
<button onclick="document.getElementById('file-detail-modal').classList.add('hidden')"
|
||||||
|
class="flex-1 py-2 rounded-full border border-pink-200 text-gray-600 text-sm hover:bg-pink-50">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{-- ── Upload Form (collapsible) ────────────────────────────── --}}
|
{{-- ── Upload Form (collapsible) ────────────────────────────── --}}
|
||||||
<div class="cat-card p-4 mb-4"
|
<div class="cat-card p-4 mb-4" id="upload-panel"
|
||||||
x-data="{
|
x-data="{
|
||||||
open: false,
|
open: false,
|
||||||
uploading: false,
|
uploading: false,
|
||||||
@@ -172,12 +194,14 @@
|
|||||||
<div class="cat-card p-3 flex flex-col gap-2">
|
<div class="cat-card p-3 flex flex-col gap-2">
|
||||||
{{-- Preview / Icon --}}
|
{{-- Preview / Icon --}}
|
||||||
@if($file->is_image)
|
@if($file->is_image)
|
||||||
<div class="w-full aspect-square rounded-2xl overflow-hidden bg-pink-50">
|
<div class="w-full aspect-square rounded-2xl overflow-hidden bg-pink-50 cursor-pointer"
|
||||||
|
onclick="showFileDetail({{ json_encode(['name' => $file->name ?: $file->original_name, 'url' => $file->url, 'size' => $file->human_size, 'folder' => $file->folder, 'client' => $file->client?->name, 'is_image' => $file->is_image, 'icon' => $file->icon, 'created_at' => $file->created_at->format('d M Y')]) }})">
|
||||||
<img src="{{ $file->url }}" alt="{{ $file->name }}"
|
<img src="{{ $file->url }}" alt="{{ $file->name }}"
|
||||||
class="w-full h-full object-cover">
|
class="w-full h-full object-cover">
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<div class="w-full aspect-square rounded-2xl bg-pink-50 flex items-center justify-center text-4xl">
|
<div class="w-full aspect-square rounded-2xl bg-pink-50 flex items-center justify-center text-4xl cursor-pointer"
|
||||||
|
onclick="showFileDetail({{ json_encode(['name' => $file->name ?: $file->original_name, 'url' => $file->url, 'size' => $file->human_size, 'folder' => $file->folder, 'client' => $file->client?->name, 'is_image' => false, 'icon' => $file->icon, 'created_at' => $file->created_at->format('d M Y')]) }})">
|
||||||
{{ $file->icon }}
|
{{ $file->icon }}
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@@ -219,11 +243,42 @@
|
|||||||
<div class="text-center py-16 text-gray-400">
|
<div class="text-center py-16 text-gray-400">
|
||||||
<span class="text-5xl block mb-3">📁</span>
|
<span class="text-5xl block mb-3">📁</span>
|
||||||
<p class="text-sm">No files yet.</p>
|
<p class="text-sm">No files yet.</p>
|
||||||
<button onclick="document.querySelector('[x-data]').__x.$data.open = true"
|
<button onclick="document.getElementById('upload-panel')._x_dataStack[0].open = true; document.getElementById('upload-panel').scrollIntoView({behavior:'smooth'})"
|
||||||
class="btn-sakura mt-4 inline-block text-sm">Upload your first file</button>
|
class="btn-sakura mt-4 inline-block text-sm">Upload your first file</button>
|
||||||
</div>
|
</div>
|
||||||
@endforelse
|
@endforelse
|
||||||
|
|
||||||
<div class="mt-4">{{ $files->links() }}</div>
|
<div class="mt-4">{{ $files->links() }}</div>
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
function showFileDetail(file) {
|
||||||
|
document.getElementById('fdm-name').textContent = file.name;
|
||||||
|
document.getElementById('fdm-view-btn').href = file.url;
|
||||||
|
|
||||||
|
// Preview area
|
||||||
|
const preview = document.getElementById('fdm-preview');
|
||||||
|
if (file.is_image) {
|
||||||
|
preview.innerHTML = `<img src="${file.url}" alt="${file.name}" class="max-h-48 rounded-2xl object-contain border border-pink-100">`;
|
||||||
|
} else {
|
||||||
|
preview.innerHTML = `<span class="text-6xl">${file.icon}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info rows
|
||||||
|
const rows = [
|
||||||
|
['Size', file.size],
|
||||||
|
['Folder', file.folder],
|
||||||
|
file.client ? ['Client', file.client] : null,
|
||||||
|
['Uploaded', file.created_at],
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
document.getElementById('fdm-body').innerHTML = rows.map(([label, value]) =>
|
||||||
|
`<div class="flex gap-2"><span class="text-gray-400 w-16 shrink-0">${label}</span><span class="font-medium text-gray-700 truncate">${value}</span></div>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
document.getElementById('file-detail-modal').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
body { font-family: 'Helvetica', Arial, sans-serif; color: #374151; font-size: 13px; padding: 40px; }
|
body { font-family: 'Helvetica', Arial, sans-serif; color: #374151; font-size: 13px; padding: 40px; }
|
||||||
.header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 40px; }
|
.header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 40px; }
|
||||||
.logo { font-size: 22px; font-weight: 900; color: #FFB7C5; }
|
.logo { font-size: 22px; font-weight: 900; color: {{ $invoiceSettings->accent_color ?? '#FFB7C5' }}; }
|
||||||
.logo-sub { font-size: 11px; color: #9CA3AF; margin-top: 2px; }
|
.logo-sub { font-size: 11px; color: #9CA3AF; margin-top: 2px; }
|
||||||
.inv-number { font-size: 22px; font-weight: 700; color: #1F2937; text-align: right; }
|
.inv-number { font-size: 22px; font-weight: 700; color: #1F2937; text-align: right; }
|
||||||
.badge { display: inline-block; padding: 3px 12px; border-radius: 20px; font-size: 11px; font-weight: 700; background: #FDE68A; color: #92400E; }
|
.badge { display: inline-block; padding: 3px 12px; border-radius: 20px; font-size: 11px; font-weight: 700; background: #FDE68A; color: #92400E; }
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
.totals { display: flex; justify-content: flex-end; margin-bottom: 24px; }
|
.totals { display: flex; justify-content: flex-end; margin-bottom: 24px; }
|
||||||
.totals-box { width: 260px; }
|
.totals-box { width: 260px; }
|
||||||
.totals-row { display: flex; justify-content: space-between; padding: 4px 0; font-size: 12px; }
|
.totals-row { display: flex; justify-content: space-between; padding: 4px 0; font-size: 12px; }
|
||||||
.totals-row.total { border-top: 2px solid #FFB7C5; margin-top: 6px; padding-top: 8px; font-size: 15px; font-weight: 800; color: #FFB7C5; }
|
.totals-row.total { border-top: 2px solid {{ $invoiceSettings->accent_color ?? '#FFB7C5' }}; margin-top: 6px; padding-top: 8px; font-size: 15px; font-weight: 800; color: {{ $invoiceSettings->accent_color ?? '#FFB7C5' }}; }
|
||||||
.bank-box { background: #FFF5F7; border-radius: 12px; padding: 14px 16px; margin-bottom: 16px; }
|
.bank-box { background: #FFF5F7; border-radius: 12px; padding: 14px 16px; margin-bottom: 16px; }
|
||||||
.bank-title { font-weight: 700; font-size: 12px; color: #374151; margin-bottom: 4px; }
|
.bank-title { font-weight: 700; font-size: 12px; color: #374151; margin-bottom: 4px; }
|
||||||
.bank-detail { font-size: 12px; color: #6B7280; }
|
.bank-detail { font-size: 12px; color: #6B7280; }
|
||||||
@@ -91,7 +91,12 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
@foreach($invoice->items as $item)
|
@foreach($invoice->items as $item)
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ $item->description }}</td>
|
<td>
|
||||||
|
{{ $item->description }}
|
||||||
|
@if(!$item->is_taxable)
|
||||||
|
<span style="margin-left:6px; font-size:10px; background:#F3F4F6; color:#6B7280; padding:2px 8px; border-radius:10px; font-weight:600;">Non-Taxable</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
<td style="text-align:center">{{ $item->quantity }}</td>
|
<td style="text-align:center">{{ $item->quantity }}</td>
|
||||||
<td style="text-align:right">Rp {{ number_format($item->unit_price, 0, ',', '.') }}</td>
|
<td style="text-align:right">Rp {{ number_format($item->unit_price, 0, ',', '.') }}</td>
|
||||||
<td style="text-align:right; font-weight:600">Rp {{ number_format($item->total, 0, ',', '.') }}</td>
|
<td style="text-align:right; font-weight:600">Rp {{ number_format($item->total, 0, ',', '.') }}</td>
|
||||||
|
|||||||
@@ -96,6 +96,22 @@
|
|||||||
value="{{ old('header_title', $settings->header_title) }}"
|
value="{{ old('header_title', $settings->header_title) }}"
|
||||||
class="input-soft text-sm" placeholder="Content Creator · KOL">
|
class="input-soft text-sm" placeholder="Content Creator · KOL">
|
||||||
</div>
|
</div>
|
||||||
|
<div x-data="{ color: '{{ old('accent_color', $settings->accent_color ?? '#FFB7C5') }}' }">
|
||||||
|
<label class="block text-xs font-semibold text-gray-600 mb-1">Accent Color</label>
|
||||||
|
<div class="flex items-center gap-3 mt-1">
|
||||||
|
<input type="color" name="accent_color" x-model="color"
|
||||||
|
class="w-10 h-10 rounded-xl cursor-pointer border-2 border-pink-100 p-0.5">
|
||||||
|
<div class="flex gap-2 flex-wrap">
|
||||||
|
@foreach(['#FFB7C5','#B5EAD7','#FDFD96','#E8D5F5','#FFD5B8','#AEC6CF','#FF8FAD','#A8D8EA'] as $preset)
|
||||||
|
<button type="button"
|
||||||
|
class="w-7 h-7 rounded-full ring-2 ring-transparent hover:ring-gray-400 transition-all"
|
||||||
|
style="background:{{ $preset }}"
|
||||||
|
@click="color = '{{ $preset }}'"></button>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-400 mt-1.5">Applied to your invoice header name and totals.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,8 @@
|
|||||||
alt="Logo" class="h-14 w-14 object-cover rounded-2xl border border-pink-100 shrink-0">
|
alt="Logo" class="h-14 w-14 object-cover rounded-2xl border border-pink-100 shrink-0">
|
||||||
@endif
|
@endif
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xl font-extrabold text-pink-500">
|
<div class="text-xl font-extrabold"
|
||||||
|
style="color:{{ $invoiceSettings->accent_color ?? '#FFB7C5' }}">
|
||||||
{{ $invoiceSettings->header_name ?: '🐱 KOL Manager' }}
|
{{ $invoiceSettings->header_name ?: '🐱 KOL Manager' }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-400 mt-0.5">
|
<div class="text-sm text-gray-400 mt-0.5">
|
||||||
@@ -74,7 +75,12 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
@foreach($invoice->items as $item)
|
@foreach($invoice->items as $item)
|
||||||
<tr class="border-b border-pink-50">
|
<tr class="border-b border-pink-50">
|
||||||
<td class="py-3 px-4 text-sm text-gray-700">{{ $item->description }}</td>
|
<td class="py-3 px-4 text-sm text-gray-700">
|
||||||
|
{{ $item->description }}
|
||||||
|
@if(!$item->is_taxable)
|
||||||
|
<span class="ml-1.5 text-xs bg-gray-100 text-gray-500 px-2 py-0.5 rounded-full font-medium">Non-Taxable</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
<td class="py-3 px-4 text-sm text-center text-gray-600">{{ $item->quantity }}</td>
|
<td class="py-3 px-4 text-sm text-center text-gray-600">{{ $item->quantity }}</td>
|
||||||
<td class="py-3 px-4 text-sm text-right text-gray-600">Rp {{ number_format($item->unit_price, 0, ',', '.') }}</td>
|
<td class="py-3 px-4 text-sm text-right text-gray-600">Rp {{ number_format($item->unit_price, 0, ',', '.') }}</td>
|
||||||
<td class="py-3 px-4 text-sm text-right font-semibold text-gray-800">Rp {{ number_format($item->total, 0, ',', '.') }}</td>
|
<td class="py-3 px-4 text-sm text-right font-semibold text-gray-800">Rp {{ number_format($item->total, 0, ',', '.') }}</td>
|
||||||
@@ -95,7 +101,8 @@
|
|||||||
<span class="font-medium">Rp {{ number_format($invoice->tax_amount, 0, ',', '.') }}</span>
|
<span class="font-medium">Rp {{ number_format($invoice->tax_amount, 0, ',', '.') }}</span>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
<div class="flex justify-between text-base font-bold text-pink-600 pt-2 border-t border-pink-200">
|
<div class="flex justify-between text-base font-bold pt-2 border-t border-pink-200"
|
||||||
|
style="color:{{ $invoiceSettings->accent_color ?? '#FFB7C5' }}">
|
||||||
<span>Total (you receive)</span><span>{{ $invoice->formatted_total }}</span>
|
<span>Total (you receive)</span><span>{{ $invoice->formatted_total }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -131,19 +131,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Color --}}
|
{{-- Color --}}
|
||||||
<div>
|
<div x-data="{ color: '{{ old('color', $event->color ?? '#FFB7C5') }}' }">
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-2">Color</label>
|
<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 class="flex gap-2 flex-wrap">
|
<div class="flex gap-2 flex-wrap">
|
||||||
@foreach(['#FFB7C5','#B5EAD7','#FDFD96','#E8D5F5','#FFD5B8','#AEC6CF'] as $hex)
|
@foreach(['#FFB7C5','#B5EAD7','#FDFD96','#E8D5F5','#FFD5B8','#AEC6CF'] as $hex)
|
||||||
<label class="cursor-pointer">
|
<button type="button"
|
||||||
<input type="radio" name="color" value="{{ $hex }}" class="sr-only peer"
|
class="w-8 h-8 rounded-full ring-2 ring-transparent hover:ring-gray-400 transition-all"
|
||||||
{{ old('color', $event->color) === $hex ? 'checked' : '' }}>
|
style="background:{{ $hex }}"
|
||||||
<span class="w-8 h-8 rounded-full block ring-2 ring-transparent peer-checked:ring-gray-500 peer-checked:ring-offset-1 transition-all"
|
@click="color = '{{ $hex }}'"></button>
|
||||||
style="background:{{ $hex }};"></span>
|
|
||||||
</label>
|
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{-- Priority checkbox --}}
|
{{-- Priority checkbox --}}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -159,6 +162,35 @@
|
|||||||
placeholder="Additional notes…">{{ old('notes', $event->notes) }}</textarea>
|
placeholder="Additional notes…">{{ old('notes', $event->notes) }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- Active Contract checkbox --}}
|
||||||
|
<div x-data="{ createContract: false }">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input type="checkbox" name="create_brand_contract" value="1" id="edit_create_contract"
|
||||||
|
class="w-4 h-4 accent-pink-400" x-model="createContract">
|
||||||
|
<label for="edit_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', $event->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', $event->start_time->format('Y-m-d')) }}">
|
||||||
|
</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', $event->end_time?->format('Y-m-d') ?? '') }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col sm:flex-row gap-3 pt-1">
|
<div class="flex flex-col sm:flex-row gap-3 pt-1">
|
||||||
<button type="submit" class="btn-sakura flex-1 py-3">💾 Save Changes</button>
|
<button type="submit" class="btn-sakura flex-1 py-3">💾 Save Changes</button>
|
||||||
<a href="{{ route('schedule.index') }}"
|
<a href="{{ route('schedule.index') }}"
|
||||||
|
|||||||
@@ -85,18 +85,22 @@
|
|||||||
<input type="time" id="end_time_input" class="input-soft mt-1" value="18:00">
|
<input type="time" id="end_time_input" class="input-soft mt-1" value="18:00">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div x-data="{ color: '#FFB7C5' }">
|
||||||
<label class="text-xs font-semibold text-gray-600">Color</label>
|
<label class="text-xs font-semibold text-gray-600">Color</label>
|
||||||
<div class="flex gap-2 mt-1 flex-wrap">
|
<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)
|
@foreach(['#FFB7C5','#B5EAD7','#FDFD96','#E8D5F5','#FFD5B8','#AEC6CF'] as $hex)
|
||||||
<label class="cursor-pointer">
|
<button type="button"
|
||||||
<input type="radio" name="color" value="{{ $hex }}" class="sr-only peer" {{ $loop->first ? 'checked' : '' }}>
|
class="w-7 h-7 rounded-full ring-2 ring-transparent hover:ring-gray-400 transition-all"
|
||||||
<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 }}"
|
||||||
style="background:{{ $hex }};"></span>
|
@click="color = '{{ $hex }}'"></button>
|
||||||
</label>
|
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="flex items-center gap-2 mt-1">
|
<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">
|
<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>
|
<label for="priority" class="text-xs text-gray-600 cursor-pointer">⭐ Priority</label>
|
||||||
@@ -105,6 +109,33 @@
|
|||||||
<label class="text-xs font-semibold text-gray-600">Notes</label>
|
<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>
|
<textarea name="notes" rows="2" class="input-soft mt-1 resize-none text-sm"></textarea>
|
||||||
</div>
|
</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">
|
<div class="sm:col-span-2">
|
||||||
<button type="submit" class="btn-sakura w-full"
|
<button type="submit" class="btn-sakura w-full"
|
||||||
:class="{ 'opacity-50 cursor-not-allowed': contractWarning }">
|
:class="{ 'opacity-50 cursor-not-allowed': contractWarning }">
|
||||||
@@ -121,7 +152,8 @@
|
|||||||
@if($upcomingList->isNotEmpty())
|
@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">
|
<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)
|
@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="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 class="w-1.5 h-8 rounded-full shrink-0" style="background:{{ $event->color }}"></div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs font-semibold text-gray-700 whitespace-nowrap">{{ $event->title }}</div>
|
<div class="text-xs font-semibold text-gray-700 whitespace-nowrap">{{ $event->title }}</div>
|
||||||
@@ -255,6 +287,28 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
// Note: datetime hidden fields are populated in contractCheck's submitIfClear()
|
// Note: datetime hidden fields are populated in contractCheck's submitIfClear()
|
||||||
// since @submit.prevent is used on the form (form.submit() bypasses native submit event)
|
// 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>
|
</script>
|
||||||
|
|
||||||
{{-- Share scripts disabled --}}
|
{{-- Share scripts disabled --}}
|
||||||
|
|||||||
Reference in New Issue
Block a user