diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php
index cfa94fe..73943ab 100644
--- a/app/Http/Controllers/ClientController.php
+++ b/app/Http/Controllers/ClientController.php
@@ -94,13 +94,11 @@ class ClientController extends Controller
public function show(Client $client): View
{
- $client->load([
- 'category',
- 'events' => fn ($q) => $q->latest()->limit(5),
- 'invoices' => fn ($q) => $q->latest()->limit(5),
- 'contentBriefs'=> fn ($q) => $q->latest()->limit(5),
- 'mediaFiles' => fn ($q) => $q->latest()->limit(10),
- ]);
+ $client->load('category');
+ $client->setRelation('events', $client->events()->latest()->limit(5)->get());
+ $client->setRelation('invoices', $client->invoices()->latest()->limit(5)->get());
+ $client->setRelation('contentBriefs', $client->contentBriefs()->latest()->limit(5)->get());
+ $client->setRelation('mediaFiles', $client->mediaFiles()->latest()->limit(10)->get());
return view('clients.show', compact('client'));
}
diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php
index 785953f..bf90314 100644
--- a/app/Http/Controllers/InvoiceController.php
+++ b/app/Http/Controllers/InvoiceController.php
@@ -28,10 +28,9 @@ class InvoiceController extends Controller
public function create(): View
{
- $clients = Client::where('status', 'active')->orderBy('name')->get();
- $statuses = InvoiceStatus::cases();
- $invoiceSettings = InvoiceSettings::instance();
- return view('invoices.create', compact('clients', 'statuses', 'invoiceSettings'));
+ $clients = Client::where('status', 'active')->orderBy('name')->get();
+ $statuses = InvoiceStatus::cases();
+ return view('invoices.create', compact('clients', 'statuses'));
}
public function store(Request $request): RedirectResponse
@@ -84,17 +83,15 @@ class InvoiceController extends Controller
public function show(Invoice $invoice): View
{
$invoice->load('client', 'items');
- $invoiceSettings = InvoiceSettings::instance();
- return view('invoices.show', compact('invoice', 'invoiceSettings'));
+ return view('invoices.show', compact('invoice'));
}
public function edit(Invoice $invoice): View
{
- $clients = Client::where('status', 'active')->orderBy('name')->get();
- $statuses = InvoiceStatus::cases();
- $invoiceSettings = InvoiceSettings::instance();
+ $clients = Client::where('status', 'active')->orderBy('name')->get();
+ $statuses = InvoiceStatus::cases();
$invoice->load('client', 'items');
- return view('invoices.edit', compact('invoice', 'clients', 'statuses', 'invoiceSettings'));
+ return view('invoices.edit', compact('invoice', 'clients', 'statuses'));
}
public function update(Request $request, Invoice $invoice): RedirectResponse
diff --git a/app/Http/Controllers/RateCardController.php b/app/Http/Controllers/RateCardController.php
index 321ca18..2ce27ca 100644
--- a/app/Http/Controllers/RateCardController.php
+++ b/app/Http/Controllers/RateCardController.php
@@ -17,8 +17,7 @@ class RateCardController extends Controller
public function index(): View
{
$rateCards = RateCard::orderBy('sort_order')->orderBy('platform')->get();
- $profile = RateCardProfile::getSingleton();
- return view('rate-cards.index', compact('rateCards', 'profile'));
+ return view('rate-cards.index', compact('rateCards'));
}
public function create(): View
@@ -88,23 +87,21 @@ class RateCardController extends Controller
public function public(): View
{
- $profile = RateCardProfile::getSingleton();
$rateCards = RateCard::where('is_active', true)
->orderBy('sort_order')
->orderBy('platform')
->get()
->groupBy('platform');
- return view('rate-cards.public', compact('rateCards', 'profile'));
+ return view('rate-cards.public', compact('rateCards'));
}
// ── Profile Settings ─────────────────────────────────────────
public function profilePage(): View
{
- $profile = RateCardProfile::getSingleton();
$mediaFiles = \App\Models\MediaFile::latest()->get();
- return view('rate-cards.profile', compact('profile', 'mediaFiles'));
+ return view('rate-cards.profile', compact('mediaFiles'));
}
public function profileUpdate(Request $request): RedirectResponse
@@ -143,7 +140,7 @@ class RateCardController extends Controller
}
$validated['show_prices'] = $request->has('show_prices');
- $contentShowcase = $request->input('content_showcase', []);
+ $contentShowcase = $validated['content_showcase'] ?? [];
foreach ($contentShowcase as &$item) {
$item['show_on_public'] = filter_var($item['show_on_public'] ?? true, FILTER_VALIDATE_BOOLEAN);
$item['show_on_pdf'] = filter_var($item['show_on_pdf'] ?? true, FILTER_VALIDATE_BOOLEAN);
@@ -206,6 +203,7 @@ class RateCardController extends Controller
public function deleteBrandLogo(Request $request)
{
+ $request->validate(['path' => 'required|string']);
$path = $request->input('path');
$profile = RateCardProfile::getSingleton();
$brands = $profile->past_brands ?? [];
diff --git a/app/Models/InvoiceSettings.php b/app/Models/InvoiceSettings.php
index d516f5e..c0c5d9d 100644
--- a/app/Models/InvoiceSettings.php
+++ b/app/Models/InvoiceSettings.php
@@ -35,25 +35,39 @@ class InvoiceSettings extends Model
'default_tax_rate' => 'decimal:2',
];
+ protected static ?self $instance = null;
+
/** Always return the single settings row, creating it if absent. */
public static function instance(): self
{
- return static::firstOrCreate([], [
+ if (self::$instance) {
+ return self::$instance;
+ }
+
+ self::$instance = static::firstOrCreate([], [
'invoice_prefix' => 'INV',
'invoice_next_number' => 1,
'accent_color' => '#FFB7C5',
'default_tax_rate' => 2.5,
'default_currency' => 'IDR',
]);
+
+ return self::$instance;
+ }
+
+ protected static function booted()
+ {
+ static::saved(fn ($model) => self::$instance = $model);
+ static::deleted(fn () => self::$instance = null);
}
- /** Generate the next invoice number and increment the counter. */
public static function generateNumber(): string
{
$settings = static::instance();
$prefix = rtrim($settings->invoice_prefix, '-');
$number = str_pad((string) $settings->invoice_next_number, 3, '0', STR_PAD_LEFT);
$settings->increment('invoice_next_number');
+ \Illuminate\Support\Facades\Cache::forget('invoice_settings');
return $prefix . '-' . $number;
}
}
diff --git a/app/Models/RateCardProfile.php b/app/Models/RateCardProfile.php
index 1e368ae..bc1f9be 100644
--- a/app/Models/RateCardProfile.php
+++ b/app/Models/RateCardProfile.php
@@ -26,16 +26,30 @@ class RateCardProfile extends Model
'content_showcase' => 'array',
];
+ protected static ?self $instance = null;
+
/** Get the singleton profile, creating defaults if needed */
public static function getSingleton(): self
{
- return static::firstOrCreate([], [
+ if (self::$instance) {
+ return self::$instance;
+ }
+
+ self::$instance = static::firstOrCreate([], [
'owner_name' => config('app.name', 'Cathy Page'),
'bg_color' => '#FFF8F0',
'accent_color' => '#FFB7C5',
'text_color' => '#374151',
'show_prices' => true,
]);
+
+ return self::$instance;
+ }
+
+ protected static function booted()
+ {
+ static::saved(fn ($model) => self::$instance = $model);
+ static::deleted(fn () => self::$instance = null);
}
public function getWhatsappUrlAttribute(): ?string
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 8a9a502..4789468 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -16,6 +16,13 @@ class AppServiceProvider extends ServiceProvider
{
// Inject alert badge count into all views
View::composer('*', function (\Illuminate\View\View $view): void {
+ if (!$view->offsetExists('profile')) {
+ $view->with('profile', \App\Models\RateCardProfile::getSingleton());
+ }
+ if (!$view->offsetExists('invoiceSettings')) {
+ $view->with('invoiceSettings', \App\Models\InvoiceSettings::instance());
+ }
+
try {
$alertCount = Event::upcoming(24)->count();
} catch (\Throwable) {
diff --git a/resources/views/rate-cards/pdf.blade.php b/resources/views/rate-cards/pdf.blade.php
index 27f3479..08a0354 100644
--- a/resources/views/rate-cards/pdf.blade.php
+++ b/resources/views/rate-cards/pdf.blade.php
@@ -315,23 +315,23 @@
$file = !$isUrl ? \App\Models\MediaFile::find($item['file_id']) : null;
@endphp
@if($file || $isUrl)
-
-
+
+
@if(!empty($item['thumbnail_path']) && file_exists(storage_path('app/public/' . $item['thumbnail_path'])))
-
+
@if(!$isUrl && $file->is_video)
@endif
@elseif(!$isUrl && $file->is_image && file_exists(storage_path('app/private/' . $file->path)))
-
+
@else
-
+
{{ $isUrl ? '🌐' : ($file->is_video ? '🎬' : '📁') }}
{{ $isUrl ? 'Open Link' : 'View File' }}
diff --git a/resources/views/rate-cards/public.blade.php b/resources/views/rate-cards/public.blade.php
index b6c5e4e..79972ab 100644
--- a/resources/views/rate-cards/public.blade.php
+++ b/resources/views/rate-cards/public.blade.php
@@ -111,6 +111,63 @@
gap: 14px;
}
+ .carousel-wrapper {
+ position: relative;
+ }
+
+ .carousel-track {
+ display: flex;
+ overflow-x: auto;
+ scroll-snap-type: x mandatory;
+ gap: 14px;
+ padding-bottom: 10px;
+ scrollbar-width: none;
+ }
+
+ .carousel-track::-webkit-scrollbar {
+ display: none;
+ }
+
+ .carousel-slide {
+ flex: 0 0 220px;
+ scroll-snap-align: start;
+ }
+
+ .carousel-btn {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ background: white;
+ border: 1px solid rgba(0,0,0,0.05);
+ box-shadow: 0 4px 10px rgba(0,0,0,0.1);
+ color:
+ {{ $profile->accent_color }}
+ ;
+ font-size: 20px;
+ font-weight: 900;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ z-index: 10;
+ }
+
+ .carousel-btn:hover {
+ background: #fdfdfd;
+ box-shadow: 0 6px 14px rgba(0,0,0,0.15);
+ }
+
+ .carousel-btn.prev {
+ left: -20px;
+ }
+
+ .carousel-btn.next {
+ right: -20px;
+ }
+
.card {
background: white;
border-radius: 24px;
@@ -123,6 +180,17 @@
22;
}
+ .popup-modal {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.8);
+ z-index: 9999;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 20px;
+ }
+
.service {
font-weight: 700;
color: #1F2937;
@@ -337,22 +405,32 @@
@if(!empty($publicShowcase))