initial commit
This commit is contained in:
137
app/Services/DashboardService.php
Normal file
137
app/Services/DashboardService.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Client;
|
||||
use App\Models\ContentBrief;
|
||||
use App\Models\Event;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\SocialStat;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
final class DashboardService
|
||||
{
|
||||
public function getStats(): array
|
||||
{
|
||||
return [
|
||||
'total_clients' => Client::count(),
|
||||
'active_clients' => Client::where('status', 'active')->count(),
|
||||
'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(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined today's activities: events (today or ongoing), invoices due, content targets.
|
||||
*/
|
||||
public function getTodayActivities(): Collection
|
||||
{
|
||||
$activities = collect();
|
||||
|
||||
// Events starting today OR currently ongoing (started before today, ends today or later)
|
||||
$events = Event::with('client')
|
||||
->where(function ($q) {
|
||||
$q->whereDate('start_time', today())
|
||||
->orWhere(function ($q2) {
|
||||
$q2->whereDate('start_time', '<', today())
|
||||
->where(function ($q3) {
|
||||
$q3->whereDate('end_time', '>=', today())
|
||||
->orWhereNull('end_time');
|
||||
})
|
||||
->whereNotIn('status', ['completed', 'cancelled']);
|
||||
});
|
||||
})
|
||||
->orderBy('start_time')
|
||||
->limit(15)
|
||||
->get();
|
||||
|
||||
foreach ($events as $e) {
|
||||
$timeLabel = $e->is_all_day
|
||||
? 'All day'
|
||||
: ($e->start_time->format('H:i') . ($e->end_time ? ' – ' . $e->end_time->format('H:i') : ''));
|
||||
|
||||
$activities->push([
|
||||
'type' => 'event',
|
||||
'icon' => '📅',
|
||||
'title' => $e->title,
|
||||
'subtitle' => $timeLabel . ($e->client ? ' · ' . $e->client->name : ''),
|
||||
'label' => $e->event_type,
|
||||
'badge' => $e->status->label(),
|
||||
'badge_class' => $e->status->badgeClass(),
|
||||
'color' => $e->color ?? '#FFB7C5',
|
||||
'priority' => $e->is_priority,
|
||||
'sort_key' => $e->start_time->timestamp,
|
||||
]);
|
||||
}
|
||||
|
||||
// Invoices due today (not yet paid or cancelled)
|
||||
$invoices = Invoice::with('client')
|
||||
->whereDate('due_date', today())
|
||||
->whereNotIn('status', ['paid', 'cancelled'])
|
||||
->get();
|
||||
|
||||
foreach ($invoices as $inv) {
|
||||
$activities->push([
|
||||
'type' => 'invoice',
|
||||
'icon' => '🧾',
|
||||
'title' => $inv->title ?? $inv->invoice_number,
|
||||
'subtitle' => ($inv->client?->name ?? 'No client') . ' · ' . ($inv->formatted_total ?? ''),
|
||||
'label' => 'Invoice Due',
|
||||
'badge' => $inv->status->label(),
|
||||
'badge_class' => $inv->status->badgeClass(),
|
||||
'color' => '#FDFD96',
|
||||
'priority' => false,
|
||||
'sort_key' => today()->setTime(23, 59, 0)->timestamp,
|
||||
]);
|
||||
}
|
||||
|
||||
// Content briefs with target date today (not posted/archived)
|
||||
$content = ContentBrief::with('client')
|
||||
->whereDate('target_date', today())
|
||||
->whereNotIn('status', ['posted', 'archived'])
|
||||
->get();
|
||||
|
||||
foreach ($content as $c) {
|
||||
$activities->push([
|
||||
'type' => 'content',
|
||||
'icon' => $c->platform_icon,
|
||||
'title' => $c->title,
|
||||
'subtitle' => ucfirst($c->platform) . ($c->client ? ' · ' . $c->client->name : ''),
|
||||
'label' => 'Content Target',
|
||||
'badge' => $c->status->label(),
|
||||
'badge_class' => 'badge-ghost',
|
||||
'color' => '#E8D5F5',
|
||||
'priority' => false,
|
||||
'sort_key' => today()->setTime(12, 0, 0)->timestamp,
|
||||
]);
|
||||
}
|
||||
|
||||
return $activities->sortBy('sort_key')->values();
|
||||
}
|
||||
|
||||
public function getAlertCount(): int
|
||||
{
|
||||
return Event::upcoming(24)->count();
|
||||
}
|
||||
|
||||
public function getLatestInvoices(): Collection
|
||||
{
|
||||
return Invoice::with('client')
|
||||
->latest()
|
||||
->limit(5)
|
||||
->get();
|
||||
}
|
||||
|
||||
public function getSocialStats(): Collection
|
||||
{
|
||||
return SocialStat::whereIn('id', function ($query): void {
|
||||
$query->selectRaw('MAX(id)')
|
||||
->from('social_stats')
|
||||
->groupBy('platform');
|
||||
})->get();
|
||||
}
|
||||
}
|
||||
44
app/Services/FileService.php
Normal file
44
app/Services/FileService.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\MediaFile;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
final class FileService
|
||||
{
|
||||
public function uploadAsset(UploadedFile $file, string $folder, ?int $clientId = null): MediaFile
|
||||
{
|
||||
// Sanitise folder name — allow alphanumeric, dash, underscore only
|
||||
$folder = preg_replace('/[^a-zA-Z0-9\-_]/', '-', trim($folder)) ?: 'general';
|
||||
|
||||
$name = Str::uuid() . '.' . $file->getClientOriginalExtension();
|
||||
$directory = "talent-assets/{$folder}";
|
||||
|
||||
// Ensure the directory exists on the local disk
|
||||
Storage::disk('local')->makeDirectory($directory);
|
||||
|
||||
$path = $file->storeAs($directory, $name, 'local');
|
||||
|
||||
return MediaFile::create([
|
||||
'client_id' => $clientId,
|
||||
'name' => pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME),
|
||||
'original_name' => $file->getClientOriginalName(),
|
||||
'path' => $path,
|
||||
'disk' => 'local',
|
||||
'mime_type' => $file->getMimeType(),
|
||||
'size' => $file->getSize(),
|
||||
'folder' => $folder,
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete(MediaFile $file): void
|
||||
{
|
||||
Storage::disk($file->disk)->delete($file->path);
|
||||
$file->delete();
|
||||
}
|
||||
}
|
||||
58
app/Services/InvoiceService.php
Normal file
58
app/Services/InvoiceService.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Invoice;
|
||||
use App\Models\InvoiceItem;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
final class InvoiceService
|
||||
{
|
||||
public function create(array $data, array $items): Invoice
|
||||
{
|
||||
$invoice = Invoice::create([
|
||||
...$data,
|
||||
'invoice_number' => Invoice::generateNumber(),
|
||||
'subtotal' => 0,
|
||||
'tax_amount' => 0,
|
||||
'total' => 0,
|
||||
]);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$item['total'] = $item['quantity'] * $item['unit_price'];
|
||||
$invoice->items()->create($item);
|
||||
}
|
||||
|
||||
$invoice->recalculate();
|
||||
|
||||
return $invoice;
|
||||
}
|
||||
|
||||
public function update(Invoice $invoice, array $data, array $items): Invoice
|
||||
{
|
||||
$invoice->update($data);
|
||||
$invoice->items()->delete();
|
||||
|
||||
foreach ($items as $item) {
|
||||
$item['total'] = $item['quantity'] * $item['unit_price'];
|
||||
$invoice->items()->create($item);
|
||||
}
|
||||
|
||||
$invoice->recalculate();
|
||||
|
||||
return $invoice->fresh();
|
||||
}
|
||||
|
||||
public function generatePdf(Invoice $invoice): Response
|
||||
{
|
||||
$invoice->load('client', 'items');
|
||||
|
||||
$pdf = Pdf::loadView('invoices.pdf', compact('invoice'));
|
||||
$pdf->setPaper('A4', 'portrait');
|
||||
|
||||
return $pdf->download("invoice-{$invoice->invoice_number}.pdf");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user