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.
This commit is contained in:
126
app/Http/Controllers/BrandContractController.php
Normal file
126
app/Http/Controllers/BrandContractController.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\BrandContract;
|
||||
use App\Models\Client;
|
||||
use App\Models\ClientCategory;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class BrandContractController extends Controller
|
||||
{
|
||||
private function categoryTree(): \Illuminate\Support\Collection
|
||||
{
|
||||
return ClientCategory::with('children')
|
||||
->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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'];
|
||||
|
||||
55
app/Http/Controllers/InvoiceSettingsController.php
Normal file
55
app/Http/Controllers/InvoiceSettingsController.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\InvoiceSettings;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class InvoiceSettingsController extends Controller
|
||||
{
|
||||
public function edit(): View
|
||||
{
|
||||
$settings = InvoiceSettings::instance();
|
||||
return view('invoices.settings', compact('settings'));
|
||||
}
|
||||
|
||||
public function update(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->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!');
|
||||
}
|
||||
}
|
||||
@@ -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!');
|
||||
}
|
||||
|
||||
|
||||
@@ -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!');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user