From 0f6dbb95e7bac2ecf40ba749cbe7adca905489e9 Mon Sep 17 00:00:00 2001 From: Bayu Lukman Yusuf Date: Tue, 3 Mar 2026 11:54:40 +0700 Subject: [PATCH] item visitor log --- app/Http/Controllers/ProductController.php | 49 +++- app/Models/AnalyticsProductVisit.php | 40 +++ .../AnalyticsProductVisitRepository.php | 255 ++++++++++++++++++ config/database.php | 13 + ..._create_analytics_product_visits_table.php | 47 ++++ 5 files changed, 403 insertions(+), 1 deletion(-) create mode 100644 app/Models/AnalyticsProductVisit.php create mode 100644 app/Repositories/AnalyticsProductVisitRepository.php create mode 100644 database/migrations/2026_03_03_015321_create_analytics_product_visits_table.php diff --git a/app/Http/Controllers/ProductController.php b/app/Http/Controllers/ProductController.php index 30afa56..87a0d0e 100644 --- a/app/Http/Controllers/ProductController.php +++ b/app/Http/Controllers/ProductController.php @@ -6,6 +6,7 @@ use App\Models\Gender; use App\Models\StoreCategory; use App\Repositories\Catalog\CategoryRepository; use App\Repositories\Catalog\GenderRepository; +use App\Repositories\AnalyticsProductVisitRepository; use App\Repositories\Catalog\ProductRepository; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; @@ -477,11 +478,57 @@ class ProductController extends Controller ]); } + /** + * Track product visit analytics + */ + private function trackProductVisit(int $productId, Request $request): void + { + try { + $analyticsRepository = new AnalyticsProductVisitRepository(); + + $visitData = [ + 'item_id' => $productId, + 'user_id' => auth()->id(), + 'session_id' => session()->getId(), + 'ip_address' => $request->ip(), + 'user_agent' => $request->userAgent(), + 'started_at' => now(), + 'duration_seconds' => 0, // Will be updated when user leaves + 'device_type' => $this->getDeviceType($request->userAgent()), + ]; + + $analyticsRepository->create($visitData); + } catch (\Exception $e) { + // Log error but don't break the application + \Log::error('Failed to track product visit: ' . $e->getMessage()); + } + } + + /** + * Detect device type from user agent + */ + private function getDeviceType(string $userAgent): string + { + $userAgent = strtolower($userAgent); + + if (strpos($userAgent, 'mobile') !== false) { + return 'Mobile'; + } + + if (strpos($userAgent, 'tablet') !== false) { + return 'Tablet'; + } + + return 'Desktop'; + } + public function detailFashion($slug, Request $request, ProductRepository $productRepository) { - $product = $productRepository->show($slug); + // Track product visit analytics + $this->trackProductVisit($product->id, $request); + $complete_look_products_data = $productRepository->getList([ 'category_id' => $product->category1_id, 'limit' => 4, diff --git a/app/Models/AnalyticsProductVisit.php b/app/Models/AnalyticsProductVisit.php new file mode 100644 index 0000000..9647aa2 --- /dev/null +++ b/app/Models/AnalyticsProductVisit.php @@ -0,0 +1,40 @@ + 'datetime', + 'ended_at' => 'datetime', + ]; + + + public function calculateDuration(): void + { + if ($this->started_at && $this->ended_at) { + $this->duration_seconds = + $this->ended_at->diffInSeconds($this->started_at); + } + } + +} diff --git a/app/Repositories/AnalyticsProductVisitRepository.php b/app/Repositories/AnalyticsProductVisitRepository.php new file mode 100644 index 0000000..e9f1d1b --- /dev/null +++ b/app/Repositories/AnalyticsProductVisitRepository.php @@ -0,0 +1,255 @@ +where('item_id', $filters['item_id']); + } + + // Filter by user ID + if (isset($filters['user_id'])) { + $query->where('user_id', $filters['user_id']); + } + + // Filter by date range + if (isset($filters['date_from'])) { + $query->where('started_at', '>=', $filters['date_from']); + } + + if (isset($filters['date_to'])) { + $query->where('started_at', '<=', $filters['date_to']); + } + + // Filter by IP address + if (isset($filters['ip_address'])) { + $query->where('ip_address', $filters['ip_address']); + } + + return $query->orderBy('started_at', 'desc')->get(); + } + + /** + * Get product visits with pagination + */ + public function getPaginated(array $filters = [], int $perPage = 20): LengthAwarePaginator + { + $query = AnalyticsProductVisit::query(); + + // Apply same filters as getAll method + if (isset($filters['item_id'])) { + $query->where('item_id', $filters['item_id']); + } + + if (isset($filters['user_id'])) { + $query->where('user_id', $filters['user_id']); + } + + if (isset($filters['date_from'])) { + $query->where('started_at', '>=', $filters['date_from']); + } + + if (isset($filters['date_to'])) { + $query->where('started_at', '<=', $filters['date_to']); + } + + return $query->orderBy('started_at', 'desc') + ->paginate($perPage); + } + + /** + * Create a new product visit record + */ + public function create(array $data): AnalyticsProductVisit + { + return AnalyticsProductVisit::create($data); + } + + /** + * Update existing product visit + */ + public function update(int $id, array $data): bool + { + $visit = AnalyticsProductVisit::find($id); + + if (!$visit) { + return false; + } + + return $visit->update($data); + } + + /** + * Delete a product visit record + */ + public function delete(int $id): bool + { + $visit = AnalyticsProductVisit::find($id); + + if (!$visit) { + return false; + } + + return $visit->delete(); + } + + /** + * Get visit statistics + */ + public function getStatistics(array $filters = []): array + { + $query = AnalyticsProductVisit::query(); + + // Apply date filters + if (isset($filters['date_from'])) { + $query->where('started_at', '>=', $filters['date_from']); + } + + if (isset($filters['date_to'])) { + $query->where('started_at', '<=', $filters['date_to']); + } + + $totalVisits = $query->count(); + + $uniqueVisitors = $query->distinct('user_id')->count('user_id'); + + $averageDuration = $query->avg('duration_seconds'); + + $totalDuration = $query->sum('duration_seconds'); + + return [ + 'total_visits' => $totalVisits, + 'unique_visitors' => $uniqueVisitors, + 'average_duration_seconds' => round($averageDuration, 2), + 'total_duration_seconds' => $totalDuration, + 'average_duration_formatted' => $this->formatDuration($averageDuration), + 'total_duration_formatted' => $this->formatDuration($totalDuration), + ]; + } + + /** + * Get most visited products + */ + public function getMostVisitedProducts(int $limit = 10): array + { + return AnalyticsProductVisit::query() + ->selectRaw('item_id, COUNT(*) as visit_count, AVG(duration_seconds) as avg_duration') + ->groupBy('item_id') + ->orderBy('visit_count', 'desc') + ->limit($limit) + ->get() + ->toArray(); + } + + /** + * Get visits by device type + */ + public function getVisitsByDeviceType(array $filters = []): array + { + $query = AnalyticsProductVisit::query(); + + if (isset($filters['date_from'])) { + $query->where('started_at', '>=', $filters['date_from']); + } + + if (isset($filters['date_to'])) { + $query->where('started_at', '<=', $filters['date_to']); + } + + return $query->selectRaw('device_type, COUNT(*) as count') + ->groupBy('device_type') + ->orderBy('count', 'desc') + ->get() + ->toArray(); + } + + /** + * Get daily visit statistics + */ + public function getDailyStatistics(array $filters = []): array + { + $query = AnalyticsProductVisit::query(); + + if (isset($filters['date_from'])) { + $query->where('started_at', '>=', $filters['date_from']); + } + + if (isset($filters['date_to'])) { + $query->where('started_at', '<=', $filters['date_to']); + } + + return $query->selectRaw('DATE(started_at) as date, COUNT(*) as visits, COUNT(DISTINCT user_id) as unique_visitors') + ->groupBy('date') + ->orderBy('date', 'desc') + ->get() + ->toArray(); + } + + /** + * Format duration in human readable format + */ + private function formatDuration(float $seconds): string + { + if ($seconds < 60) { + return round($seconds) . 's'; + } + + if ($seconds < 3600) { + $minutes = floor($seconds / 60); + $remainingSeconds = $seconds % 60; + return $minutes . 'm ' . round($remainingSeconds) . 's'; + } + + $hours = floor($seconds / 3600); + $remainingSeconds = $seconds % 3600; + $minutes = floor($remainingSeconds / 60); + $remainingSeconds = $remainingSeconds % 60; + + return $hours . 'h ' . $minutes . 'm ' . round($remainingSeconds) . 's'; + } + + /** + * Get product visit by ID + */ + public function findById(int $id): ?AnalyticsProductVisit + { + return AnalyticsProductVisit::find($id); + } + + /** + * Get visits by session ID + */ + public function getBySessionId(string $sessionId): Collection + { + return AnalyticsProductVisit::where('session_id', $sessionId) + ->orderBy('started_at', 'desc') + ->get(); + } + + /** + * Bulk insert visits + */ + public function bulkInsert(array $visits): bool + { + try { + AnalyticsProductVisit::insert($visits); + return true; + } catch (\Exception $e) { + return false; + } + } +} diff --git a/config/database.php b/config/database.php index 42b3e25..450a8b6 100644 --- a/config/database.php +++ b/config/database.php @@ -112,6 +112,19 @@ return [ // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), ], + + + 'ecommerce' => [ + 'driver' => 'sqlite', + 'url' => env('DB_ECOMMERCE_URL'), + 'database' => env('DB_ECOMMERCE_DATABASE', database_path('ecommerce.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_ECOMMERCE_FOREIGN_KEYS', true), + 'busy_timeout' => null, + 'journal_mode' => null, + 'synchronous' => null, + ], + ], /* diff --git a/database/migrations/2026_03_03_015321_create_analytics_product_visits_table.php b/database/migrations/2026_03_03_015321_create_analytics_product_visits_table.php new file mode 100644 index 0000000..ba23305 --- /dev/null +++ b/database/migrations/2026_03_03_015321_create_analytics_product_visits_table.php @@ -0,0 +1,47 @@ +create('analytics_product_visits', function (Blueprint $table) { + $table->id(); + + // Relasi dasar + $table->unsignedBigInteger('item_id')->index(); + $table->unsignedBigInteger('user_id')->nullable()->index(); + $table->string('session_id')->nullable()->index(); + + // Tracking waktu + $table->timestamp('started_at')->index(); + $table->timestamp('ended_at')->nullable(); + $table->integer('duration_seconds')->nullable(); + + // Optional analytics tambahan + $table->string('ip_address', 45)->nullable(); + $table->string('device_type')->nullable(); + $table->text('user_agent')->nullable(); + $table->string('referrer')->nullable(); + + $table->timestamps(); + + // Index tambahan untuk performa + $table->index(['item_id', 'started_at']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::connection('ecommerce')->dropIfExists('analytics_product_visits'); + } +};