diff --git a/.dockerignore b/.dockerignore index a2cc395..be30cbf 100644 --- a/.dockerignore +++ b/.dockerignore @@ -44,7 +44,6 @@ Thumbs.db # ── Docker config itself ───────────────────── Dockerfile docker-compose*.yml -docker/ # ── Claude / project meta ──────────────────── .claude diff --git a/Dockerfile b/Dockerfile index 2a4b704..8b9af39 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,6 +24,7 @@ RUN apk add --no-cache \ nginx \ supervisor \ sqlite \ + sqlite-dev \ curl \ git \ unzip \ diff --git a/app/Http/Controllers/EventController.php b/app/Http/Controllers/EventController.php index a7c9b0b..09fdf96 100644 --- a/app/Http/Controllers/EventController.php +++ b/app/Http/Controllers/EventController.php @@ -5,12 +5,14 @@ declare(strict_types=1); namespace App\Http\Controllers; use App\Enums\EventStatus; +use App\Models\BrandContract; use App\Models\Client; use App\Models\ClientCategory; use App\Models\Event; use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\Support\Arr; use Illuminate\View\View; class EventController extends Controller @@ -51,24 +53,41 @@ class EventController extends Controller public function store(Request $request): RedirectResponse { $validated = $request->validate([ - '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', - 'location' => 'nullable|string|max:255', - 'is_priority' => 'boolean', - 'is_all_day' => 'boolean', - 'color' => 'nullable|string|max:20', - 'notes' => 'nullable|string', + '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', + 'location' => 'nullable|string|max:255', + 'is_priority' => 'boolean', + 'is_all_day' => 'boolean', + 'color' => 'nullable|string|max:20', + 'notes' => 'nullable|string', + 'create_brand_contract' => 'nullable|boolean', + 'contract_title' => 'nullable|string|max:255', + 'contract_start_date' => 'nullable|date', + 'contract_end_date' => 'nullable|date|after_or_equal:contract_start_date', ]); $validated['is_priority'] = $request->boolean('is_priority'); $validated['is_all_day'] = $request->boolean('is_all_day'); - Event::create($validated); + $event = Event::create(Arr::except($validated, [ + 'create_brand_contract', 'contract_title', 'contract_start_date', 'contract_end_date', + ])); + + if ($request->boolean('create_brand_contract') && $event->client_id) { + BrandContract::create([ + 'title' => $request->input('contract_title') ?: $event->title, + 'client_id' => $event->client_id, + 'category_id' => $event->category_id, + 'start_date' => $request->input('contract_start_date') ?? $event->start_time->toDateString(), + 'end_date' => $request->input('contract_end_date') ?? ($event->end_time?->toDateString() ?? $event->start_time->toDateString()), + 'is_active' => true, + ]); + } return redirect()->route('schedule.index') ->with('success', '🐾 Event scheduled!'); @@ -86,22 +105,39 @@ class EventController extends Controller public function update(Request $request, Event $event): RedirectResponse { $validated = $request->validate([ - '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', - 'location' => 'nullable|string|max:255', - 'is_priority' => 'boolean', - 'status' => 'required|in:pending,in_progress,completed,cancelled', - 'color' => 'nullable|string|max:20', - 'notes' => 'nullable|string', + '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', + 'location' => 'nullable|string|max:255', + 'is_priority' => 'boolean', + 'status' => 'required|in:pending,in_progress,completed,cancelled', + 'color' => 'nullable|string|max:20', + 'notes' => 'nullable|string', + 'create_brand_contract' => 'nullable|boolean', + 'contract_title' => 'nullable|string|max:255', + 'contract_start_date' => 'nullable|date', + 'contract_end_date' => 'nullable|date|after_or_equal:contract_start_date', ]); $validated['is_priority'] = $request->boolean('is_priority'); - $event->update($validated); + $event->update(Arr::except($validated, [ + 'create_brand_contract', 'contract_title', 'contract_start_date', 'contract_end_date', + ])); + + if ($request->boolean('create_brand_contract') && $event->client_id) { + BrandContract::create([ + 'title' => $request->input('contract_title') ?: $event->title, + 'client_id' => $event->client_id, + 'category_id' => $event->category_id, + 'start_date' => $request->input('contract_start_date') ?? $event->start_time->toDateString(), + 'end_date' => $request->input('contract_end_date') ?? ($event->end_time?->toDateString() ?? $event->start_time->toDateString()), + 'is_active' => true, + ]); + } return redirect()->route('schedule.index') ->with('success', '🐾 Event updated!'); diff --git a/app/Http/Controllers/InvoiceSettingsController.php b/app/Http/Controllers/InvoiceSettingsController.php index 01e3e03..251187f 100644 --- a/app/Http/Controllers/InvoiceSettingsController.php +++ b/app/Http/Controllers/InvoiceSettingsController.php @@ -26,6 +26,7 @@ class InvoiceSettingsController extends Controller 'header_name' => 'nullable|string|max:100', 'header_title' => 'nullable|string|max:150', 'header_logo' => 'nullable|image|max:2048', + 'accent_color' => 'nullable|string|max:20', 'default_tax_rate' => 'nullable|numeric|min:0|max:100', 'default_currency' => 'nullable|string|max:10', 'default_service_description'=> 'nullable|string|max:255', diff --git a/app/Models/InvoiceSettings.php b/app/Models/InvoiceSettings.php index cf81fac..c9fc2af 100644 --- a/app/Models/InvoiceSettings.php +++ b/app/Models/InvoiceSettings.php @@ -16,6 +16,7 @@ class InvoiceSettings extends Model 'header_name', 'header_title', 'header_logo', + 'accent_color', 'default_tax_rate', 'default_currency', 'default_service_description', @@ -37,6 +38,7 @@ class InvoiceSettings extends Model return static::firstOrCreate([], [ 'invoice_prefix' => 'INV', 'invoice_next_number' => 1, + 'accent_color' => '#FFB7C5', 'default_tax_rate' => 11, 'default_currency' => 'IDR', ]); diff --git a/config/filesystems.php b/config/filesystems.php index 37d8fca..e3ebbd5 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -38,6 +38,13 @@ return [ 'report' => false, ], + 'private' => [ + 'driver' => 'local', + 'root' => storage_path('app/private'), + 'serve' => false, + 'throw' => false, + ], + 'public' => [ 'driver' => 'local', 'root' => storage_path('app/public'), diff --git a/database/migrations/2026_04_01_200000_add_accent_color_to_invoice_settings.php b/database/migrations/2026_04_01_200000_add_accent_color_to_invoice_settings.php new file mode 100644 index 0000000..2dbfed4 --- /dev/null +++ b/database/migrations/2026_04_01_200000_add_accent_color_to_invoice_settings.php @@ -0,0 +1,22 @@ +string('accent_color', 20)->default('#FFB7C5')->after('header_logo'); + }); + } + + public function down(): void + { + Schema::table('invoice_settings', function (Blueprint $table) { + $table->dropColumn('accent_color'); + }); + } +}; diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 922c367..15fe6b5 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -45,6 +45,10 @@ chmod -R 775 \ echo "[migrate] Running database migrations…" php artisan migrate --force --no-interaction +# ── 5b. Create public storage symlink ──────── +echo "[storage] Creating public storage symlink…" +php artisan storage:link --quiet 2>/dev/null || true + # ── 6. Cache config / routes / views ──────── echo "[optimize] Caching configuration…" php artisan config:cache --quiet diff --git a/docker/supervisord.conf b/docker/supervisord.conf index 457465b..0b088d0 100644 --- a/docker/supervisord.conf +++ b/docker/supervisord.conf @@ -38,11 +38,10 @@ stderr_logfile_maxbytes=0 ; ── Laravel Queue Worker ────────────────────────────────────────────────── [program:queue] -command=php /var/www/html/artisan queue:work \ - --sleep=3 \ - --tries=3 \ - --max-time=3600 \ - --queue=default +command=php /var/www/html/artisan queue:work --sleep=3 --tries=3 --max-time=3600 --queue=default + + +queue=default directory=/var/www/html user=www-data autostart=true diff --git a/resources/views/files/index.blade.php b/resources/views/files/index.blade.php index 88164ee..54fe180 100644 --- a/resources/views/files/index.blade.php +++ b/resources/views/files/index.blade.php @@ -6,8 +6,30 @@ @section('content') +{{-- ── File Detail Popup Modal ──────────────────────────────── --}} +
+ {{-- ── Upload Form (collapsible) ────────────────────────────── --}} -No files yet.
-Applied to your invoice header name and totals.
+