From 22f03ccca7df1d32b9353da8617f13975b8bd23c Mon Sep 17 00:00:00 2001 From: I-Fun Date: Wed, 1 Apr 2026 21:47:34 +0700 Subject: [PATCH] Add contract management and invoice settings pages - Created a new Blade view for managing brand contracts with forms for adding and editing contracts, including client and category selection. - Implemented success message notifications for contract actions. - Added a new Blade view for invoice settings, allowing customization of invoice number format, header branding, and default values for new invoices. - Introduced a new PDF view for generating rate cards with customizable styles based on user profile settings. - Developed a profile management page for rate cards, enabling users to update their identity, appearance, and visibility settings. --- .claude/settings.local.json | 3 +- .../Controllers/BrandContractController.php | 126 +++++++++++ .../Controllers/ClientCategoryController.php | 14 +- .../Controllers/ContentBriefController.php | 30 ++- app/Http/Controllers/EventController.php | 25 +- app/Http/Controllers/InvoiceController.php | 36 +-- .../Controllers/InvoiceSettingsController.php | 55 +++++ app/Http/Controllers/MediaFileController.php | 6 +- .../NotificationSettingController.php | 26 +-- app/Http/Controllers/RateCardController.php | 119 ++++++++-- app/Models/BrandContract.php | 75 ++++++ app/Models/ContentBrief.php | 7 +- app/Models/Event.php | 7 +- app/Models/Invoice.php | 19 +- app/Models/InvoiceItem.php | 3 +- app/Models/InvoiceSettings.php | 54 +++++ app/Models/RateCardProfile.php | 47 ++++ app/Services/DashboardService.php | 10 +- app/Services/InvoiceService.php | 10 +- app/Services/Notifications/WahaNotifier.php | 29 ++- ..._category_to_events_and_content_briefs.php | 37 +++ ...01_000110_create_brand_contracts_table.php | 33 +++ ...000120_create_rate_card_profiles_table.php | 37 +++ ...6_add_invoice_settings_and_branch_name.php | 45 ++++ ...43218_add_defaults_to_invoice_settings.php | 36 +++ resources/css/app.css | 34 +++ resources/views/categories/index.blade.php | 213 ++++++++++++++++++ resources/views/clients/index.blade.php | 120 +++++----- resources/views/content/create.blade.php | 60 ++++- resources/views/content/edit.blade.php | 16 ++ resources/views/contracts/index.blade.php | 212 +++++++++++++++++ resources/views/dashboard.blade.php | 33 ++- resources/views/files/index.blade.php | 179 +++++++++++---- resources/views/invoices/create.blade.php | 72 ++++-- resources/views/invoices/edit.blade.php | 49 +++- resources/views/invoices/index.blade.php | 11 +- resources/views/invoices/pdf.blade.php | 26 ++- resources/views/invoices/settings.blade.php | 175 ++++++++++++++ resources/views/invoices/show.blade.php | 33 ++- resources/views/layouts/app.blade.php | 12 + resources/views/rate-cards/index.blade.php | 26 ++- resources/views/rate-cards/pdf.blade.php | 142 ++++++++++++ resources/views/rate-cards/profile.blade.php | 172 ++++++++++++++ resources/views/rate-cards/public.blade.php | 79 +++++-- resources/views/schedule/edit.blade.php | 22 +- resources/views/schedule/index.blade.php | 64 +++++- .../views/settings/notifications.blade.php | 54 +++-- routes/web.php | 18 ++ 48 files changed, 2415 insertions(+), 296 deletions(-) create mode 100644 app/Http/Controllers/BrandContractController.php create mode 100644 app/Http/Controllers/InvoiceSettingsController.php create mode 100644 app/Models/BrandContract.php create mode 100644 app/Models/InvoiceSettings.php create mode 100644 app/Models/RateCardProfile.php create mode 100644 database/migrations/2024_01_01_000100_add_category_to_events_and_content_briefs.php create mode 100644 database/migrations/2024_01_01_000110_create_brand_contracts_table.php create mode 100644 database/migrations/2024_01_01_000120_create_rate_card_profiles_table.php create mode 100644 database/migrations/2026_04_01_104816_add_invoice_settings_and_branch_name.php create mode 100644 database/migrations/2026_04_01_143218_add_defaults_to_invoice_settings.php create mode 100644 resources/views/categories/index.blade.php create mode 100644 resources/views/contracts/index.blade.php create mode 100644 resources/views/invoices/settings.blade.php create mode 100644 resources/views/rate-cards/pdf.blade.php create mode 100644 resources/views/rate-cards/profile.blade.php diff --git a/.claude/settings.local.json b/.claude/settings.local.json index bad401d..aca9b84 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -13,7 +13,8 @@ "Bash(NODE_BIN=\"/c/laragon/bin/nodejs/node-v22\" cd \"E:/_PERSONAL-PROJECT_/PHP/KOL-Manager\" \"$NODE_BIN/node.exe\" \"$NODE_BIN/node_modules/npm/bin/npm-cli.js\" run build)", "Bash(/c/laragon/bin/nodejs/node-v22/node node_modules/npm/bin/npm-cli.js run build)", "Bash(/c/laragon/bin/nodejs/node-v22/node /c/laragon/bin/nodejs/node-v22/node_modules/npm/bin/npm-cli.js run build)", - "Bash(php artisan:*)" + "Bash(php artisan:*)", + "Bash(php -r \":*)" ] } } diff --git a/app/Http/Controllers/BrandContractController.php b/app/Http/Controllers/BrandContractController.php new file mode 100644 index 0000000..542241f --- /dev/null +++ b/app/Http/Controllers/BrandContractController.php @@ -0,0 +1,126 @@ +whereNull('parent_id') + ->orderBy('sort_order') + ->get(); + } + + public function index(): View + { + $contracts = BrandContract::with(['client', 'category']) + ->orderByDesc('end_date') + ->get(); + $clients = Client::orderBy('name')->get(); + $categoryTree = $this->categoryTree(); + + return view('contracts.index', compact('contracts', 'clients', 'categoryTree')); + } + + public function store(Request $request): RedirectResponse + { + $validated = $request->validate([ + 'title' => 'required|string|max:255', + 'client_id' => 'nullable|exists:clients,id', + 'category_id' => 'required|exists:client_categories,id', + 'start_date' => 'required|date', + 'end_date' => 'required|date|after_or_equal:start_date', + 'notes' => 'nullable|string', + ]); + + $validated['is_active'] = true; + BrandContract::create($validated); + + return back()->with('success', 'πŸ“ Contract added!'); + } + + public function update(Request $request, BrandContract $contract): RedirectResponse + { + $validated = $request->validate([ + 'title' => 'required|string|max:255', + 'client_id' => 'nullable|exists:clients,id', + 'category_id' => 'required|exists:client_categories,id', + 'start_date' => 'required|date', + 'end_date' => 'required|date|after_or_equal:start_date', + 'notes' => 'nullable|string', + 'is_active' => 'boolean', + ]); + + $validated['is_active'] = $request->boolean('is_active', $contract->is_active); + $contract->update($validated); + + return back()->with('success', 'πŸ“ Contract updated!'); + } + + public function destroy(BrandContract $contract): RedirectResponse + { + $contract->delete(); + return back()->with('success', 'πŸ“ Contract removed.'); + } + + public function toggle(BrandContract $contract): RedirectResponse + { + $contract->update(['is_active' => ! $contract->is_active]); + return back()->with('success', $contract->is_active ? 'βœ… Contract activated.' : '⏸️ Contract paused.'); + } + + /** + * AJAX: check if a category has an active exclusive contract. + * Returns null if clear, or contract details if blocked. + */ + public function check(Request $request): JsonResponse + { + $categoryId = $request->integer('category_id'); + $clientId = $request->integer('client_id'); + + if (! $categoryId) { + return response()->json(null); + } + + // Collect the category and its parent chain (so blocking a parent also blocks children) + $category = ClientCategory::find($categoryId); + $checkIds = [$categoryId]; + if ($category?->parent_id) { + $checkIds[] = $category->parent_id; + } + + $contract = BrandContract::with(['client', 'category']) + ->active() + ->whereIn('category_id', $checkIds) + ->first(); + + if (! $contract) { + return response()->json(null); + } + + // Allow if it's the contracted brand itself + if ($clientId && $contract->client_id && $contract->client_id === $clientId) { + return response()->json(null); + } + + return response()->json([ + 'id' => $contract->id, + 'title' => $contract->title, + 'client_name' => $contract->client?->name ?? 'Unknown', + 'category' => $contract->category?->name ?? 'Unknown', + 'end_date' => $contract->end_date->format('d M Y'), + 'days_left' => $contract->days_remaining, + ]); + } +} diff --git a/app/Http/Controllers/ClientCategoryController.php b/app/Http/Controllers/ClientCategoryController.php index 46cfad7..773fc5a 100644 --- a/app/Http/Controllers/ClientCategoryController.php +++ b/app/Http/Controllers/ClientCategoryController.php @@ -10,10 +10,18 @@ use Illuminate\Http\Request; class ClientCategoryController extends Controller { - public function index() + public function index(): \Illuminate\View\View { - // Managed inline from the clients page; redirect there - return redirect()->route('clients.index'); + $categories = ClientCategory::with('children', 'parent') + ->withCount('clients') + ->orderBy('sort_order') + ->get(); + + $parents = ClientCategory::whereNull('parent_id') + ->orderBy('sort_order') + ->get(); + + return view('categories.index', compact('categories', 'parents')); } public function store(Request $request): RedirectResponse diff --git a/app/Http/Controllers/ContentBriefController.php b/app/Http/Controllers/ContentBriefController.php index 7afa338..dc80d58 100644 --- a/app/Http/Controllers/ContentBriefController.php +++ b/app/Http/Controllers/ContentBriefController.php @@ -6,6 +6,7 @@ namespace App\Http\Controllers; use App\Enums\ContentStatus; use App\Models\Client; +use App\Models\ClientCategory; use App\Models\ContentBrief; use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; @@ -14,6 +15,14 @@ use Illuminate\View\View; class ContentBriefController extends Controller { + private function categoryTree(): \Illuminate\Support\Collection + { + return ClientCategory::with('children') + ->whereNull('parent_id') + ->orderBy('sort_order') + ->get(); + } + public function index(): View { $clients = Client::where('status', 'active')->orderBy('name')->get(); @@ -32,9 +41,10 @@ class ContentBriefController extends Controller public function create(): View { - $clients = Client::where('status', 'active')->orderBy('name')->get(); - $statuses = ContentStatus::cases(); - return view('content.create', compact('clients', 'statuses')); + $clients = Client::where('status', 'active')->orderBy('name')->get(); + $statuses = ContentStatus::cases(); + $categoryTree = $this->categoryTree(); + return view('content.create', compact('clients', 'statuses', 'categoryTree')); } public function store(Request $request): RedirectResponse @@ -42,6 +52,7 @@ class ContentBriefController extends Controller $validated = $request->validate([ 'title' => 'required|string|max:255', 'client_id' => 'nullable|exists:clients,id', + 'category_id' => 'nullable|exists:client_categories,id', 'platform' => 'required|in:instagram,tiktok,youtube', 'content_type' => 'required|string|max:50', 'status' => 'required|in:drafting,filming,editing,review,posted,archived', @@ -69,10 +80,16 @@ class ContentBriefController extends Controller public function edit(ContentBrief $contentBrief): View { - $clients = Client::where('status', 'active')->orderBy('name')->get(); - $statuses = ContentStatus::cases(); + $clients = Client::where('status', 'active')->orderBy('name')->get(); + $statuses = ContentStatus::cases(); + $categoryTree = $this->categoryTree(); $contentBrief->load('client'); - return view('content.edit', ['brief' => $contentBrief, 'clients' => $clients, 'statuses' => $statuses]); + return view('content.edit', [ + 'brief' => $contentBrief, + 'clients' => $clients, + 'statuses' => $statuses, + 'categoryTree' => $categoryTree, + ]); } public function update(Request $request, ContentBrief $contentBrief): RedirectResponse @@ -80,6 +97,7 @@ class ContentBriefController extends Controller $validated = $request->validate([ 'title' => 'required|string|max:255', 'client_id' => 'nullable|exists:clients,id', + 'category_id' => 'nullable|exists:client_categories,id', 'platform' => 'required|in:instagram,tiktok,youtube', 'content_type' => 'required|string|max:50', 'status' => 'required|in:drafting,filming,editing,review,posted,archived', diff --git a/app/Http/Controllers/EventController.php b/app/Http/Controllers/EventController.php index 851b285..a7c9b0b 100644 --- a/app/Http/Controllers/EventController.php +++ b/app/Http/Controllers/EventController.php @@ -6,6 +6,7 @@ namespace App\Http\Controllers; use App\Enums\EventStatus; use App\Models\Client; +use App\Models\ClientCategory; use App\Models\Event; use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; @@ -14,10 +15,19 @@ use Illuminate\View\View; class EventController extends Controller { + private function categoryTree(): \Illuminate\Support\Collection + { + return ClientCategory::with('children') + ->whereNull('parent_id') + ->orderBy('sort_order') + ->get(); + } + public function index(): View { - $events = Event::with('client')->orderBy('start_time')->get(); - $clients = Client::where('status', 'active')->orderBy('name')->get(); + $events = Event::with('client')->orderBy('start_time')->get(); + $clients = Client::where('status', 'active')->orderBy('name')->get(); + $categoryTree = $this->categoryTree(); // Format for FullCalendar $calendarEvents = $events->map(fn (Event $e) => [ @@ -35,7 +45,7 @@ class EventController extends Controller ], ]); - return view('schedule.index', compact('events', 'clients', 'calendarEvents')); + return view('schedule.index', compact('events', 'clients', 'calendarEvents', 'categoryTree')); } public function store(Request $request): RedirectResponse @@ -44,6 +54,7 @@ class EventController extends Controller 'title' => 'required|string|max:255', 'description' => 'nullable|string', 'client_id' => 'nullable|exists:clients,id', + 'category_id' => 'nullable|exists:client_categories,id', 'event_type' => 'required|in:task,meeting,deadline,posting', 'start_time' => 'required|date', 'end_time' => 'nullable|date|after:start_time', @@ -65,10 +76,11 @@ class EventController extends Controller public function edit(Event $event): View { - $clients = Client::where('status', 'active')->orderBy('name')->get(); - $statuses = EventStatus::cases(); + $clients = Client::where('status', 'active')->orderBy('name')->get(); + $statuses = EventStatus::cases(); + $categoryTree = $this->categoryTree(); - return view('schedule.edit', compact('event', 'clients', 'statuses')); + return view('schedule.edit', compact('event', 'clients', 'statuses', 'categoryTree')); } public function update(Request $request, Event $event): RedirectResponse @@ -77,6 +89,7 @@ class EventController extends Controller 'title' => 'required|string|max:255', 'description' => 'nullable|string', 'client_id' => 'nullable|exists:clients,id', + 'category_id' => 'nullable|exists:client_categories,id', 'event_type' => 'required|in:task,meeting,deadline,posting', 'start_time' => 'required|date', 'end_time' => 'nullable|date', diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index 8d0d7f4..8904bc4 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -7,6 +7,7 @@ namespace App\Http\Controllers; use App\Enums\InvoiceStatus; use App\Models\Client; use App\Models\Invoice; +use App\Models\InvoiceSettings; use App\Services\InvoiceService; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -27,9 +28,10 @@ class InvoiceController extends Controller public function create(): View { - $clients = Client::where('status', 'active')->orderBy('name')->get(); - $statuses = InvoiceStatus::cases(); - return view('invoices.create', compact('clients', 'statuses')); + $clients = Client::where('status', 'active')->orderBy('name')->get(); + $statuses = InvoiceStatus::cases(); + $invoiceSettings = InvoiceSettings::instance(); + return view('invoices.create', compact('clients', 'statuses', 'invoiceSettings')); } public function store(Request $request): RedirectResponse @@ -43,13 +45,15 @@ class InvoiceController extends Controller 'issue_date' => 'required|date', 'due_date' => 'required|date|after_or_equal:issue_date', 'notes' => 'nullable|string', - 'bank_name' => 'nullable|string|max:100', + 'bank_name' => 'nullable|string|max:100', 'bank_account' => 'nullable|string|max:50', - 'bank_holder' => 'nullable|string|max:100', - 'items' => 'required|array|min:1', + 'bank_holder' => 'nullable|string|max:100', + 'branch_name' => 'nullable|string|max:100', + 'items' => 'required|array|min:1', 'items.*.description' => 'required|string', 'items.*.quantity' => 'required|integer|min:1', 'items.*.unit_price' => 'required|numeric|min:0', + 'items.*.is_taxable' => 'nullable|boolean', ]); $items = $validated['items']; @@ -65,15 +69,17 @@ class InvoiceController extends Controller public function show(Invoice $invoice): View { $invoice->load('client', 'items'); - return view('invoices.show', compact('invoice')); + $invoiceSettings = InvoiceSettings::instance(); + return view('invoices.show', compact('invoice', 'invoiceSettings')); } public function edit(Invoice $invoice): View { - $clients = Client::where('status', 'active')->orderBy('name')->get(); - $statuses = InvoiceStatus::cases(); + $clients = Client::where('status', 'active')->orderBy('name')->get(); + $statuses = InvoiceStatus::cases(); + $invoiceSettings = InvoiceSettings::instance(); $invoice->load('client', 'items'); - return view('invoices.edit', compact('invoice', 'clients', 'statuses')); + return view('invoices.edit', compact('invoice', 'clients', 'statuses', 'invoiceSettings')); } public function update(Request $request, Invoice $invoice): RedirectResponse @@ -87,14 +93,16 @@ class InvoiceController extends Controller 'issue_date' => 'required|date', 'due_date' => 'required|date', 'paid_date' => 'nullable|date', - 'notes' => 'nullable|string', - 'bank_name' => 'nullable|string|max:100', + 'notes' => 'nullable|string', + 'bank_name' => 'nullable|string|max:100', 'bank_account' => 'nullable|string|max:50', - 'bank_holder' => 'nullable|string|max:100', - 'items' => 'required|array|min:1', + 'bank_holder' => 'nullable|string|max:100', + 'branch_name' => 'nullable|string|max:100', + 'items' => 'required|array|min:1', 'items.*.description' => 'required|string', 'items.*.quantity' => 'required|integer|min:1', 'items.*.unit_price' => 'required|numeric|min:0', + 'items.*.is_taxable' => 'nullable|boolean', ]); $items = $validated['items']; diff --git a/app/Http/Controllers/InvoiceSettingsController.php b/app/Http/Controllers/InvoiceSettingsController.php new file mode 100644 index 0000000..01e3e03 --- /dev/null +++ b/app/Http/Controllers/InvoiceSettingsController.php @@ -0,0 +1,55 @@ +validate([ + 'invoice_prefix' => 'required|string|max:20', + 'invoice_next_number' => 'required|integer|min:1', + 'header_name' => 'nullable|string|max:100', + 'header_title' => 'nullable|string|max:150', + 'header_logo' => 'nullable|image|max:2048', + 'default_tax_rate' => 'nullable|numeric|min:0|max:100', + 'default_currency' => 'nullable|string|max:10', + 'default_service_description'=> 'nullable|string|max:255', + 'default_bank_name' => 'nullable|string|max:100', + 'default_branch_name' => 'nullable|string|max:100', + 'default_bank_account' => 'nullable|string|max:50', + 'default_bank_holder' => 'nullable|string|max:100', + 'default_notes' => 'nullable|string|max:1000', + ]); + + $settings = InvoiceSettings::instance(); + + if ($request->hasFile('header_logo')) { + // Delete old logo if exists + if ($settings->header_logo) { + Storage::disk('public')->delete($settings->header_logo); + } + $path = $request->file('header_logo')->store('invoice-logos', 'public'); + $validated['header_logo'] = $path; + } + + $settings->update($validated); + + return redirect()->route('invoice-settings.edit') + ->with('success', '🧾 Invoice settings saved!'); + } +} diff --git a/app/Http/Controllers/MediaFileController.php b/app/Http/Controllers/MediaFileController.php index 0ba8a52..a3a9699 100644 --- a/app/Http/Controllers/MediaFileController.php +++ b/app/Http/Controllers/MediaFileController.php @@ -41,7 +41,7 @@ class MediaFileController extends Controller return view('files.index', compact('files', 'folders', 'folder', 'clients')); } - public function store(Request $request): RedirectResponse + public function store(Request $request): RedirectResponse|\Illuminate\Http\JsonResponse { $request->validate([ 'file' => 'required|file|max:51200', // 50 MB @@ -55,6 +55,10 @@ class MediaFileController extends Controller $request->input('client_id') ); + if ($request->expectsJson() || $request->ajax()) { + return response()->json(['success' => true, 'message' => 'File uploaded successfully!']); + } + return back()->with('success', 'πŸ“ File uploaded successfully!'); } diff --git a/app/Http/Controllers/NotificationSettingController.php b/app/Http/Controllers/NotificationSettingController.php index b18c6b2..7c5c3f3 100644 --- a/app/Http/Controllers/NotificationSettingController.php +++ b/app/Http/Controllers/NotificationSettingController.php @@ -23,26 +23,26 @@ class NotificationSettingController extends Controller public function store(Request $request): RedirectResponse { $validated = $request->validate([ - 'name' => 'required|string|max:100', - 'channel' => 'required|in:email,telegram,whatsapp', - 'triggers' => 'nullable|array', - 'triggers.*' => 'in:upcoming_event,invoice_due,content_due', - 'notify_before_hours' => 'nullable|integer|min:1|max:720', + 'name' => 'required|string|max:100', + 'channel' => 'required|in:email,telegram,whatsapp', + 'triggers' => 'nullable|array', + 'triggers.*' => 'in:upcoming_event,invoice_due,content_due', + 'notify_before_hours' => 'nullable|integer|min:1|max:720', // Email - 'config.to_email' => 'required_if:channel,email|nullable|email', + 'config.to_email' => 'required_if:channel,email|nullable|email', // Telegram - 'config.bot_token' => 'required_if:channel,telegram|nullable|string', - 'config.chat_id' => 'required_if:channel,telegram|nullable|string', + 'config.bot_token' => 'required_if:channel,telegram|nullable|string', + 'config.chat_id' => 'nullable|string', // WhatsApp (WAHA) - 'config.waha_url' => 'required_if:channel,whatsapp|nullable|url', - 'config.waha_session' => 'required_if:channel,whatsapp|nullable|string', - 'config.phone' => 'required_if:channel,whatsapp|nullable|string', + 'config.waha_endpoint' => 'required_if:channel,whatsapp|nullable|url', + 'config.waha_session' => 'nullable|string', + 'config.api_key' => 'nullable|string', ]); NotificationSetting::create([ 'name' => $validated['name'], 'channel' => $validated['channel'], - 'config' => $validated['config'] ?? [], + 'config' => $request->input('config', []), // pass raw config β€” not stripped by validation 'triggers' => $validated['triggers'] ?? [], 'notify_before_hours' => $validated['notify_before_hours'] ?? 24, 'is_enabled' => true, @@ -65,7 +65,7 @@ class NotificationSettingController extends Controller 'name' => $validated['name'], 'triggers' => $validated['triggers'] ?? [], 'notify_before_hours' => $validated['notify_before_hours'] ?? $setting->notify_before_hours, - 'config' => $validated['config'] ?? $setting->config, + 'config' => $request->input('config', $setting->config), // use raw input so no keys are stripped ]); return back()->with('success', 'πŸ”” Notification updated!'); diff --git a/app/Http/Controllers/RateCardController.php b/app/Http/Controllers/RateCardController.php index 75d90aa..8f08c87 100644 --- a/app/Http/Controllers/RateCardController.php +++ b/app/Http/Controllers/RateCardController.php @@ -5,8 +5,11 @@ declare(strict_types=1); namespace App\Http\Controllers; use App\Models\RateCard; +use App\Models\RateCardProfile; +use Barryvdh\DomPDF\Facade\Pdf; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\Http\Response; use Illuminate\View\View; class RateCardController extends Controller @@ -14,7 +17,8 @@ class RateCardController extends Controller public function index(): View { $rateCards = RateCard::orderBy('sort_order')->orderBy('platform')->get(); - return view('rate-cards.index', compact('rateCards')); + $profile = RateCardProfile::getSingleton(); + return view('rate-cards.index', compact('rateCards', 'profile')); } public function create(): View @@ -25,15 +29,15 @@ class RateCardController extends Controller public function store(Request $request): RedirectResponse { $validated = $request->validate([ - 'service_name' => 'required|string|max:255', - 'description' => 'nullable|string', - 'platform' => 'required|in:instagram,tiktok,youtube,general', - 'content_type' => 'nullable|string|max:50', - 'price' => 'required|numeric|min:0', - 'currency' => 'required|string|max:10', - 'price_unit' => 'required|in:per_content,per_month,per_campaign', + 'service_name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'platform' => 'required|in:instagram,tiktok,youtube,general', + 'content_type' => 'nullable|string|max:50', + 'price' => 'required|numeric|min:0', + 'currency' => 'required|string|max:10', + 'price_unit' => 'required|in:per_content,per_month,per_campaign', 'is_negotiable' => 'boolean', - 'sort_order' => 'nullable|integer', + 'sort_order' => 'nullable|integer', ]); $validated['is_negotiable'] = $request->boolean('is_negotiable'); @@ -53,16 +57,16 @@ class RateCardController extends Controller public function update(Request $request, RateCard $rateCard): RedirectResponse { $validated = $request->validate([ - 'service_name' => 'required|string|max:255', - 'description' => 'nullable|string', - 'platform' => 'required|in:instagram,tiktok,youtube,general', - 'content_type' => 'nullable|string|max:50', - 'price' => 'required|numeric|min:0', - 'currency' => 'required|string|max:10', - 'price_unit' => 'required|in:per_content,per_month,per_campaign', + 'service_name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'platform' => 'required|in:instagram,tiktok,youtube,general', + 'content_type' => 'nullable|string|max:50', + 'price' => 'required|numeric|min:0', + 'currency' => 'required|string|max:10', + 'price_unit' => 'required|in:per_content,per_month,per_campaign', 'is_negotiable' => 'boolean', - 'is_active' => 'boolean', - 'sort_order' => 'nullable|integer', + 'is_active' => 'boolean', + 'sort_order' => 'nullable|integer', ]); $validated['is_negotiable'] = $request->boolean('is_negotiable'); @@ -80,14 +84,91 @@ class RateCardController extends Controller return redirect()->route('rate-cards.index')->with('success', 'Entry removed.'); } + // ── Public Page ────────────────────────────────────────────── + 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')); + return view('rate-cards.public', compact('rateCards', 'profile')); + } + + // ── Profile Settings ───────────────────────────────────────── + + public function profilePage(): View + { + $profile = RateCardProfile::getSingleton(); + return view('rate-cards.profile', compact('profile')); + } + + public function profileUpdate(Request $request): RedirectResponse + { + $validated = $request->validate([ + 'owner_name' => 'required|string|max:100', + 'tagline' => 'nullable|string|max:200', + 'whatsapp' => 'nullable|string|max:30', + 'ig_handle' => 'nullable|string|max:60', + 'tiktok_handle' => 'nullable|string|max:60', + 'youtube_handle'=> 'nullable|string|max:60', + 'email' => 'nullable|email|max:150', + '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', + 'logo' => 'nullable|image|max:2048', + ]); + + $profile = RateCardProfile::getSingleton(); + + if ($request->hasFile('logo')) { + $path = $request->file('logo')->store('profile', 'private'); + $validated['logo_path'] = $path; + } + + $validated['show_prices'] = $request->boolean('show_prices'); + unset($validated['logo']); + + $profile->update($validated); + + return back()->with('success', '🎨 Profile updated!'); + } + + public function serveProfileLogo(): Response + { + $profile = RateCardProfile::getSingleton(); + + if (! $profile->logo_path || ! \Storage::disk('private')->exists($profile->logo_path)) { + abort(404); + } + + $contents = \Storage::disk('private')->get($profile->logo_path); + $mime = \Storage::disk('private')->mimeType($profile->logo_path); + + return response($contents, 200)->header('Content-Type', $mime); + } + + // ── PDF Download ───────────────────────────────────────────── + + public function pdf(): \Illuminate\Http\Response + { + $profile = RateCardProfile::getSingleton(); + $rateCards = RateCard::where('is_active', true) + ->orderBy('sort_order') + ->orderBy('platform') + ->get() + ->groupBy('platform'); + + $pdf = Pdf::loadView('rate-cards.pdf', compact('rateCards', 'profile')) + ->setPaper('a4', 'portrait'); + + $filename = 'rate-card-' . now()->format('Y-m-d') . '.pdf'; + + return $pdf->download($filename); } } diff --git a/app/Models/BrandContract.php b/app/Models/BrandContract.php new file mode 100644 index 0000000..7f0ae5e --- /dev/null +++ b/app/Models/BrandContract.php @@ -0,0 +1,75 @@ + 'date', + 'end_date' => 'date', + 'is_active' => 'boolean', + ]; + + // ── Relationships ──────────────────────────────────────────── + + public function client(): BelongsTo + { + return $this->belongsTo(Client::class); + } + + public function category(): BelongsTo + { + return $this->belongsTo(ClientCategory::class, 'category_id'); + } + + // ── Scopes ─────────────────────────────────────────────────── + + public function scopeActive($query) + { + return $query->where('is_active', true) + ->where('start_date', '<=', today()) + ->where('end_date', '>=', today()); + } + + // ── Helpers ────────────────────────────────────────────────── + + public function isCurrentlyActive(): bool + { + return $this->is_active + && $this->start_date->lte(today()) + && $this->end_date->gte(today()); + } + + public function getDaysRemainingAttribute(): int + { + return (int) max(0, today()->diffInDays($this->end_date, false)); + } + + public function getStatusLabelAttribute(): string + { + if (! $this->is_active) return 'Inactive'; + if ($this->start_date->gt(today())) return 'Upcoming'; + if ($this->end_date->lt(today())) return 'Expired'; + return 'Active'; + } + + public function getStatusColorAttribute(): string + { + return match ($this->statusLabel) { + 'Active' => 'badge-success', + 'Upcoming' => 'badge-info', + 'Expired' => 'badge-neutral', + default => 'badge-ghost', + }; + } +} diff --git a/app/Models/ContentBrief.php b/app/Models/ContentBrief.php index 8fdda2f..3980230 100644 --- a/app/Models/ContentBrief.php +++ b/app/Models/ContentBrief.php @@ -15,7 +15,7 @@ class ContentBrief extends Model use HasFactory, SoftDeletes; protected $fillable = [ - 'client_id', 'title', 'platform', 'content_type', 'status', + 'client_id', 'category_id', 'title', 'platform', 'content_type', 'status', 'hook', 'body', 'cta', 'script', 'caption', 'hashtags', 'target_date', 'posted_date', 'posted_url', 'sort_order', 'notes', ]; @@ -31,6 +31,11 @@ class ContentBrief extends Model return $this->belongsTo(Client::class); } + public function category(): BelongsTo + { + return $this->belongsTo(ClientCategory::class, 'category_id'); + } + public function getPlatformIconAttribute(): string { return match($this->platform) { diff --git a/app/Models/Event.php b/app/Models/Event.php index 634e56e..d909c85 100644 --- a/app/Models/Event.php +++ b/app/Models/Event.php @@ -15,7 +15,7 @@ class Event extends Model use HasFactory, SoftDeletes; protected $fillable = [ - 'client_id', 'title', 'description', 'event_type', + 'client_id', 'category_id', 'title', 'description', 'event_type', 'start_time', 'end_time', 'location', 'is_priority', 'is_all_day', 'status', 'color', 'notes', ]; @@ -33,6 +33,11 @@ class Event extends Model return $this->belongsTo(Client::class); } + public function category(): BelongsTo + { + return $this->belongsTo(ClientCategory::class, 'category_id'); + } + public function scopeUpcoming($query, int $hours = 24) { return $query->where('start_time', '>=', now()) diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index ba9952d..9db9979 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -6,6 +6,7 @@ namespace App\Models; use App\Enums\InvoiceStatus; use Illuminate\Database\Eloquent\Factories\HasFactory; +// InvoiceSettings used for number generation use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -19,7 +20,7 @@ class Invoice extends Model 'client_id', 'invoice_number', 'title', 'description', 'subtotal', 'tax_rate', 'tax_amount', 'total', 'currency', 'status', 'issue_date', 'due_date', 'paid_date', 'notes', - 'bank_name', 'bank_account', 'bank_holder', + 'bank_name', 'bank_account', 'bank_holder', 'branch_name', ]; protected $casts = [ @@ -45,9 +46,15 @@ class Invoice extends Model public function recalculate(): void { - $this->subtotal = $this->items->sum('total'); - $this->tax_amount = round($this->subtotal * ($this->tax_rate / 100), 2); - $this->total = $this->subtotal + $this->tax_amount; + // Total of all items (what the creator receives) + $this->subtotal = $this->items->sum('total'); + + // Tax is calculated only on taxable items, but paid by the client on top + $taxableSubtotal = $this->items->where('is_taxable', true)->sum('total'); + $this->tax_amount = round((float) $taxableSubtotal * ($this->tax_rate / 100), 2); + + // Grand total = subtotal (creator's fee, pre-tax β€” tax is borne by client separately) + $this->total = $this->subtotal; $this->save(); } @@ -58,8 +65,6 @@ class Invoice extends Model public static function generateNumber(): string { - $prefix = 'INV-' . now()->format('Ym'); - $last = static::where('invoice_number', 'like', $prefix . '%')->count(); - return $prefix . '-' . str_pad((string) ($last + 1), 3, '0', STR_PAD_LEFT); + return InvoiceSettings::generateNumber(); } } diff --git a/app/Models/InvoiceItem.php b/app/Models/InvoiceItem.php index 2f6ec88..7affe3c 100644 --- a/app/Models/InvoiceItem.php +++ b/app/Models/InvoiceItem.php @@ -9,11 +9,12 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; class InvoiceItem extends Model { - protected $fillable = ['invoice_id', 'description', 'quantity', 'unit_price', 'total']; + protected $fillable = ['invoice_id', 'description', 'quantity', 'unit_price', 'total', 'is_taxable']; protected $casts = [ 'unit_price' => 'decimal:2', 'total' => 'decimal:2', + 'is_taxable' => 'boolean', ]; public function invoice(): BelongsTo diff --git a/app/Models/InvoiceSettings.php b/app/Models/InvoiceSettings.php new file mode 100644 index 0000000..cf81fac --- /dev/null +++ b/app/Models/InvoiceSettings.php @@ -0,0 +1,54 @@ + 'integer', + '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_next_number' => 1, + 'default_tax_rate' => 11, + 'default_currency' => 'IDR', + ]); + } + + /** 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'); + return $prefix . '-' . $number; + } +} diff --git a/app/Models/RateCardProfile.php b/app/Models/RateCardProfile.php new file mode 100644 index 0000000..2678bed --- /dev/null +++ b/app/Models/RateCardProfile.php @@ -0,0 +1,47 @@ + 'boolean', + ]; + + /** Get the singleton profile, creating defaults if needed */ + public static function getSingleton(): self + { + return static::firstOrCreate([], [ + 'owner_name' => config('app.name', 'KOL Manager'), + 'bg_color' => '#FFF8F0', + 'accent_color' => '#FFB7C5', + 'text_color' => '#374151', + 'show_prices' => true, + ]); + } + + public function getWhatsappUrlAttribute(): ?string + { + if (! $this->whatsapp) return null; + $number = preg_replace('/[^0-9]/', '', $this->whatsapp); + return "https://wa.me/{$number}"; + } + + public function getLogoUrlAttribute(): ?string + { + return $this->logo_path + ? route('files.serve-profile-logo') + : null; + } +} diff --git a/app/Services/DashboardService.php b/app/Services/DashboardService.php index a24210e..cea6ed2 100644 --- a/app/Services/DashboardService.php +++ b/app/Services/DashboardService.php @@ -4,10 +4,13 @@ declare(strict_types=1); namespace App\Services; +use App\Models\BrandContract; use App\Models\Client; +use App\Models\ClientCategory; use App\Models\ContentBrief; use App\Models\Event; use App\Models\Invoice; +use App\Models\MediaFile; use App\Models\SocialStat; use Illuminate\Support\Collection; @@ -21,7 +24,12 @@ final class DashboardService 'upcoming_events' => Event::upcoming(24 * 90)->count(), // 3 months 'pending_invoices' => Invoice::where('status', 'sent')->count(), 'total_revenue' => Invoice::where('status', 'paid')->sum('total'), - 'content_in_progress' => ContentBrief::whereNotIn('status', ['posted', 'archived'])->count(), + 'content_in_progress' => ContentBrief::whereNotIn('status', ['posted', 'archived'])->count(), + 'total_categories' => ClientCategory::whereNull('parent_id')->count(), + 'total_subcategories' => ClientCategory::whereNotNull('parent_id')->count(), + 'total_files' => MediaFile::count(), + 'active_contracts' => BrandContract::where('is_active', true)->where('end_date', '>=', today())->count(), + 'total_contracts' => BrandContract::count(), ]; } diff --git a/app/Services/InvoiceService.php b/app/Services/InvoiceService.php index 9eed952..bcb1ccd 100644 --- a/app/Services/InvoiceService.php +++ b/app/Services/InvoiceService.php @@ -6,6 +6,7 @@ namespace App\Services; use App\Models\Invoice; use App\Models\InvoiceItem; +use App\Models\InvoiceSettings; use Barryvdh\DomPDF\Facade\Pdf; use Illuminate\Http\Response; @@ -22,7 +23,8 @@ final class InvoiceService ]); foreach ($items as $item) { - $item['total'] = $item['quantity'] * $item['unit_price']; + $item['total'] = $item['quantity'] * $item['unit_price']; + $item['is_taxable'] = isset($item['is_taxable']) && $item['is_taxable'] ? true : false; $invoice->items()->create($item); } @@ -37,7 +39,8 @@ final class InvoiceService $invoice->items()->delete(); foreach ($items as $item) { - $item['total'] = $item['quantity'] * $item['unit_price']; + $item['total'] = $item['quantity'] * $item['unit_price']; + $item['is_taxable'] = isset($item['is_taxable']) && $item['is_taxable'] ? true : false; $invoice->items()->create($item); } @@ -49,8 +52,9 @@ final class InvoiceService public function generatePdf(Invoice $invoice): Response { $invoice->load('client', 'items'); + $invoiceSettings = InvoiceSettings::instance(); - $pdf = Pdf::loadView('invoices.pdf', compact('invoice')); + $pdf = Pdf::loadView('invoices.pdf', compact('invoice', 'invoiceSettings')); $pdf->setPaper('A4', 'portrait'); return $pdf->download("invoice-{$invoice->invoice_number}.pdf"); diff --git a/app/Services/Notifications/WahaNotifier.php b/app/Services/Notifications/WahaNotifier.php index 3d08b2e..76b15fb 100644 --- a/app/Services/Notifications/WahaNotifier.php +++ b/app/Services/Notifications/WahaNotifier.php @@ -30,26 +30,33 @@ class WahaNotifier private function send(NotificationSetting $setting, string $text): void { - $config = $setting->config ?? []; - $url = rtrim($config['waha_url'] ?? '', '/'); - $session = $config['waha_session'] ?? 'default'; - $phone = $config['phone'] ?? null; + $config = $setting->config ?? []; + $endpoint = $config['waha_endpoint'] ?? null; + $session = $config['waha_session'] ?? 'default'; + $chatId = $config['chat_id'] ?? null; + $apiKey = $config['api_key'] ?? null; - if (! $url || ! $phone) { - throw new \RuntimeException('WAHA waha_url and phone are required.'); + if (! $endpoint || ! $chatId) { + throw new \RuntimeException('WAHA waha_endpoint and chat_id are required.'); } - // Normalise phone: strip non-digits, ensure no leading + - $chatId = preg_replace('/[^0-9]/', '', $phone) . '@c.us'; + $http = Http::timeout(15)->acceptJson()->contentType('application/json'); - $response = Http::timeout(15)->post("{$url}/api/sendText", [ - 'session' => $session, + if ($apiKey) { + $http = $http->withHeaders(['X-Api-Key' => $apiKey]); + } + + $response = $http->post($endpoint, [ 'chatId' => $chatId, 'text' => $text, + 'session' => $session, ]); if (! $response->successful()) { - throw new \RuntimeException('WAHA API error: ' . $response->body()); + $keyHint = $apiKey ? '(api_key set, length=' . strlen($apiKey) . ')' : '(no api_key)'; + throw new \RuntimeException( + "WAHA API error {$response->status()} {$keyHint} β†’ " . $response->body() + ); } } } diff --git a/database/migrations/2024_01_01_000100_add_category_to_events_and_content_briefs.php b/database/migrations/2024_01_01_000100_add_category_to_events_and_content_briefs.php new file mode 100644 index 0000000..dde9060 --- /dev/null +++ b/database/migrations/2024_01_01_000100_add_category_to_events_and_content_briefs.php @@ -0,0 +1,37 @@ +unsignedBigInteger('category_id')->nullable()->after('client_id'); + $table->foreign('category_id') + ->references('id')->on('client_categories')->onDelete('set null'); + }); + + Schema::table('content_briefs', function (Blueprint $table): void { + $table->unsignedBigInteger('category_id')->nullable()->after('client_id'); + $table->foreign('category_id') + ->references('id')->on('client_categories')->onDelete('set null'); + }); + } + + public function down(): void + { + Schema::table('events', function (Blueprint $table): void { + $table->dropForeign(['category_id']); + $table->dropColumn('category_id'); + }); + Schema::table('content_briefs', function (Blueprint $table): void { + $table->dropForeign(['category_id']); + $table->dropColumn('category_id'); + }); + } +}; diff --git a/database/migrations/2024_01_01_000110_create_brand_contracts_table.php b/database/migrations/2024_01_01_000110_create_brand_contracts_table.php new file mode 100644 index 0000000..c2b2e71 --- /dev/null +++ b/database/migrations/2024_01_01_000110_create_brand_contracts_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('title'); + $table->unsignedBigInteger('client_id')->nullable(); // the contracted brand + $table->unsignedBigInteger('category_id')->nullable(); // the exclusive category + $table->date('start_date'); + $table->date('end_date'); + $table->text('notes')->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + + $table->foreign('client_id')->references('id')->on('clients')->onDelete('set null'); + $table->foreign('category_id')->references('id')->on('client_categories')->onDelete('set null'); + }); + } + + public function down(): void + { + Schema::dropIfExists('brand_contracts'); + } +}; diff --git a/database/migrations/2024_01_01_000120_create_rate_card_profiles_table.php b/database/migrations/2024_01_01_000120_create_rate_card_profiles_table.php new file mode 100644 index 0000000..049f192 --- /dev/null +++ b/database/migrations/2024_01_01_000120_create_rate_card_profiles_table.php @@ -0,0 +1,37 @@ +id(); + $table->string('owner_name')->default('KOL Manager'); + $table->string('tagline')->nullable(); + $table->string('whatsapp')->nullable(); + $table->string('ig_handle')->nullable(); + $table->string('tiktok_handle')->nullable(); + $table->string('youtube_handle')->nullable(); + $table->string('email')->nullable(); + $table->string('logo_path')->nullable(); // uploaded image + $table->string('bg_color')->default('#FFF8F0'); + $table->string('accent_color')->default('#FFB7C5'); + $table->string('text_color')->default('#374151'); + $table->text('custom_footer')->nullable(); + $table->boolean('show_prices')->default(true); + $table->string('access_token')->nullable(); // optional: require ?token=xxx + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('rate_card_profiles'); + } +}; diff --git a/database/migrations/2026_04_01_104816_add_invoice_settings_and_branch_name.php b/database/migrations/2026_04_01_104816_add_invoice_settings_and_branch_name.php new file mode 100644 index 0000000..b6c6268 --- /dev/null +++ b/database/migrations/2026_04_01_104816_add_invoice_settings_and_branch_name.php @@ -0,0 +1,45 @@ +string('branch_name')->nullable()->after('bank_holder'); + }); + + // Add is_taxable to invoice_items + Schema::table('invoice_items', function (Blueprint $table): void { + $table->boolean('is_taxable')->default(true)->after('total'); + }); + + // Invoice settings singleton table + Schema::create('invoice_settings', function (Blueprint $table): void { + $table->id(); + $table->string('invoice_prefix')->default('INV'); + $table->unsignedInteger('invoice_next_number')->default(1); + $table->string('header_name')->nullable(); // talent name + $table->string('header_title')->nullable(); // subtitle / tagline + $table->string('header_logo')->nullable(); // stored path + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('invoice_settings'); + Schema::table('invoice_items', function (Blueprint $table): void { + $table->dropColumn('is_taxable'); + }); + Schema::table('invoices', function (Blueprint $table): void { + $table->dropColumn('branch_name'); + }); + } +}; diff --git a/database/migrations/2026_04_01_143218_add_defaults_to_invoice_settings.php b/database/migrations/2026_04_01_143218_add_defaults_to_invoice_settings.php new file mode 100644 index 0000000..4e78759 --- /dev/null +++ b/database/migrations/2026_04_01_143218_add_defaults_to_invoice_settings.php @@ -0,0 +1,36 @@ +decimal('default_tax_rate', 5, 2)->default(11)->after('header_logo'); + $table->string('default_currency')->default('IDR')->after('default_tax_rate'); + $table->string('default_service_description')->nullable()->after('default_currency'); + $table->string('default_bank_name')->nullable()->after('default_service_description'); + $table->string('default_branch_name')->nullable()->after('default_bank_name'); + $table->string('default_bank_account')->nullable()->after('default_branch_name'); + $table->string('default_bank_holder')->nullable()->after('default_bank_account'); + $table->text('default_notes')->nullable()->after('default_bank_holder'); + }); + } + + public function down(): void + { + Schema::table('invoice_settings', function (Blueprint $table): void { + $table->dropColumn([ + 'default_tax_rate', 'default_currency', + 'default_service_description', + 'default_bank_name', 'default_branch_name', + 'default_bank_account', 'default_bank_holder', 'default_notes', + ]); + }); + } +}; diff --git a/resources/css/app.css b/resources/css/app.css index d6d3437..ea619bd 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -115,6 +115,40 @@ aside { } .paw-bounce { animation: paw-bounce 0.4s ease-in-out; } +/* ── Label Soft ───────────────────────────────────────────── */ +.label-soft { + @apply block text-xs font-semibold text-gray-600 mb-1; +} + +/* ── Select Soft ──────────────────────────────────────────── */ +.select-soft { + @apply rounded-2xl border border-pink-200 px-4 py-2 + focus:border-pink-400 focus:outline-none focus:ring-2 focus:ring-pink-200 + w-full bg-white text-sm; +} + +/* ── Btn Pink (alias for btn-sakura) ─────────────────────── */ +.btn-pink { + @apply rounded-full text-white font-semibold shadow-sm + transition-all duration-150 inline-block cursor-pointer text-center; + background-color: #FFB7C5; + border: none; + text-decoration: none; +} +.btn-pink:hover { background-color: #FF8FAD; } +.btn-pink:active { transform: scale(0.96); } + +/* ── Badge Soft ───────────────────────────────────────────── */ +.badge-soft { @apply badge badge-ghost; } + +/* ── Cat Card Header ──────────────────────────────────────── */ +.cat-card-header { + @apply flex items-center gap-3 px-4 py-3 border-b border-pink-50; +} +.cat-card-icon { + @apply text-xl shrink-0; +} + /* ── Mobile touch targets ─────────────────────────────────── */ @media (max-width: 1023px) { .nav-item { @apply py-3.5; } diff --git a/resources/views/categories/index.blade.php b/resources/views/categories/index.blade.php new file mode 100644 index 0000000..3b14e78 --- /dev/null +++ b/resources/views/categories/index.blade.php @@ -0,0 +1,213 @@ +@extends('layouts.app') +@section('title', 'Categories') +@section('page-title', '🏷️ Category Management') +@section('page-subtitle', 'Organise clients, schedules & content by category') + +@section('content') +
+ + @if(session('success')) +
+ {{ session('success') }} + +
+ @endif + + {{-- Add form --}} +
+
+ βž• +

Add New Category

+
+
+ @csrf +
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ +
+
+
+ + {{-- List --}} +
+
+ πŸ—‚οΈ +

All Categories

+ + {{ $categories->count() }} total + +
+ + @php $roots = $categories->whereNull('parent_id'); @endphp + + @forelse($roots as $root) +
+ {{-- Root row --}} +
+ {{ $root->icon ?: '🏷️' }} + @if($root->color) + + @endif +
+
{{ $root->name }}
+
{{ $root->children->count() }} sub Β· {{ $root->clients_count }} client(s)
+
+
+ + @unless($root->is_system) +
+ @csrf @method('DELETE') + +
+ @endunless +
+
+ + {{-- Edit form --}} + + + {{-- Children --}} + @foreach($root->children as $child) +
+
+ {{ $child->icon ?: '↳' }} + @if($child->color) + + @endif +
+
↳ {{ $child->name }}
+
{{ $child->clients_count }} client(s)
+
+
+ + @unless($child->is_system) +
+ @csrf @method('DELETE') + +
+ @endunless +
+
+ +
+ @endforeach +
+ @empty +
+
🏷️
+

No categories yet. Add your first one above.

+
+ @endforelse +
+
+@endsection diff --git a/resources/views/clients/index.blade.php b/resources/views/clients/index.blade.php index f957cd4..d5ea3cb 100644 --- a/resources/views/clients/index.blade.php +++ b/resources/views/clients/index.blade.php @@ -6,70 +6,77 @@ @section('content') -{{-- ── Filters bar ─────────────────────────────────────────── --}} -
-
- {{-- Local text filter --}} -
- πŸ” - + +
+
+ οΌ‹ Add + @if(request('category') || request('status')) + + βœ• Clear filters + + @endif +
+ +
+ +{{-- ── Filters panel (collapsible) ────────────────────────── --}} +
+
+ {{-- Row 1: search + dropdowns --}} +
+ {{-- Search (fills remaining space on desktop) --}} +
+ πŸ” +
- {{-- Category filter --}} - - - - + + @foreach($categories as $parent) + + @foreach($parent->children as $child) + @endforeach - + @endforeach + - @if(request('category') || request('status')) - βœ• Clear - @endif - - - οΌ‹ Add Client + {{-- Status dropdown --}} +
- {{-- Category pills (top-level only) --}} -
- - All - - @foreach($categories as $cat) - - {{ $cat->icon }} {{ $cat->name }} - - @endforeach -
+
+
{{-- end x-data --}} {{-- ── Client grid ─────────────────────────────────────────── --}}
@@ -135,11 +142,12 @@ class="flex-1 text-center text-xs py-1.5 px-3 rounded-full border border-pink-200 text-gray-600 hover:bg-pink-50"> Edit + {{-- Share button disabled --}}
@csrf @method('DELETE')
diff --git a/resources/views/content/create.blade.php b/resources/views/content/create.blade.php index a45e38a..ce2b3cf 100644 --- a/resources/views/content/create.blade.php +++ b/resources/views/content/create.blade.php @@ -6,8 +6,9 @@ @section('content')
-
- @csrf +
@@ -40,19 +41,38 @@
- @foreach($clients as $client) @endforeach
+
+ + +
+ {{-- Contract warning --}} + + {{-- Storyline Maker --}}

✍️ Storyline Maker

@@ -95,7 +115,8 @@
- + Cancel @@ -104,4 +125,35 @@
+@push('scripts') + +@endpush @endsection diff --git a/resources/views/content/edit.blade.php b/resources/views/content/edit.blade.php index 9a88e12..c2e11d2 100644 --- a/resources/views/content/edit.blade.php +++ b/resources/views/content/edit.blade.php @@ -43,6 +43,22 @@ @endforeach
+
+ + +
diff --git a/resources/views/contracts/index.blade.php b/resources/views/contracts/index.blade.php new file mode 100644 index 0000000..baf9bcd --- /dev/null +++ b/resources/views/contracts/index.blade.php @@ -0,0 +1,212 @@ +@extends('layouts.app') +@section('title', 'Brand Contracts') +@section('page-title', 'πŸ“‹ Brand Contracts') +@section('page-subtitle', 'Manage exclusive brand agreements per category') + +@section('content') +
+ + @if(session('success')) +
+ {{ session('success') }} + +
+ @endif + + {{-- Add Contract --}} +
+
+ βž• +

Add New Contract

+
+
+ @csrf +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ + {{-- Contracts List --}} +
+
+ πŸ“‹ +

Active & Upcoming Contracts

+ + {{ $contracts->count() }} total + +
+ + @forelse($contracts as $contract) +
+
+ {{ $contract->statusLabel }} +
+
{{ $contract->title }}
+
+ @if($contract->client)πŸ‘€ {{ $contract->client->name }}@endif + @if($contract->category)🏷️ {{ $contract->category->fullName }}@endif + πŸ“… {{ $contract->start_date->format('d M Y') }} – {{ $contract->end_date->format('d M Y') }} + @if($contract->isCurrentlyActive()) + {{ $contract->days_remaining }}d left + @endif +
+ @if($contract->notes) +
{{ $contract->notes }}
+ @endif +
+
+
+ @csrf @method('PATCH') + +
+ +
+ @csrf @method('DELETE') + +
+
+
+ + +
+ @empty +
+
πŸ“‹
+

No contracts yet. Add your first exclusive agreement above.

+
+ @endforelse +
+ + {{-- How it works --}} +
+
+ πŸ’‘ +

How Brand Contracts Work

+
+
    +
  • πŸ”’ When a contract is Active, creating schedules or content for a competing brand in the same category will show a warning.
  • +
  • βœ… The contracted brand itself is always allowed β€” no warning shown.
  • +
  • 🏷️ Blocking a parent category also blocks all its subcategories.
  • +
  • πŸ“… Contracts only block during their active date range. Expired contracts are ignored automatically.
  • +
+
+ +
+@endsection diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index 78838e6..8f3ba06 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -45,22 +45,22 @@
{{-- ── Stat Pills (BELOW schedule) ─────────────────────────── --}} -
-
+ - - +
Rp {{ number_format($stats['total_revenue'], 0, ',', '.') }} @@ -70,6 +70,25 @@
+{{-- ── Secondary Stats ──────────────────────────────────────── --}} + + {{-- ── Bottom Row ───────────────────────────────────────────── --}}
diff --git a/resources/views/files/index.blade.php b/resources/views/files/index.blade.php index 5108b6b..88164ee 100644 --- a/resources/views/files/index.blade.php +++ b/resources/views/files/index.blade.php @@ -6,60 +6,141 @@ @section('content') -{{-- ── Upload Form (collapsible on mobile) ─────────────────── --}} -
+{{-- ── Upload Form (collapsible) ────────────────────────────── --}} +

πŸ“€ Upload File

-
-
- @csrf -
- - +
+ + @csrf +
+ + +
+
+ + + + @foreach($folders as $f) + +
+
+ + +
+
+ +
+ + + {{-- Progress Bar --}} +
+
+ Uploading… + +
+
+
+
-
- - - - @foreach($folders as $f) - + + {{-- Status Message --}} +
-
- - -
-
- -
- +
{{-- ── Folder Filter Chips ──────────────────────────────────── --}} @@ -124,7 +205,7 @@
@csrf @method('DELETE') -
@@ -138,7 +219,7 @@
πŸ“

No files yet.

-
@endforelse diff --git a/resources/views/invoices/create.blade.php b/resources/views/invoices/create.blade.php index 5b7bacb..a792cd4 100644 --- a/resources/views/invoices/create.blade.php +++ b/resources/views/invoices/create.blade.php @@ -31,9 +31,9 @@
@@ -50,7 +50,7 @@
+ value="{{ old('tax_rate', $invoiceSettings->default_tax_rate ?? 11) }}" class="input-soft" step="0.01">
@@ -93,6 +93,18 @@
+ {{-- Taxable checkbox --}} +
+ + + +
@@ -101,34 +113,43 @@ {{-- Totals --}}
- Subtotal + Subtotal (all items) Rp
-
- Tax (%) +
+ Tax (% β€” paid by client) Rp
- Total + Total (you receive) Rp
{{-- Bank Info --}} -
+
-
+
+ + +
-
-
@@ -136,7 +157,7 @@
+ placeholder="Payment terms, etc.">{{ old('notes', $invoiceSettings->default_notes) }}
@@ -154,15 +175,22 @@ diff --git a/resources/views/invoices/edit.blade.php b/resources/views/invoices/edit.blade.php index b12e3da..2ceff02 100644 --- a/resources/views/invoices/edit.blade.php +++ b/resources/views/invoices/edit.blade.php @@ -88,6 +88,18 @@
+ {{-- Taxable checkbox --}} +
+ + + +
@@ -96,26 +108,31 @@ {{-- Totals --}}
- Subtotal + Subtotal (all items) Rp
-
- Tax (%) +
+ Tax (% β€” paid by client) Rp
- Total + Total (you receive) Rp
{{-- Bank Info --}} -
+
+
+ + +
function invoiceEditForm() { + @php + $itemsJson = $invoice->items->map(function($i) { + return ['description' => $i->description, 'quantity' => $i->quantity, 'unit_price' => (float)$i->unit_price, 'is_taxable' => (bool)$i->is_taxable]; + }); + @endphp return { - items: @json($invoice->items->map(fn($i) => ['description' => $i->description, 'quantity' => $i->quantity, 'unit_price' => (float)$i->unit_price])), + items: @json($itemsJson), taxRate: {{ $invoice->tax_rate }}, - get subtotal() { return this.items.reduce((s, i) => s + (i.quantity * i.unit_price), 0); }, - get taxAmount() { return Math.round(this.subtotal * this.taxRate / 100); }, - get total() { return this.subtotal + this.taxAmount; }, - addItem() { this.items.push({ description: '', quantity: 1, unit_price: 0 }); }, + get subtotal() { + return this.items.reduce((s, i) => s + (i.quantity * i.unit_price), 0); + }, + get taxAmount() { + const taxable = this.items + .filter(i => i.is_taxable) + .reduce((s, i) => s + (i.quantity * i.unit_price), 0); + return Math.round(taxable * this.taxRate / 100); + }, + get total() { return this.subtotal; }, + addItem() { this.items.push({ description: '', quantity: 1, unit_price: 0, is_taxable: true }); }, removeItem(i) { if (this.items.length > 1) this.items.splice(i, 1); }, recalc() {}, formatNum(n) { return Number(n).toLocaleString('id-ID'); }, diff --git a/resources/views/invoices/index.blade.php b/resources/views/invoices/index.blade.php index 90d3039..9025055 100644 --- a/resources/views/invoices/index.blade.php +++ b/resources/views/invoices/index.blade.php @@ -18,7 +18,13 @@ @endforeach
- οΌ‹ New Invoice +
{{-- Mobile card list --}} @@ -41,6 +47,7 @@ View PDF Edit + {{-- Share button disabled --}}
@empty @@ -85,6 +92,7 @@ View PDF Edit + {{-- Share button disabled --}}
@@ -100,4 +108,5 @@
{{ $invoices->links() }}
+{{-- Share scripts disabled --}} @endsection diff --git a/resources/views/invoices/pdf.blade.php b/resources/views/invoices/pdf.blade.php index 3fcef76..7c2b518 100644 --- a/resources/views/invoices/pdf.blade.php +++ b/resources/views/invoices/pdf.blade.php @@ -38,9 +38,15 @@
-
- -
Personal Talent Assistant
+
+ @if($invoiceSettings->header_logo && file_exists(storage_path('app/public/' . $invoiceSettings->header_logo))) + Logo + @endif +
+ +
{{ $invoiceSettings->header_title ?: 'Personal Talent Assistant' }}
+
{{ $invoice->invoice_number }}
@@ -97,15 +103,21 @@
SubtotalRp {{ number_format($invoice->subtotal, 0, ',', '.') }}
-
Tax ({{ $invoice->tax_rate }}%)Rp {{ number_format($invoice->tax_amount, 0, ',', '.') }}
-
Total{{ $invoice->formatted_total }}
+ @if($invoice->tax_rate > 0) +
Tax ({{ $invoice->tax_rate }}% β€” paid by client)Rp {{ number_format($invoice->tax_amount, 0, ',', '.') }}
+ @endif +
Total (you receive){{ $invoice->formatted_total }}
@if($invoice->bank_name)
-
πŸ’³ Payment Information
-
{{ $invoice->bank_name }} Β· {{ $invoice->bank_account }} Β· a/n {{ $invoice->bank_holder }}
+
Payment Information
+
+ {{ $invoice->bank_name }} + @if($invoice->branch_name) Β· {{ $invoice->branch_name }} @endif + Β· {{ $invoice->bank_account }} Β· a/n {{ $invoice->bank_holder }} +
@endif diff --git a/resources/views/invoices/settings.blade.php b/resources/views/invoices/settings.blade.php new file mode 100644 index 0000000..1c74b76 --- /dev/null +++ b/resources/views/invoices/settings.blade.php @@ -0,0 +1,175 @@ +@extends('layouts.app') + +@section('title', 'Invoice Settings') +@section('page-title', 'Invoice Settings 🧾') +@section('page-subtitle', 'Customize number format, header, and invoice defaults') + +@section('content') +
+ + + @if(session('success')) +
+ {{ session('success') }} + +
+ @endif + +
+ @csrf + + {{-- Number Format --}} +
+

πŸ”’ Invoice Number Format

+
+
+ + +

e.g. INV-EA β†’ INV-EA-001

+
+
+ + +

Used for the next new invoice

+
+
+
+ Preview: + {{ rtrim(old('invoice_prefix', $settings->invoice_prefix), '-') }}-{{ str_pad((string) (old('invoice_next_number', $settings->invoice_next_number) ?? 1), 3, '0', STR_PAD_LEFT) }} + +
+
+ + {{-- Header / Branding --}} +
+

🎨 Invoice Header

+ + @if($settings->header_logo) +
+ Logo +
+

Current Logo

+

Upload a new file to replace it

+
+
+ @endif + +
+
+ + +

Max 2 MB. Shown at the top-left of your invoice.

+
+
+ + +
+
+ + +
+
+
+ + {{-- Invoice Defaults --}} +
+

πŸ“‹ Invoice Defaults

+

These values will pre-fill every new invoice you create.

+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ +@push('scripts') + +@endpush +@endsection diff --git a/resources/views/invoices/show.blade.php b/resources/views/invoices/show.blade.php index 67f330d..5ac4f7e 100644 --- a/resources/views/invoices/show.blade.php +++ b/resources/views/invoices/show.blade.php @@ -18,9 +18,19 @@
{{-- Invoice Header --}}
-
-
🐱 KOL Manager
-
Personal Talent Assistant
+
+ @if($invoiceSettings->header_logo) + Logo + @endif +
+
+ {{ $invoiceSettings->header_name ?: '🐱 KOL Manager' }} +
+
+ {{ $invoiceSettings->header_title ?: 'Personal Talent Assistant' }} +
+
{{ $invoice->invoice_number }}
@@ -75,15 +85,18 @@ {{-- Totals --}}
-
+
SubtotalRp {{ number_format($invoice->subtotal, 0, ',', '.') }}
-
- Tax ({{ $invoice->tax_rate }}%)Rp {{ number_format($invoice->tax_amount, 0, ',', '.') }} + @if($invoice->tax_rate > 0) +
+ Tax ({{ $invoice->tax_rate }}% β€” paid by client) + Rp {{ number_format($invoice->tax_amount, 0, ',', '.') }}
+ @endif
- Total{{ $invoice->formatted_total }} + Total (you receive){{ $invoice->formatted_total }}
@@ -92,7 +105,11 @@ @if($invoice->bank_name)
πŸ’³ Payment Info
-
{{ $invoice->bank_name }} Β· {{ $invoice->bank_account }} Β· {{ $invoice->bank_holder }}
+
+ {{ $invoice->bank_name }} + @if($invoice->branch_name) Β· {{ $invoice->branch_name }} @endif + Β· {{ $invoice->bank_account }} Β· a/n {{ $invoice->bank_holder }} +
@endif diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 8b49107..b033676 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -75,6 +75,18 @@ πŸ‘₯ Clients / CRM + + 🏷️ Categories + + + + πŸ“‹ Brand Contracts + + diff --git a/resources/views/rate-cards/index.blade.php b/resources/views/rate-cards/index.blade.php index cbd55fb..cbd56a2 100644 --- a/resources/views/rate-cards/index.blade.php +++ b/resources/views/rate-cards/index.blade.php @@ -5,11 +5,21 @@ @section('page-subtitle', 'Manage your service pricing') @section('content') -
- - πŸ”— View Public Page β†’ - + @@ -57,11 +67,12 @@ class="flex-1 text-center text-xs py-2 rounded-full border border-pink-200 text-gray-600 hover:bg-pink-50 font-medium"> Edit + {{-- Share button disabled --}}
@csrf @method('DELETE') -
@@ -79,4 +90,5 @@ Add your first service!
@endif +{{-- Share scripts disabled --}} @endsection diff --git a/resources/views/rate-cards/pdf.blade.php b/resources/views/rate-cards/pdf.blade.php new file mode 100644 index 0000000..783bc57 --- /dev/null +++ b/resources/views/rate-cards/pdf.blade.php @@ -0,0 +1,142 @@ +@php + // Convert hex accent color to rgb for rgba() usage in dompdf + $accent = ltrim($profile->accent_color ?? '#FFB7C5', '#'); + $ar = hexdec(substr($accent, 0, 2)); + $ag = hexdec(substr($accent, 2, 2)); + $ab = hexdec(substr($accent, 4, 2)); + $accentRgb = "{$ar},{$ag},{$ab}"; + + $bg = $profile->bg_color ?? '#FFFBFE'; + $text = $profile->text_color ?? '#374151'; + $accentHex = '#' . $accent; +@endphp + + + + + Rate Card β€” {{ $profile->owner_name }} + + + + +
+ @if($profile->logo_path && file_exists(storage_path('app/private/' . $profile->logo_path))) + {{ $profile->owner_name }} + @endif +
{{ $profile->owner_name }}
+ @if($profile->tagline) +
{{ $profile->tagline }}
+ @endif +
+ @if($profile->email)βœ‰ {{ $profile->email }}@endif + @if($profile->whatsapp)Β· {{ $profile->whatsapp }}@endif + @if($profile->ig_handle)Β· @{{ $profile->ig_handle }}@endif + @if($profile->tiktok_handle)Β· @{{ $profile->tiktok_handle }}@endif + @if($profile->youtube_handle)Β· {{ $profile->youtube_handle }}@endif +
+
+ +@foreach($rateCards as $platform => $cards) +
+
+ @php $icons = ['instagram' => 'Instagram', 'tiktok' => 'TikTok', 'youtube' => 'YouTube', 'general' => 'General']; @endphp + {{ $icons[$platform] ?? ucfirst($platform) }} +
+ + + + + + + + + + @foreach($cards as $card) + + + + + + @endforeach + +
ServiceDescriptionPrice
+
{{ $card->service_name }}
+ @if($card->content_type) + {{ $card->content_type }} + @endif +
+ @if($card->description) +
{{ $card->description }}
+ @endif +
+ @if($profile->show_prices) +
{{ $card->formatted_price }}
+
{{ str_replace('_', ' ', $card->price_unit) }}
+ @else +
On Request
+ @endif + @if($card->is_negotiable) +
Negotiable
+ @endif +
+
+@endforeach + + + + + diff --git a/resources/views/rate-cards/profile.blade.php b/resources/views/rate-cards/profile.blade.php new file mode 100644 index 0000000..136b2dc --- /dev/null +++ b/resources/views/rate-cards/profile.blade.php @@ -0,0 +1,172 @@ +@extends('layouts.app') +@section('title', 'Rate Card Profile') +@section('page-title', '🎨 Rate Card Profile') +@section('page-subtitle', 'Customise your public rate card page') + +@section('content') +
+ + @if(session('success')) +
+ {{ session('success') }} + +
+ @endif + +
+ @csrf + + {{-- Identity --}} +
+
+ πŸ‘€ +

Identity

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ @ + +
+
+
+ +
+ @ + +
+
+
+ + +
+
+
+ + {{-- Appearance --}} +
+
+ 🎨 +

Appearance

+
+
+
+ + @if($profile->logo_path) +
+ Logo + Current logo +
+ @endif + +

Max 2MB. PNG/JPG/WebP.

+
+ +
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ + +
+
+ +
+
+
+ + {{-- Actions --}} + +
+
+@endsection diff --git a/resources/views/rate-cards/public.blade.php b/resources/views/rate-cards/public.blade.php index cae82c4..bccabbd 100644 --- a/resources/views/rate-cards/public.blade.php +++ b/resources/views/rate-cards/public.blade.php @@ -3,36 +3,80 @@ - Rate Card β€” KOL Manager + Rate Card β€” {{ $profile->owner_name }}
- -
Rate Card
-
Services & Pricing Β· {{ now()->format('Y') }}
+ @if($profile->logo_path) + {{ $profile->owner_name }} + @else +
🐱
+ @endif +
{{ $profile->owner_name }}
+ @if($profile->tagline) +
{{ $profile->tagline }}
+ @endif + +
+ @if($profile->whatsappUrl) + πŸ“± WhatsApp + @endif + @if($profile->ig_handle) + πŸ“Έ @{{ $profile->ig_handle }} + @endif + @if($profile->tiktok_handle) + 🎡 @{{ $profile->tiktok_handle }} + @endif + @if($profile->youtube_handle) + ▢️ {{ $profile->youtube_handle }} + @endif + @if($profile->email) + βœ‰οΈ {{ $profile->email }} + @endif +
+ +
@forelse($rateCards as $platform => $cards) @@ -51,8 +95,12 @@ @if($card->description)
{{ $card->description }}
@endif + @if($profile->show_prices)
{{ $card->formatted_price }}
{{ str_replace('_', ' ', $card->price_unit) }}
+ @else +
On Request
+ @endif @if($card->is_negotiable)
✦ Negotiable
@endif @@ -65,7 +113,8 @@ @endforelse
diff --git a/resources/views/schedule/edit.blade.php b/resources/views/schedule/edit.blade.php index 127bb60..2a26fe8 100644 --- a/resources/views/schedule/edit.blade.php +++ b/resources/views/schedule/edit.blade.php @@ -52,7 +52,7 @@
{{-- Client --}} -
+
+ {{-- Category --}} +
+ + +
+ {{-- Start Date --}}
- @foreach($clients as $client) @endforeach
+
+ + +
+ + {{-- Contract warning --}} + {{-- Start: date + time side by side --}}
@@ -86,7 +106,10 @@
- +
@@ -209,8 +232,9 @@ document.addEventListener('DOMContentLoaded', function () {
Start${e.start?.toLocaleString()}
${e.end ? `
End${e.end.toLocaleString()}
` : ''} `; + const editUrl = `/schedule/${e.id}/edit`; document.getElementById('modal-actions').innerHTML = ` - Edit + Edit
@@ -248,5 +272,37 @@ document.addEventListener('DOMContentLoaded', function () { }); }); + +{{-- Share scripts disabled --}} + + @endpush @endsection diff --git a/resources/views/settings/notifications.blade.php b/resources/views/settings/notifications.blade.php index efa5289..d9d8c10 100644 --- a/resources/views/settings/notifications.blade.php +++ b/resources/views/settings/notifications.blade.php @@ -134,11 +134,12 @@
@elseif($setting->channel === 'whatsapp')
-
- - +
+ + +

Full URL to the sendText endpoint

@@ -146,11 +147,21 @@ value="{{ $setting->config['waha_session'] ?? 'default' }}" class="input-soft text-sm" placeholder="default">
+
+ + +

Sent as X-Api-Key header. Leave blank if WAHA has no auth.

+
- - + + +

Individual: 628xxx@c.us Β· Group: 120363xxx@g.us

@endif @@ -238,20 +249,29 @@ {{-- WhatsApp / WAHA --}}
-
- - -

Your self-hosted WAHA instance

+
+ + +

Full URL to the sendText endpoint of your WAHA instance

+
+ + +

Sent as X-Api-Key header. Leave blank if WAHA has no auth.

+
- - + + +

Individual: 628xxx@c.us Β· Group: 120363xxx@g.us

diff --git a/routes/web.php b/routes/web.php index 33535db..08061ba 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,12 +2,14 @@ declare(strict_types=1); +use App\Http\Controllers\BrandContractController; use App\Http\Controllers\ClientCategoryController; use App\Http\Controllers\ClientController; use App\Http\Controllers\ContentBriefController; use App\Http\Controllers\DashboardController; use App\Http\Controllers\EventController; use App\Http\Controllers\InvoiceController; +use App\Http\Controllers\InvoiceSettingsController; use App\Http\Controllers\MediaFileController; use App\Http\Controllers\NotificationSettingController; use App\Http\Controllers\RateCardController; @@ -28,6 +30,16 @@ Route::prefix('client-categories')->name('client-categories.')->group(function ( Route::delete('/{category}', [ClientCategoryController::class, 'destroy'])->name('destroy'); }); +// Brand Contracts +Route::prefix('contracts')->name('contracts.')->group(function (): void { + Route::get('/', [BrandContractController::class, 'index'])->name('index'); + Route::post('/', [BrandContractController::class, 'store'])->name('store'); + Route::put('/{contract}', [BrandContractController::class, 'update'])->name('update'); + Route::delete('/{contract}', [BrandContractController::class, 'destroy'])->name('destroy'); + Route::patch('/{contract}/toggle', [BrandContractController::class, 'toggle'])->name('toggle'); + Route::get('/check', [BrandContractController::class, 'check'])->name('check'); +}); + // Schedule & Calendar Route::prefix('schedule')->name('schedule.')->group(function (): void { Route::get('/', [EventController::class, 'index'])->name('index'); @@ -48,9 +60,15 @@ Route::prefix('social-hub')->name('social-hub.')->group(function (): void { // Financial – Invoices Route::resource('invoices', InvoiceController::class); Route::get('/invoices/{invoice}/pdf', [InvoiceController::class, 'pdf'])->name('invoices.pdf'); +Route::get('/invoice-settings', [InvoiceSettingsController::class, 'edit'])->name('invoice-settings.edit'); +Route::post('/invoice-settings', [InvoiceSettingsController::class, 'update'])->name('invoice-settings.update'); // Financial – Rate Cards Route::get('/rate-cards/public', [RateCardController::class, 'public'])->name('rate-cards.public'); +Route::get('/rate-cards/pdf', [RateCardController::class, 'pdf'])->name('rate-cards.pdf'); +Route::get('/rate-cards/profile', [RateCardController::class, 'profilePage'])->name('rate-cards.profile'); +Route::post('/rate-cards/profile', [RateCardController::class, 'profileUpdate'])->name('rate-cards.profile.update'); +Route::get('/files/profile-logo', [RateCardController::class, 'serveProfileLogo'])->name('files.serve-profile-logo'); Route::resource('rate-cards', RateCardController::class)->except(['show']); // Content Strategy – Kanban