initial commit

This commit is contained in:
I-Fun
2026-04-01 08:04:56 +07:00
commit e09c8a5fa9
111 changed files with 18022 additions and 0 deletions

View 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();
}
}

View 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();
}
}

View 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");
}
}