item visitor log
This commit is contained in:
parent
089308d86c
commit
0f6dbb95e7
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AnalyticsProductVisit extends Model
|
||||
{
|
||||
protected $connection = 'ecommerce';
|
||||
protected $table = 'analytics_product_visits';
|
||||
|
||||
|
||||
protected $fillable = [
|
||||
'item_id',
|
||||
'user_id',
|
||||
'session_id',
|
||||
'started_at',
|
||||
'ended_at',
|
||||
'duration_seconds',
|
||||
'ip_address',
|
||||
'device_type',
|
||||
'user_agent',
|
||||
'referrer',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'started_at' => '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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,255 @@
|
|||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\AnalyticsProductVisit;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AnalyticsProductVisitRepository
|
||||
{
|
||||
/**
|
||||
* Get all product visits with optional filtering
|
||||
*/
|
||||
public function getAll(array $filters = []): Collection
|
||||
{
|
||||
$query = AnalyticsProductVisit::query();
|
||||
|
||||
// Filter by product ID
|
||||
if (isset($filters['item_id'])) {
|
||||
$query->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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection('ecommerce')->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');
|
||||
}
|
||||
};
|
||||
Loading…
Reference in New Issue