# Visitor Counter — Custom Module

## Overview

Custom visitor tracking system built without third-party packages. Logs every page visit with device/browser/platform detection, supports authenticated user tracking and per-content view counting.

---

## Table of Contents

1. [Database Structure](#database-structure)
2. [Model & Methods](#model--methods)
3. [Traits](#traits)
4. [Usage Examples](#usage-examples)
5. [Admin Panel](#admin-panel)
6. [File Locations](#file-locations)

---

## Database Structure

### `visits` table

| Column | Type | Description |
|--------|------|-------------|
| `id` | bigint unsigned (PK, auto) | Primary key |
| `method` | varchar(255) | HTTP method (`GET`, `POST`, `PUT`, etc.) |
| `request` | mediumtext | Form/query data (auto-cast to array) |
| `url` | mediumtext | Full visited URL |
| `referer` | mediumtext | Previous page URL (HTTP_REFERER) |
| `languages` | text | Browser languages e.g. `["ms-MY","en-US"]` (auto-cast to array) |
| `useragent` | text | Full User-Agent string |
| `headers` | text | All HTTP headers (auto-cast to array) |
| `device` | text | `Desktop`, `Mobile`, `Tablet`, `Android` |
| `platform` | text | `Windows`, `macOS`, `Linux`, `Android`, `iOS`, `Unknown` |
| `browser` | text | `Chrome`, `Firefox`, `Safari`, `Edge`, `Opera`, `IE`, `Unknown` |
| `ip` | varchar(45) | Visitor IP (supports IPv6) |
| `geo_raw` | json | Optional GeoIP payload (auto-cast to array) |
| `visitable_type` | varchar(255) | Morph — model class of visited content |
| `visitable_id` | bigint unsigned | Morph — ID of visited content |
| `visitor_type` | varchar(255) | Morph — model class of the visitor user |
| `visitor_id` | bigint unsigned | Morph — ID of the visitor user |
| `created_at` | timestamp | Visit timestamp |
| `updated_at` | timestamp | Last update timestamp |

**Indexes:**
- `visits_visitable_type_visitable_id_index` — for content-based queries
- `visits_visitor_type_visitor_id_index` — for user-based queries

### `visitor_settings` table

| Column | Type | Description |
|--------|------|-------------|
| `id` | bigint unsigned (PK) | Primary key |
| `is_active` | boolean (default `true`) | Toggle logging on/off globally |
| `created_at` | timestamp | |
| `updated_at` | timestamp | |

Only **one row** exists — fetched via `VisitorSetting::first()`.

---

## Model & Methods

### `App\Models\Backend\Visit`

#### Logging

**Guard check — always log through `VisitorSetting`:**
```php
use App\Models\Backend\Visit;
use App\Models\Backend\VisitorSetting;

if (VisitorSetting::first()?->is_active) {
    Visit::log();
}
```

**Available log calls:**

```php
// Auto — uses current request + Auth::user()
Visit::log();

// With custom visitor (e.g. guest without auth)
Visit::log(visitor: $user);

// With visited content
Visit::log(visitable: $article);

// Full
Visit::log(request: $request, visitor: $user, visitable: $article);
```

**Parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `$request` | `?Request` | `request()` | HTTP request instance |
| `$visitor` | `?Model` | `Auth::user()` | The user making the visit |
| `$visitable` | `?Model` | `null` | The content/page being visited |

Returns `?Visit` — `null` if no request available.

---

#### Static Stats Methods

| Method | Return | Example Result |
|--------|--------|----------------|
| `Visit::totalVisits()` | `int` | `1500` |
| `Visit::uniqueIps()` | `int` | `342` |
| `Visit::todayVisits()` | `int` | `47` |
| `Visit::yesterdayVisits()` | `int` | `52` |
| `Visit::thisWeekVisits()` | `int` | `310` |
| `Visit::thisMonthVisits()` | `int` | `1200` |
| `Visit::last3MonthsVisits()` | `int` | `3500` |
| `Visit::last6MonthsVisits()` | `int` | `7200` |
| `Visit::thisYearVisits()` | `int` | `15000` |
| `Visit::lastYearVisits()` | `int` | `18000` |
| `Visit::uniqueVisitors()` | `int` | `89` |

---

#### Scopes (chainable `Builder`)

```php
// Time-based
Visit::today()->count();
Visit::yesterday()->count();
Visit::thisWeek()->get();
Visit::thisMonth()->get();
Visit::last3Months()->count();
Visit::last6Months()->count();
Visit::thisYear()->get();
Visit::lastYear()->count();
Visit::between('2026-01-01', '2026-05-18')->count();
Visit::online(180)->count();              // active within last 3 minutes

// Filter by visitor / visitable
Visit::byVisitor($user)->count();          // visits by a specific user
Visit::byVisitable($article)->count();     // visits on specific content

// Filter by attributes
Visit::byDevice('Mobile')->count();
Visit::byBrowser('Chrome')->count();
Visit::byPlatform('macOS')->count();
Visit::byIp('192.168.1.1')->get();
Visit::byMethod('POST')->count();
```

**Chainable example:**

```php
Visit::today()
    ->byDevice('Mobile')
    ->byBrowser('Chrome')
    ->count();
// → Number of Chrome mobile visits today
```

---

#### Relations

```php
$visit = Visit::find(1);

$visit->visitor;    // → BackendUser / FrontendUser (the visitor) or null
$visit->visitable;  // → ContentArticle / etc (the visited page) or null
```

Both are `morphTo` — polymorphic. The actual model depends on the `*_type` / `*_id` columns.

---

## Traits

### `App\Models\Traits\HasVisits`

Use on models that **act as visitors** (users).

```php
use App\Models\Traits\HasVisits;

class BackendUser extends Authenticatable
{
    use HasVisits;
}
```

**Available methods:**

| Method | Return | Description |
|--------|--------|-------------|
| `$user->visits()` | `MorphMany` | Query builder for user's visits |
| `$user->logVisit($visitable = null)` | `?Visit` | Log a visit for this user |
| `$user->totalVisits()` | `int` | Total visits by this user |
| `$user->uniqueIps()` | `int` | Unique IPs this user visited from |
| `$user->visitHistory($limit = 20)` | `Collection` | Last N visits |

**Examples:**

```php
// Log login (always guard with VisitorSetting)
if (VisitorSetting::first()?->is_active) {
    $user->logVisit();
}

// Log visit to article
if (VisitorSetting::first()?->is_active) {
    $user->logVisit($article);
}

// Get user's visit history
foreach ($user->visitHistory() as $visit) {
    echo $visit->url . ' — ' . $visit->created_at;
}
```

---

### `App\Models\Traits\Visitable`

Use on models that **are visited** (content/articles/pages).

```php
use App\Models\Traits\Visitable;

class ContentArticle extends Model
{
    use Visitable;
}
```

**Available methods:**

| Method | Return | Description |
|--------|--------|-------------|
| `$page->visitLogs()` | `MorphMany` | Query builder for visits on this content |
| `$page->totalViews()` | `int` | Total views |
| `$page->uniqueViewers()` | `int` | Unique authenticated visitors |
| `$page->uniqueViewerIps()` | `int` | Unique viewer IPs |
| `$page->dailyViews($days = 30)` | `Collection` | `['date' => 'total']` for charting |

**Examples:**

```php
$article->totalViews();              // → 240
$article->uniqueViewers();           // → 85
$article->uniqueViewerIps();         // → 120

// Chart data for last 7 days
$chart = $article->dailyViews(7);
// [
//   '2026-05-11' => 12,
//   '2026-05-12' => 8,
//   ...
// ]
```

---

## Usage Examples

### Track all page visits (global)

In `routes/web.php` or a middleware:

```php
if (VisitorSetting::first()?->is_active) {
    Visit::log();
}
```

### Track authenticated user login

```php
use App\Models\Backend\Visit;

if (VisitorSetting::first()?->is_active) {
    Visit::log(visitor: Auth::guard('admin')->user());
}
```

### Track article page views

```php
// In ContentArticleController@show
if (VisitorSetting::first()?->is_active) {
    Visit::log(visitable: $article);
}

// Or combine with authenticated user
if (VisitorSetting::first()?->is_active) {
    Visit::log(visitor: Auth::user(), visitable: $article);
}
```

### Top 10 most viewed articles

```php
use App\Models\Backend\Visit;
use App\Models\ContentArticle;

Visit::selectRaw('visitable_id, COUNT(*) as views')
    ->where('visitable_type', ContentArticle::class)
    ->groupBy('visitable_id')
    ->orderByDesc('views')
    ->limit(10)
    ->get()
    ->map(fn($v) => [
        'article' => ContentArticle::find($v->visitable_id),
        'views'   => $v->views,
    ]);
```

### Dashboard stats widget

```php
$stats = [
    'total'       => Visit::totalVisits(),
    'today'       => Visit::todayVisits(),
    'yesterday'   => Visit::yesterdayVisits(),
    'unique_ips'  => Visit::uniqueIps(),
    'active_now'  => Visit::online(180)->count(),
    'this_month'  => Visit::thisMonthVisits(),
    'last_3mo'    => Visit::last3MonthsVisits(),
    'last_6mo'    => Visit::last6MonthsVisits(),
    'this_year'   => Visit::thisYearVisits(),
];
```

### Browser / device breakdown

```php
$byBrowser = Visit::selectRaw('browser, COUNT(*) as total')
    ->groupBy('browser')
    ->orderByDesc('total')
    ->pluck('total', 'browser');

$byDevice = Visit::selectRaw('device, COUNT(*) as total')
    ->groupBy('device')
    ->orderByDesc('total')
    ->pluck('total', 'device');
```

### Clear all logs

```php
Visit::truncate();
// OR via admin panel → /admin/visitor → Clear All
```

---

## Admin Panel

URL: `/admin/visitor` (requires `visitor.view` permission)

**Features:**
- **Stats cards:** Total Visits, Unique IPs, Today Visits
- **Toggle Active/Inactive:** Disables all logging globally (does not delete data)
- **Clear All:** Truncates all visit logs (with confirmation prompt)

**Permissions:**

| Permission | Route | Action |
|------------|-------|--------|
| `visitor.view` | GET `/admin/visitor` | View stats page |
| `visitor.update` | POST `/admin/visitor/toggle` | Toggle active/inactive |
| `visitor.delete` | POST `/admin/visitor/clear` | Clear all logs |

---

## File Locations

| File | Role |
|------|------|
| `app/Models/Backend/Visit.php` | Main model with log(), stats, scopes, detection |
| `app/Models/Backend/VisitorSetting.php` | Toggle setting model |
| `app/Models/Traits/HasVisits.php` | Trait for user/visitor models |
| `app/Models/Traits/Visitable.php` | Trait for content/page models |
| `app/Http/Controllers/Backend/VisitorController.php` | Admin panel controller |
| `resources/views/backend/module/visitor/index.blade.php` | Admin panel view |
| `database/migrations/2026_05_18_190730_create_visits_table.php` | Visits table migration |
| `database/migrations/2026_05_18_194337_create_visitor_settings_table.php` | Settings table migration |
| `database/seeders/BackendMenuSeeder.php` | Menu + permission seeding |
| `routes/web.php` | Route definitions + active check in welcome route |
