user(); $setting = $this->settingFor($user->id); $credentials = $user->socialPlatformCredentials() ->get() ->keyBy('platform'); $posts = $user->socialPosts() ->latest() ->paginate(12) ->withQueryString(); $stats = [ 'scheduled' => $user->socialPosts()->where('status', 'scheduled')->count(), 'posted' => $user->socialPosts()->where('status', 'posted')->count(), 'failed' => $user->socialPosts()->where('status', 'failed')->count(), 'need_approval' => $user->socialPosts()->where('requires_approval', true)->where('status', 'draft')->count(), ]; return view('social.index', [ 'credentials' => $credentials, 'posts' => $posts, 'stats' => $stats, 'platforms' => $this->platforms(), 'setting' => $setting, ]); } public function credentialsIndex(Request $request, SocialAutomationHealthService $health): View { $user = $request->user(); $platforms = $this->platforms(); return view('social.credentials', [ 'credentials' => $user->socialPlatformCredentials() ->get() ->keyBy('platform'), 'cronStatus' => $health->cronStatus(), 'platformProgress' => $health->platformProgress($user, $platforms), 'platforms' => $platforms, ]); } public function generate(Request $request, SocialContentGeneratorService $generator): RedirectResponse { $data = $request->validate([ 'topic' => ['required', 'string', 'max:350'], 'tone' => ['nullable', 'string', 'max:100'], 'platform_hint' => ['nullable', 'string', 'max:100'], ]); try { $result = $generator->generate( $data['topic'], $data['platform_hint'] ?? 'facebook, instagram, tiktok, x', $data['tone'] ?? 'friendly-professional' ); } catch (\Throwable $e) { return redirect()->route('social.index') ->with('social_error', $e->getMessage()) ->withInput(); } return redirect()->route('social.index')->with('social_generated', $result); } public function store(Request $request): RedirectResponse { $data = $request->validate([ 'platforms' => ['required', 'array', 'min:1'], 'platforms.*' => ['required', 'in:facebook,instagram,tiktok,x'], 'title' => ['nullable', 'string', 'max:150'], 'caption' => ['required', 'string', 'max:2000'], 'hashtags' => ['nullable', 'string', 'max:500'], 'image_url' => ['nullable', 'url', 'max:2048'], 'video_url' => ['nullable', 'url', 'max:2048'], 'scheduled_at' => ['required', 'date'], 'status' => ['required', 'in:draft,scheduled'], ]); $hashtags = collect(explode(',', (string) ($data['hashtags'] ?? ''))) ->map(fn ($item) => trim($item)) ->filter() ->map(fn ($item) => ltrim($item, '#')) ->values() ->all(); $setting = $this->settingFor($request->user()->id); $scheduledAt = Carbon::parse($data['scheduled_at']); if (in_array('instagram', $data['platforms'], true) && blank($data['image_url'] ?? null)) { return redirect()->route('social.index') ->with('social_error', 'Instagram membutuhkan image_url publik sebelum post bisa dibuat.') ->withInput(); } if (in_array('tiktok', $data['platforms'], true) && blank($data['video_url'] ?? null)) { return redirect()->route('social.index') ->with('social_error', 'TikTok membutuhkan video_url publik sebelum post bisa dibuat.') ->withInput(); } foreach ($data['platforms'] as $platform) { $requiresApproval = ($data['status'] === 'scheduled') && ($setting->approval_mode === 'manual'); $status = $requiresApproval ? 'draft' : $data['status']; $request->user()->socialPosts()->create([ 'platform' => $platform, 'status' => $status, 'requires_approval' => $requiresApproval, 'title' => $data['title'] ?? null, 'caption' => $data['caption'], 'hashtags' => $hashtags, 'image_url' => $data['image_url'] ?? null, 'video_url' => $data['video_url'] ?? null, 'scheduled_at' => $scheduledAt, 'approved_at' => $requiresApproval ? null : now(), ]); } return redirect()->route('social.index')->with('social_success', 'Post berhasil ditambahkan ke antrian.'); } public function saveCredential(Request $request): RedirectResponse { $data = $request->validate([ 'platform' => ['required', 'in:facebook,instagram,tiktok,x'], 'account_label' => ['nullable', 'string', 'max:120'], 'account_id' => ['nullable', 'string', 'max:190'], 'access_token' => ['nullable', 'string', 'max:4000'], 'meta_client_id' => ['nullable', 'string', 'max:200'], 'meta_client_secret' => ['nullable', 'string', 'max:200'], ]); $existing = $request->user()->socialPlatformCredentials() ->where('platform', $data['platform']) ->first(); $existingMeta = $existing?->meta ?? []; $meta = [ 'client_id' => filled($data['meta_client_id'] ?? null) ? $data['meta_client_id'] : data_get($existingMeta, 'client_id'), 'client_secret' => filled($data['meta_client_secret'] ?? null) ? $data['meta_client_secret'] : data_get($existingMeta, 'client_secret'), ]; $request->user()->socialPlatformCredentials()->updateOrCreate( ['platform' => $data['platform']], [ 'account_label' => $data['account_label'] ?? null, 'account_id' => $data['account_id'] ?? null, 'access_token' => filled($data['access_token'] ?? null) ? $data['access_token'] : ($existing?->access_token), 'meta' => $meta, ] ); return redirect() ->to(route('social.credentials.index').'#platform-'.$data['platform']) ->with('social_success', 'Credential platform tersimpan.'); } public function saveSettings(Request $request): RedirectResponse { $data = $request->validate([ 'approval_mode' => ['required', 'in:manual,auto'], 'daily_limit' => ['required', 'integer', 'min:1', 'max:500'], 'facebook_daily_limit' => ['required', 'integer', 'min:0', 'max:500'], 'instagram_daily_limit' => ['required', 'integer', 'min:0', 'max:500'], 'tiktok_daily_limit' => ['required', 'integer', 'min:0', 'max:500'], 'x_daily_limit' => ['required', 'integer', 'min:0', 'max:500'], 'max_retries' => ['required', 'integer', 'min:0', 'max:10'], 'retry_delay_minutes' => ['required', 'integer', 'min:1', 'max:1440'], ]); $platformDailyLimits = [ 'facebook' => (int) $data['facebook_daily_limit'], 'instagram' => (int) $data['instagram_daily_limit'], 'tiktok' => (int) $data['tiktok_daily_limit'], 'x' => (int) $data['x_daily_limit'], ]; SocialAutomationSetting::query()->updateOrCreate( ['user_id' => $request->user()->id], [ 'approval_mode' => $data['approval_mode'], 'daily_limit' => (int) $data['daily_limit'], 'platform_daily_limits' => $platformDailyLimits, 'max_retries' => (int) $data['max_retries'], 'retry_delay_minutes' => (int) $data['retry_delay_minutes'], ] ); return redirect()->route('social.index')->with('social_success', 'Pengaturan automation berhasil disimpan.'); } public function approvePost(Request $request, SocialPost $post): RedirectResponse { abort_unless($post->user_id === $request->user()->id, 403); if (! $post->requires_approval) { return redirect()->route('social.index')->with('social_error', 'Post ini tidak membutuhkan approval.'); } $post->update([ 'requires_approval' => false, 'status' => 'scheduled', 'approved_at' => now(), 'error_message' => null, ]); return redirect()->route('social.index')->with('social_success', 'Post sudah di-approve dan masuk antrian scheduled.'); } public function publishNow(Request $request, SocialPost $post, SocialPublisherService $publisher): RedirectResponse { abort_unless($post->user_id === $request->user()->id, 403); if ($post->status === 'posted') { return redirect()->route('social.index')->with('social_error', 'Post ini sudah pernah dipublish.'); } if ($post->requires_approval) { return redirect()->route('social.index')->with('social_error', 'Post masih menunggu approval.'); } [$allowed, $limitMessage] = $this->canPublishNow($request->user()->id, $post->platform); if (! $allowed) { return redirect()->route('social.index')->with('social_error', $limitMessage); } $credential = $request->user()->socialPlatformCredentials() ->where('platform', $post->platform) ->first(); if (! $credential) { return redirect()->route('social.index')->with('social_error', 'Credential untuk platform '.$post->platform.' belum diisi.'); } try { $result = $publisher->publish($post, $credential); $post->update([ 'status' => 'posted', 'posted_at' => now(), 'approved_at' => $post->approved_at ?? now(), 'external_post_id' => $result['external_post_id'] ?? null, 'error_message' => null, 'meta' => $result['payload'] ?? null, 'next_retry_at' => null, ]); return redirect()->route('social.index')->with('social_success', 'Post berhasil dipublish ke '.$post->platform.'.'); } catch (\Throwable $e) { $post->update([ 'status' => 'failed', 'attempt_count' => $post->attempt_count + 1, 'error_message' => $e->getMessage(), 'next_retry_at' => null, ]); return redirect()->route('social.index')->with('social_error', $e->getMessage()); } } private function settingFor(int $userId): SocialAutomationSetting { return SocialAutomationSetting::query()->firstOrCreate( ['user_id' => $userId], [ 'approval_mode' => 'manual', 'daily_limit' => 20, 'platform_daily_limits' => [ 'facebook' => 8, 'instagram' => 8, 'tiktok' => 4, 'x' => 12, ], 'max_retries' => 2, 'retry_delay_minutes' => 15, ] ); } private function canPublishNow(int $userId, string $platform): array { $setting = $this->settingFor($userId); $today = now()->toDateString(); $postedToday = SocialPost::query() ->where('user_id', $userId) ->where('status', 'posted') ->whereDate('posted_at', $today) ->count(); if ($postedToday >= $setting->daily_limit) { return [false, 'Batas harian total post sudah tercapai.']; } $platformLimits = $setting->platform_daily_limits ?? []; $platformLimit = (int) ($platformLimits[$platform] ?? 0); if ($platformLimit > 0) { $postedPerPlatform = SocialPost::query() ->where('user_id', $userId) ->where('platform', $platform) ->where('status', 'posted') ->whereDate('posted_at', $today) ->count(); if ($postedPerPlatform >= $platformLimit) { return [false, 'Batas harian platform '.$platform.' sudah tercapai.']; } } return [true, null]; } private function platforms(): array { return [ 'facebook' => 'Facebook Page', 'instagram' => 'Instagram Business', 'tiktok' => 'TikTok', 'x' => 'X / Twitter', ]; } }