From 1c2c10d253917895233cbe1d3c439054a1f37a70 Mon Sep 17 00:00:00 2001 From: I-Fun Date: Sun, 5 Apr 2026 18:28:48 +0700 Subject: [PATCH] feat: extend rate card profiles with brands, terms, and custom HTML, and remove search bar from search index view --- Dockerfile | 4 +- app/Http/Controllers/ClientController.php | 2 +- app/Http/Controllers/MediaFileController.php | 2 +- app/Http/Controllers/RateCardController.php | 74 +++- app/Http/Controllers/SearchController.php | 22 +- app/Models/InvoiceSettings.php | 14 +- app/Models/RateCardProfile.php | 5 + ...terms_and_brands_to_rate_card_profiles.php | 29 ++ ...tact_person_html_to_rate_card_profiles.php | 22 + ..._brand_logo_size_to_rate_card_profiles.php | 22 + docker/nginx.conf | 2 +- resources/views/clients/index.blade.php | 1 + resources/views/clients/show.blade.php | 35 +- resources/views/content/index.blade.php | 127 +++--- resources/views/layouts/app.blade.php | 15 +- resources/views/notes/index.blade.php | 33 +- resources/views/rate-cards/pdf.blade.php | 404 +++++++++++++----- resources/views/rate-cards/profile.blade.php | 267 ++++++++++-- resources/views/rate-cards/public.blade.php | 57 ++- resources/views/schedule/create.blade.php | 181 ++++---- resources/views/search/index.blade.php | 90 ++-- routes/web.php | 4 + 22 files changed, 1024 insertions(+), 388 deletions(-) create mode 100644 database/migrations/2026_04_05_101357_add_terms_and_brands_to_rate_card_profiles.php create mode 100644 database/migrations/2026_04_05_104100_add_contact_person_html_to_rate_card_profiles.php create mode 100644 database/migrations/2026_04_05_110200_add_brand_logo_size_to_rate_card_profiles.php diff --git a/Dockerfile b/Dockerfile index 9e10dbe..ec793db 100644 --- a/Dockerfile +++ b/Dockerfile @@ -120,8 +120,8 @@ RUN { \ } > /usr/local/etc/php/conf.d/opcache.ini RUN { \ - echo "upload_max_filesize = 200M"; \ - echo "post_max_size = 200M"; \ + echo "upload_max_filesize = 500M"; \ + echo "post_max_size = 500M"; \ echo "memory_limit = 512M"; \ echo "max_execution_time = 300"; \ } > /usr/local/etc/php/conf.d/uploads.ini diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index c039079..cfa94fe 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -38,7 +38,7 @@ class ClientController extends Controller public function index(Request $request): View { $query = Client::with('category') - ->withCount(['invoices', 'events', 'contentBriefs']) + ->withCount(['invoices', 'events', 'contentBriefs', 'mediaFiles']) ->latest(); if ($request->filled('category')) { diff --git a/app/Http/Controllers/MediaFileController.php b/app/Http/Controllers/MediaFileController.php index a3a9699..880146b 100644 --- a/app/Http/Controllers/MediaFileController.php +++ b/app/Http/Controllers/MediaFileController.php @@ -52,7 +52,7 @@ class MediaFileController extends Controller $this->fileService->uploadAsset( $request->file('file'), $request->input('folder'), - $request->input('client_id') + $request->filled('client_id') ? (int) $request->input('client_id') : null ); if ($request->expectsJson() || $request->ajax()) { diff --git a/app/Http/Controllers/RateCardController.php b/app/Http/Controllers/RateCardController.php index 8f08c87..4b899fd 100644 --- a/app/Http/Controllers/RateCardController.php +++ b/app/Http/Controllers/RateCardController.php @@ -119,8 +119,11 @@ class RateCardController extends Controller 'bg_color' => 'nullable|string|max:20', 'accent_color' => 'nullable|string|max:20', 'text_color' => 'nullable|string|max:20', - 'custom_footer' => 'nullable|string|max:500', - 'show_prices' => 'boolean', + 'custom_footer' => 'nullable|string|max:255', + 'show_prices' => 'nullable|boolean', + 'terms_and_conditions' => 'nullable|string', + 'brand_logo_size' => 'nullable|in:small,medium,large', + 'contact_person_html' => 'nullable|string', 'logo' => 'nullable|image|max:2048', ]); @@ -131,7 +134,7 @@ class RateCardController extends Controller $validated['logo_path'] = $path; } - $validated['show_prices'] = $request->boolean('show_prices'); + $validated['show_prices'] = $request->has('show_prices'); unset($validated['logo']); $profile->update($validated); @@ -139,6 +142,69 @@ class RateCardController extends Controller return back()->with('success', '🎨 Profile updated!'); } + public function uploadLogo(Request $request) + { + $request->validate([ + 'logo' => 'required|image|max:2048', + ]); + + $profile = RateCardProfile::getSingleton(); + + if ($profile->logo_path) { + \Storage::disk('private')->delete($profile->logo_path); + } + + $path = $request->file('logo')->store('profile', 'private'); + $profile->update(['logo_path' => $path]); + + return response()->json([ + 'url' => route('files.serve-profile-logo'), + ]); + } + + public function deleteLogo() + { + $profile = RateCardProfile::getSingleton(); + + if ($profile->logo_path) { + \Storage::disk('private')->delete($profile->logo_path); + $profile->update(['logo_path' => null]); + } + + return response()->json(['success' => true]); + } + + public function uploadBrandLogo(Request $request) + { + $request->validate(['logo' => 'required|image|max:2048']); + $profile = RateCardProfile::getSingleton(); + $path = $request->file('logo')->store('brands', 'public'); + + $brands = $profile->past_brands ?? []; + $brands[] = $path; + $profile->update(['past_brands' => $brands]); + + return response()->json([ + 'path' => $path, + 'url' => asset('storage/' . $path) + ]); + } + + public function deleteBrandLogo(Request $request) + { + $path = $request->input('path'); + $profile = RateCardProfile::getSingleton(); + $brands = $profile->past_brands ?? []; + + if (($key = array_search($path, $brands)) !== false) { + unset($brands[$key]); + $profile->update(['past_brands' => array_values($brands)]); + Storage::disk('public')->delete($path); + } + + return response()->json(['success' => true]); + } + public function serveProfileLogo(): Response { $profile = RateCardProfile::getSingleton(); @@ -165,7 +231,7 @@ class RateCardController extends Controller ->groupBy('platform'); $pdf = Pdf::loadView('rate-cards.pdf', compact('rateCards', 'profile')) - ->setPaper('a4', 'portrait'); + ->setPaper('a4', 'landscape'); $filename = 'rate-card-' . now()->format('Y-m-d') . '.pdf'; diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index 7ddf777..e371e89 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -17,7 +17,7 @@ class SearchController extends Controller { public function index(Request $request): View { - $q = trim($request->get('q', '')); + $q = trim((string) $request->get('q', '')); $results = $q ? $this->search($q) : []; return view('search.index', compact('q', 'results')); @@ -26,7 +26,7 @@ class SearchController extends Controller /** Quick AJAX endpoint β€” returns grouped results for the dropdown */ public function quick(Request $request): JsonResponse { - $q = trim($request->get('q', '')); + $q = trim((string) $request->get('q', '')); if (strlen($q) < 2) { return response()->json([]); @@ -138,6 +138,24 @@ class SearchController extends Controller ]; } + // ── Notes ─────────────────────────────────────────── + $notes = \App\Models\Note::where('title', 'LIKE', $like) + ->orWhere('content', 'LIKE', $like) + ->limit($limit)->get(); + + if ($notes->isNotEmpty()) { + $groups[] = [ + 'type' => 'notes', + 'label' => 'πŸ“ Notes', + 'items' => $notes->map(fn ($n) => [ + 'icon' => 'πŸ“', + 'title' => $n->title ?: 'Untitled', + 'subtitle' => 'Notes Management', + 'url' => route('notes.index'), + ])->values()->all(), + ]; + } + return $groups; } } diff --git a/app/Models/InvoiceSettings.php b/app/Models/InvoiceSettings.php index 6a7a941..d516f5e 100644 --- a/app/Models/InvoiceSettings.php +++ b/app/Models/InvoiceSettings.php @@ -32,18 +32,18 @@ class InvoiceSettings extends Model protected $casts = [ 'invoice_next_number' => 'integer', - 'default_tax_rate' => 'decimal:2', + 'default_tax_rate' => 'decimal:2', ]; /** Always return the single settings row, creating it if absent. */ public static function instance(): self { return static::firstOrCreate([], [ - 'invoice_prefix' => 'INV', + 'invoice_prefix' => 'INV', 'invoice_next_number' => 1, - 'accent_color' => '#FFB7C5', - 'default_tax_rate' => 11, - 'default_currency' => 'IDR', + 'accent_color' => '#FFB7C5', + 'default_tax_rate' => 2.5, + 'default_currency' => 'IDR', ]); } @@ -51,8 +51,8 @@ class InvoiceSettings extends Model 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); + $prefix = rtrim($settings->invoice_prefix, '-'); + $number = str_pad((string) $settings->invoice_next_number, 3, '0', STR_PAD_LEFT); $settings->increment('invoice_next_number'); return $prefix . '-' . $number; } diff --git a/app/Models/RateCardProfile.php b/app/Models/RateCardProfile.php index e569f65..0d82120 100644 --- a/app/Models/RateCardProfile.php +++ b/app/Models/RateCardProfile.php @@ -13,10 +13,15 @@ class RateCardProfile extends Model 'ig_handle', 'tiktok_handle', 'youtube_handle', 'email', 'logo_path', 'bg_color', 'accent_color', 'text_color', 'custom_footer', 'show_prices', 'access_token', + 'terms_and_conditions', + 'past_brands', + 'brand_logo_size', + 'contact_person_html', ]; protected $casts = [ 'show_prices' => 'boolean', + 'past_brands' => 'array', ]; /** Get the singleton profile, creating defaults if needed */ diff --git a/database/migrations/2026_04_05_101357_add_terms_and_brands_to_rate_card_profiles.php b/database/migrations/2026_04_05_101357_add_terms_and_brands_to_rate_card_profiles.php new file mode 100644 index 0000000..bb050bb --- /dev/null +++ b/database/migrations/2026_04_05_101357_add_terms_and_brands_to_rate_card_profiles.php @@ -0,0 +1,29 @@ +text('terms_and_conditions')->nullable(); + $table->json('past_brands')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('rate_card_profiles', function (Blueprint $table) { + $table->dropColumn(['terms_and_conditions', 'past_brands']); + }); + } +}; diff --git a/database/migrations/2026_04_05_104100_add_contact_person_html_to_rate_card_profiles.php b/database/migrations/2026_04_05_104100_add_contact_person_html_to_rate_card_profiles.php new file mode 100644 index 0000000..38e57f4 --- /dev/null +++ b/database/migrations/2026_04_05_104100_add_contact_person_html_to_rate_card_profiles.php @@ -0,0 +1,22 @@ +text('contact_person_html')->nullable(); + }); + } + + public function down(): void + { + Schema::table('rate_card_profiles', function (Blueprint $table) { + $table->dropColumn('contact_person_html'); + }); + } +}; diff --git a/database/migrations/2026_04_05_110200_add_brand_logo_size_to_rate_card_profiles.php b/database/migrations/2026_04_05_110200_add_brand_logo_size_to_rate_card_profiles.php new file mode 100644 index 0000000..3a1b2f7 --- /dev/null +++ b/database/migrations/2026_04_05_110200_add_brand_logo_size_to_rate_card_profiles.php @@ -0,0 +1,22 @@ +string('brand_logo_size')->default('medium')->after('past_brands'); + }); + } + + public function down(): void + { + Schema::table('rate_card_profiles', function (Blueprint $table) { + $table->dropColumn('brand_logo_size'); + }); + } +}; diff --git a/docker/nginx.conf b/docker/nginx.conf index 157befd..2543939 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -24,7 +24,7 @@ http { gzip_types text/plain text/css application/json application/javascript text/xml application/xml; # Upload size limit (matches file uploads in the app) - client_max_body_size 64M; + client_max_body_size 500M; server { listen 80; diff --git a/resources/views/clients/index.blade.php b/resources/views/clients/index.blade.php index ad29ddf..84673ed 100644 --- a/resources/views/clients/index.blade.php +++ b/resources/views/clients/index.blade.php @@ -138,6 +138,7 @@ πŸ“… {{ $client->events_count }} 🧾 {{ $client->invoices_count }} 🎬 {{ $client->content_briefs_count }} + πŸ“ {{ $client->media_files_count ?? 0 }} @if($client->email || $client->city) diff --git a/resources/views/clients/show.blade.php b/resources/views/clients/show.blade.php index d3ad498..6b58234 100644 --- a/resources/views/clients/show.blade.php +++ b/resources/views/clients/show.blade.php @@ -76,6 +76,13 @@ … + + {{-- Remove Photo Button (Top Left) --}} +
@@ -102,10 +109,6 @@ class="text-sm px-4 py-2 rounded-full border border-pink-200 text-gray-600 hover:bg-pink-50 font-semibold"> 🧾 New Invoice -
@@ -202,5 +205,29 @@ @endif + + {{-- Uploaded Files --}} + @if($client->mediaFiles->isNotEmpty()) +
+
+

πŸ“ Files

+ All Files β†’ +
+ +
+ @endif @endsection diff --git a/resources/views/content/index.blade.php b/resources/views/content/index.blade.php index c3019fa..3d01172 100644 --- a/resources/views/content/index.blade.php +++ b/resources/views/content/index.blade.php @@ -11,7 +11,7 @@ @foreach(['instagram', 'tiktok', 'youtube'] as $p) {{ ['instagram' => 'πŸ“Έ', 'tiktok' => '🎡', 'youtube' => '▢️'][$p] }} {{ ucfirst($p) }} @@ -50,105 +50,114 @@ {{-- Sortable items container --}} -
+
@foreach($col['items'] as $brief) -
-
- {{ $brief->platform_icon }} - - {{ $brief->title }} - -
- @if($brief->client) -
πŸ‘₯ {{ $brief->client->name }}
- @endif - @if($brief->target_date) -
- πŸ“… {{ $brief->target_date->format('d M') }} +
+
+ {{ $brief->platform_icon }} + + {{ $brief->title }} + +
+ @if($brief->client) +
πŸ‘₯ {{ $brief->client->name }}
+ @endif + @if($brief->target_date) +
+ πŸ“… {{ $brief->target_date->format('d M') }} +
+ @endif +
+ Edit
- @endif -
- Edit
-
@endforeach
{{-- Add button --}} + border-2 border-dashed border-pink-100 rounded-2xl hover:border-pink-300 transition-colors block"> οΌ‹ Add
@endforeach {{-- Content Detail Request Modal --}} -