Initial commit
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled

This commit is contained in:
2025-09-12 16:19:56 +00:00
commit 6b878bb0a0
13901 changed files with 88110 additions and 0 deletions

18
.editorconfig Normal file
View File

@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[docker-compose.yml]
indent_size = 4

65
.env.example Normal file
View File

@ -0,0 +1,65 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"

10
.gitattributes vendored Normal file
View File

@ -0,0 +1,10 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
CHANGELOG.md export-ignore
README.md export-ignore

51
.github/prompt.yml vendored Normal file
View File

@ -0,0 +1,51 @@
You are an expert Laravel + Livewire + API integration developer.
I am building a modular GPS tracking system using Laravel Livewire for the frontend and Traccar as the backend.
🧩 Project Context:
- Laravel project is already set up using a Livewire starter kit.
- I have imported the `traccar openapi.yaml` file so all endpoints are available.
- Livewire components must be structured **one per feature** for clean maintainability.
- Laravel will manage authentication, roles, and permissions using spatie/laravel-permission.
- Users log into Laravel; Laravel integrates with Traccar using **basic auth (username + password)**.
- Backend services to Traccar must be wrapped in service classes (e.g., `App\Services\TraccarService`) for reuse.
📌 Feature Roadmap (each is its own Livewire module):
1. Authentication & User Management
- Laravel handles auth (register, login, roles, permissions).
2. Dashboard
- Overview of devices, active connections, alerts.
- Widgets for devices online/offline, last positions, trips today.
3. Device Management
- CRUD devices, assign to users, manage attributes (IMEI, protocol, etc.).
4. Live Tracking
- Interactive map (Leaflet/Mapbox/Google Maps).
- Show live location of users devices, with status indicators.
- WebSocket/interval polling for updates.
5. Geofences
- Create/edit/delete polygons and circles.
- Assign devices to geofences.
- Trigger alerts when devices enter/exit.
6. Events & Alerts
- Show overspeed, geofence breach, SOS, and other alerts.
- Notifications in UI, optional email/SMS.
7. Reports & History
- Trip history, mileage reports, export to PDF/Excel.
8. Admin Panel
- Role/permission management.
- Manage users, assign devices.
- System settings (API keys, map config).
9. Drivers & Groups
- Assign drivers to devices.
- Group devices for management.
10. Commands
- Send commands to devices (engine stop/start, SOS reset).
- Track command status.
11. Notifications & Logs
- Centralized logs of API calls, errors, device messages.
- System audit trail.
12. Billing & Subscription (Optional)
- SaaS setup with Stripe/Paddle.
- Device limits per subscription.

46
.github/workflows/lint.yml vendored Normal file
View File

@ -0,0 +1,46 @@
name: linter
on:
push:
branches:
- develop
- main
pull_request:
branches:
- develop
- main
permissions:
contents: write
jobs:
quality:
runs-on: ubuntu-latest
environment: Testing
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
- name: Add Flux Credentials Loaded From ENV
run: composer config http-basic.composer.fluxui.dev "${{ secrets.FLUX_USERNAME }}" "${{ secrets.FLUX_LICENSE_KEY }}"
- name: Install Dependencies
run: |
composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
npm install
- name: Run Pint
run: vendor/bin/pint
# - name: Commit Changes
# uses: stefanzweifel/git-auto-commit-action@v5
# with:
# commit_message: fix code style
# commit_options: '--no-verify'
# file_pattern: |
# **/*
# !.github/workflows/*

54
.github/workflows/tests.yml vendored Normal file
View File

@ -0,0 +1,54 @@
name: tests
on:
push:
branches:
- develop
- main
pull_request:
branches:
- develop
- main
jobs:
ci:
runs-on: ubuntu-latest
environment: Testing
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.4
tools: composer:v2
coverage: xdebug
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Install Node Dependencies
run: npm i
- name: Add Flux Credentials Loaded From ENV
run: composer config http-basic.composer.fluxui.dev "${{ secrets.FLUX_USERNAME }}" "${{ secrets.FLUX_LICENSE_KEY }}"
- name: Install Dependencies
run: composer install --no-interaction --prefer-dist --optimize-autoloader
- name: Copy Environment File
run: cp .env.example .env
- name: Generate Application Key
run: php artisan key:generate
- name: Build Assets
run: npm run build
- name: Run Tests
run: ./vendor/bin/pest

23
.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
/.phpunit.cache
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/storage/pail
/vendor
.env
.env.backup
.env.production
.phpactor.json
.phpunit.result.cache
Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
/auth.json
/.fleet
/.idea
/.nova
/.vscode
/.zed

View File

@ -0,0 +1,55 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Services\TraccarService;
class TestTraccarConnection extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'traccar:test';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Test Traccar API connection';
/**
* Execute the console command.
*/
public function handle(TraccarService $traccarService)
{
$this->info('Testing Traccar API connection...');
try {
// Test connection
if ($traccarService->testConnection()) {
$this->info('✅ Connection successful!');
// Get server info
$serverInfo = $traccarService->getServerInfo();
$this->info('Server Version: ' . ($serverInfo['version'] ?? 'Unknown'));
// Get devices
$devices = $traccarService->getDevices();
$this->info('Total devices: ' . count($devices));
// Get positions
$positions = $traccarService->getPositions();
$this->info('Total positions: ' . count($positions));
} else {
$this->error('❌ Connection failed!');
}
} catch (\Exception $e) {
$this->error('❌ Error: ' . $e->getMessage());
}
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
class VerifyEmailController extends Controller
{
/**
* Mark the authenticated user's email address as verified.
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
$request->fulfill();
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserIsActive
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
// Check if user is authenticated and still active
if (Auth::check()) {
$user = Auth::user();
// If user is not active, log them out
if (!$user->isActive()) {
Auth::logout();
// If it's an AJAX/Livewire request, return JSON response
if ($request->expectsJson() || $request->header('X-Livewire')) {
return response()->json([
'message' => 'Your account has been suspended or deactivated.',
'redirect' => route('login')
], 401);
}
// For regular requests, redirect to login with message
return redirect()->route('login')->with('error', 'Your account has been suspended or deactivated. Please contact an administrator.');
}
}
return $next($request);
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Login;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class UpdateLastLoginTime
{
/**
* Create the event listener.
*/
public function __construct()
{
//
}
/**
* Handle the event.
*/
public function handle(Login $event): void
{
$event->user->update([
'last_login_at' => now(),
]);
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Livewire\Actions;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
class Logout
{
/**
* Log the current user out of the application.
*/
public function __invoke()
{
Auth::guard('web')->logout();
Session::invalidate();
Session::regenerateToken();
return redirect('/');
}
}

View File

@ -0,0 +1,123 @@
<?php
namespace App\Livewire;
use App\Models\User;
use App\Models\Device;
use App\Models\Event;
use App\Models\Command;
use App\Models\Subscription;
use Livewire\Component;
use Livewire\WithPagination;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
class AdminDashboard extends Component
{
use WithPagination;
public $statsDateRange = '30'; // days
// Activity monitoring
public $recentUsers = [];
public $recentEvents = [];
public $systemAlerts = [];
public function mount()
{
$this->loadRecentActivity();
}
public function loadRecentActivity()
{
// Load recent users
$this->recentUsers = User::latest()
->limit(5)
->get(['id', 'name', 'email', 'created_at', 'last_login_at', 'status']);
// Load recent critical events
$this->recentEvents = Event::with(['device.user'])
->whereIn('type', ['alarm', 'deviceOffline', 'deviceOverspeed', 'panic'])
->latest('event_time')
->limit(10)
->get();
// Load system alerts (could be from logs, failed commands, etc.)
$this->systemAlerts = [
[
'type' => 'warning',
'message' => 'High API usage detected',
'time' => now()->subMinutes(15),
],
[
'type' => 'info',
'message' => 'Scheduled maintenance completed',
'time' => now()->subHours(2),
],
];
}
public function getStatsProperty()
{
$days = (int) $this->statsDateRange;
$startDate = now()->subDays($days);
return [
'total_users' => User::count(),
'active_users' => User::where('status', 'active')->count(),
'new_users' => User::where('created_at', '>=', $startDate)->count(),
'total_devices' => Device::count(),
'online_devices' => Device::where('status', 'online')->count(),
'total_events' => Event::where('event_time', '>=', $startDate)->count(),
'critical_events' => Event::whereIn('type', ['alarm', 'deviceOffline', 'deviceOverspeed', 'panic'])
->where('event_time', '>=', $startDate)->count(),
'pending_commands' => Command::where('status', 'pending')->count(),
'active_subscriptions' => Subscription::where('status', 'active')->count(),
'revenue_this_month' => Subscription::where('status', 'active')
->whereMonth('created_at', now()->month)
->sum('price'),
];
}
public function getUserGrowthDataProperty()
{
$data = User::select(
DB::raw('DATE(created_at) as date'),
DB::raw('COUNT(*) as count')
)
->where('created_at', '>=', now()->subDays(30))
->groupBy('date')
->orderBy('date')
->get();
return [
'labels' => $data->pluck('date')->map(fn($date) => Carbon::parse($date)->format('M j'))->toArray(),
'data' => $data->pluck('count')->toArray(),
];
}
public function getDeviceStatusDataProperty()
{
$statusCounts = Device::select('status', DB::raw('COUNT(*) as count'))
->groupBy('status')
->get()
->pluck('count', 'status')
->toArray();
return [
'labels' => array_keys($statusCounts),
'data' => array_values($statusCounts),
];
}
public function refreshStats()
{
$this->loadRecentActivity();
session()->flash('success', 'Dashboard data refreshed!');
}
public function render()
{
return view('livewire.admin-dashboard');
}
}

View File

@ -0,0 +1,165 @@
<?php
namespace App\Livewire;
use App\Models\Command;
use Livewire\Component;
use Livewire\WithPagination;
class CommandCenter extends Component
{
use WithPagination;
public $filters = [
'search' => '',
'status' => '',
'type' => '',
'device_id' => '',
'date_range' => '',
];
public $showSendCommandModal = false;
public $showDetailsModal = false;
public $selectedCommand = null;
public $commandForm = [
'device_id' => '',
'type' => '',
'custom_command' => '',
'description' => '',
'duration' => '',
'expires_at' => '',
];
protected $layout = 'layouts.app';
public function getStatsProperty()
{
$totalCommands = Command::count();
$successfulCommands = Command::where('status', 'acknowledged')->count();
$pendingCommands = Command::whereIn('status', ['pending', 'sent'])->count();
$failedCommands = Command::where('status', 'failed')->count();
return [
'total_commands' => $totalCommands,
'successful_commands' => $successfulCommands,
'pending_commands' => $pendingCommands,
'failed_commands' => $failedCommands,
];
}
public function render()
{
$commands = Command::query()
->with('user')
->when($this->filters['search'], function ($query) {
$query->where(function ($q) {
$q->where('type', 'like', '%' . $this->filters['search'] . '%')
->orWhere('device_id', 'like', '%' . $this->filters['search'] . '%')
->orWhere('description', 'like', '%' . $this->filters['search'] . '%');
});
})
->when($this->filters['status'], function ($query) {
$query->where('status', $this->filters['status']);
})
->when($this->filters['type'], function ($query) {
$query->where('type', $this->filters['type']);
})
->when($this->filters['device_id'], function ($query) {
$query->where('device_id', 'like', '%' . $this->filters['device_id'] . '%');
})
->when($this->filters['date_range'], function ($query) {
match($this->filters['date_range']) {
'today' => $query->whereDate('created_at', today()),
'week' => $query->where('created_at', '>=', now()->subWeek()),
'month' => $query->where('created_at', '>=', now()->subMonth()),
default => null,
};
})
->orderBy('created_at', 'desc')
->paginate(15);
return view('livewire.command-center', [
'commands' => $commands,
]);
}
public function sendCommand()
{
$this->validate([
'commandForm.device_id' => 'required|string',
'commandForm.type' => 'required|string',
]);
$parameters = [];
if ($this->commandForm['duration']) {
$parameters['duration'] = $this->commandForm['duration'];
}
Command::create([
'device_id' => $this->commandForm['device_id'],
'type' => $this->commandForm['type'],
'description' => $this->commandForm['description'],
'parameters' => !empty($parameters) ? $parameters : null,
'status' => 'pending',
'expires_at' => $this->commandForm['expires_at'] ? \Carbon\Carbon::parse($this->commandForm['expires_at']) : null,
'user_id' => auth()->id(),
]);
$this->closeSendCommandModal();
$this->reset('commandForm');
session()->flash('message', 'Command sent successfully.');
}
public function viewCommand($commandId)
{
$this->selectedCommand = Command::with('user')->find($commandId);
$this->showDetailsModal = true;
}
public function cancelCommand($commandId)
{
Command::find($commandId)->update(['status' => 'cancelled']);
session()->flash('message', 'Command cancelled successfully.');
}
public function retryCommand($commandId)
{
Command::find($commandId)->update(['status' => 'pending']);
session()->flash('message', 'Command retry initiated.');
}
public function closeSendCommandModal()
{
$this->showSendCommandModal = false;
$this->reset('commandForm');
}
// Handle legacy method name (backward compatibility)
public function showSendcommandModal($deviceId = null)
{
if ($deviceId) {
$this->commandForm['device_id'] = $deviceId;
}
$this->showSendCommandModal = true;
}
// Handle legacy method name (backward compatibility)
public function showCommanddetailsModal($commandId = null)
{
if ($commandId) {
$this->viewCommand($commandId);
}
}
public function closeDetailsModal()
{
$this->showDetailsModal = false;
$this->selectedCommand = null;
}
public function updatingFilters()
{
$this->resetPage();
}
}

191
app/Livewire/Dashboard.php Normal file
View File

@ -0,0 +1,191 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use App\Services\TraccarService;
use App\Models\Device;
use App\Models\Event;
use App\Models\Position;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
class Dashboard extends Component
{
public $stats = [];
public $recentEvents = [];
public $devicesStatus = [];
public $mapDevices = [];
protected $traccarService;
public function boot(TraccarService $traccarService)
{
$this->traccarService = $traccarService;
}
public function mount()
{
$this->loadDashboardData();
}
public function loadDashboardData()
{
$user = Auth::user();
// Get user's devices
$devices = Device::where('user_id', $user->id)->get();
// Calculate statistics
$this->stats = [
'total_devices' => $devices->count(),
'online_devices' => $devices->where('status', 'online')->count(),
'offline_devices' => $devices->where('status', 'offline')->count(),
'recent_alerts' => Event::whereIn('device_id', $devices->pluck('id'))
->where('acknowledged', false)
->where('created_at', '>=', now()->subDays(7))
->count(),
];
// Get devices status for widgets
$this->devicesStatus = $devices->map(function ($device) {
return [
'id' => $device->id,
'name' => $device->name,
'status' => $device->status,
'last_update' => $device->last_update,
'is_online' => $device->isOnline(),
'position' => $device->getLatestPosition(),
];
})->toArray();
// Get recent events
$this->recentEvents = Event::with(['device', 'geofence'])
->whereIn('device_id', $devices->pluck('id'))
->orderBy('event_time', 'desc')
->limit(10)
->get()
->map(function ($event) {
return [
'id' => $event->id,
'type' => $event->getReadableType(),
'device_name' => $event->device->name,
'event_time' => $event->event_time,
'acknowledged' => $event->acknowledged,
'severity' => $event->getSeverity(),
'color' => $event->getEventColor(),
];
})->toArray();
// Get devices for map display
$this->mapDevices = $devices->filter(function ($device) {
return $device->currentPosition && $device->currentPosition->valid;
})->map(function ($device) {
$position = $device->currentPosition;
return [
'id' => $device->id,
'name' => $device->name,
'latitude' => $position->latitude,
'longitude' => $position->longitude,
'status' => $device->status,
'speed' => $position->getFormattedSpeed(),
'address' => $position->address ?? 'Unknown location',
'last_update' => $position->device_time,
];
})->values()->toArray();
}
public function acknowledgeEvent($eventId)
{
$event = Event::find($eventId);
if ($event && $event->device->user_id === Auth::id()) {
$event->update([
'acknowledged' => true,
'acknowledged_by' => Auth::id(),
'acknowledged_at' => now(),
]);
$this->loadDashboardData();
$this->dispatch('event-acknowledged');
}
}
public function refreshData()
{
try {
// Sync with Traccar API
$this->syncTraccarData();
$this->loadDashboardData();
$this->dispatch('data-refreshed');
} catch (\Exception $e) {
$this->dispatch('refresh-error', ['message' => 'Failed to refresh data: ' . $e->getMessage()]);
}
}
private function syncTraccarData()
{
$user = Auth::user();
if (!$user->traccar_user_id) {
return; // User not synced with Traccar
}
// Get latest positions from Traccar
$positions = $this->traccarService->getPositions();
foreach ($positions as $positionData) {
$device = Device::where('traccar_device_id', $positionData['deviceId'])->first();
if ($device && $device->user_id === $user->id) {
// Update or create position
$position = Position::updateOrCreate(
['traccar_position_id' => $positionData['id']],
[
'device_id' => $device->id,
'protocol' => $positionData['protocol'] ?? null,
'device_time' => $positionData['deviceTime'],
'fix_time' => $positionData['fixTime'],
'server_time' => $positionData['serverTime'],
'outdated' => $positionData['outdated'] ?? false,
'valid' => $positionData['valid'] ?? true,
'latitude' => $positionData['latitude'],
'longitude' => $positionData['longitude'],
'altitude' => $positionData['altitude'] ?? null,
'speed' => $positionData['speed'] ?? 0,
'course' => $positionData['course'] ?? 0,
'address' => $positionData['address'] ?? null,
'accuracy' => $positionData['accuracy'] ?? null,
'attributes' => $positionData['attributes'] ?? null,
]
);
// Update device status and position
$device->update([
'status' => $this->determineDeviceStatus($positionData),
'last_update' => $positionData['deviceTime'],
'position_id' => $position->id,
]);
}
}
}
private function determineDeviceStatus($positionData): string
{
$deviceTime = new \DateTime($positionData['deviceTime']);
$now = new \DateTime();
$diffMinutes = $now->diff($deviceTime)->i + ($now->diff($deviceTime)->h * 60);
if ($diffMinutes <= 5) {
return 'online';
} elseif ($diffMinutes <= 30) {
return 'offline';
} else {
return 'unknown';
}
}
public function render()
{
return view('livewire.dashboard');
}
}

View File

@ -0,0 +1,418 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use Livewire\WithPagination;
use App\Models\Device;
use App\Models\DeviceGroup;
use App\Models\Driver;
use App\Services\TraccarService;
use Illuminate\Support\Facades\Auth;
class DeviceManagement extends Component
{
use WithPagination;
public $search = '';
public $statusFilter = 'all';
public $groupFilter = 'all';
public $showForm = false;
public $editingDevice = null;
// Form fields
public $name = '';
public $unique_id = '';
public $imei = '';
public $phone = '';
public $model = '';
public $contact = '';
public $category = 'default';
public $protocol = '';
public $group_id = '';
public $driver_id = '';
public $is_active = true;
public $deviceAttributes = '';
protected $traccarService;
public function mount()
{
// Initialize TraccarService
$this->traccarService = app(TraccarService::class);
}
public function boot(TraccarService $traccarService)
{
$this->traccarService = $traccarService;
}
protected function getTraccarService()
{
if (!$this->traccarService) {
$this->traccarService = app(TraccarService::class);
}
return $this->traccarService;
}
protected $rules = [
'name' => 'required|string|max:255',
'unique_id' => 'required|string|max:255|unique:devices,unique_id',
'imei' => 'nullable|string|max:255',
'phone' => 'nullable|string|max:255',
'model' => 'nullable|string|max:255',
'contact' => 'nullable|string|max:255',
'category' => 'required|string|max:255',
'protocol' => 'nullable|string|max:255',
'group_id' => 'nullable|exists:device_groups,id',
'driver_id' => 'nullable|exists:drivers,id',
'is_active' => 'boolean',
'deviceAttributes' => 'nullable|json',
];
public function updatingSearch()
{
$this->resetPage();
}
public function updatingStatusFilter()
{
$this->resetPage();
}
public function updatingGroupFilter()
{
$this->resetPage();
}
public function createDevice()
{
$this->openCreateModal();
}
public function openCreateModal()
{
$this->reset(['name', 'unique_id', 'imei', 'phone', 'model', 'contact', 'category', 'group_id', 'driver_id', 'is_active', 'deviceAttributes']);
$this->editingDevice = false;
$this->showForm = true;
}
public function editDevice($deviceId)
{
$device = Device::findOrFail($deviceId);
if ($device->user_id !== Auth::id()) {
abort(403);
}
$this->editingDevice = $device;
$this->name = $device->name;
$this->unique_id = $device->unique_id;
$this->imei = $device->imei;
$this->phone = $device->phone;
$this->model = $device->model;
$this->contact = $device->contact;
$this->category = $device->category;
$this->protocol = $device->protocol;
$this->group_id = $device->group_id;
$this->driver_id = $device->driver_id;
$this->is_active = $device->is_active;
$this->deviceAttributes = $device->attributes ? json_encode($device->attributes) : '';
$this->showForm = true;
}
public function saveDevice()
{
if ($this->editingDevice) {
$this->rules['unique_id'] = 'required|string|max:255|unique:devices,unique_id,' . $this->editingDevice->id;
}
$this->validate();
try {
// Prepare attributes for Traccar (must be an object, not null)
$attributes = [];
if ($this->deviceAttributes) {
$decoded = json_decode($this->deviceAttributes, true);
if (is_array($decoded)) {
$attributes = $decoded;
}
}
$deviceData = [
'name' => $this->name,
'uniqueId' => $this->unique_id,
'phone' => $this->phone ?: null,
'model' => $this->model ?: null,
'contact' => $this->contact ?: null,
'category' => $this->category ?: 'default',
'attributes' => (object)$attributes, // Always send as object for Traccar
];
if ($this->editingDevice) {
// Update existing device
try {
if ($this->editingDevice->traccar_device_id) {
$this->getTraccarService()->updateDevice($this->editingDevice->traccar_device_id, $deviceData);
}
} catch (\Exception $e) {
// Log Traccar error but continue with local update
\Log::warning('Failed to update device in Traccar: ' . $e->getMessage());
}
$this->editingDevice->update([
'name' => $this->name,
'unique_id' => $this->unique_id,
'imei' => $this->imei,
'phone' => $this->phone,
'model' => $this->model,
'contact' => $this->contact,
'category' => $this->category,
'protocol' => $this->protocol,
'group_id' => $this->group_id ?: null,
'driver_id' => $this->driver_id ?: null,
'is_active' => $this->is_active,
'attributes' => $this->deviceAttributes ? json_decode($this->deviceAttributes, true) : null,
]);
session()->flash('message', 'Device updated successfully' . ($this->editingDevice->traccar_device_id ? ' and synced with Traccar' : ''));
$this->dispatch('device-updated');
} else {
// Create new device - try Traccar first, then local
$traccarDeviceId = null;
$traccarSyncMessage = '';
try {
$traccarDevice = $this->getTraccarService()->createDevice($deviceData);
if (isset($traccarDevice['id']) && !empty($traccarDevice['id'])) {
$traccarDeviceId = $traccarDevice['id'];
$traccarSyncMessage = ' and automatically synced with Traccar';
} else {
\Log::info('Traccar device creation returned empty response', ['response' => $traccarDevice]);
$traccarSyncMessage = '. Traccar sync is available but returned empty response';
}
} catch (\Exception $e) {
// Log Traccar error but continue with local creation
\Log::warning('Failed to create device in Traccar: ' . $e->getMessage());
$traccarSyncMessage = '. Traccar sync failed - device can be synced manually later';
}
// Create device in local database
$device = Device::create([
'user_id' => Auth::id(),
'traccar_device_id' => $traccarDeviceId,
'name' => $this->name,
'unique_id' => $this->unique_id,
'imei' => $this->imei,
'phone' => $this->phone,
'model' => $this->model,
'contact' => $this->contact,
'category' => $this->category,
'protocol' => $this->protocol,
'status' => 'unknown',
'group_id' => $this->group_id ?: null,
'driver_id' => $this->driver_id ?: null,
'is_active' => $this->is_active,
'attributes' => $this->deviceAttributes ? json_decode($this->deviceAttributes, true) : null,
]);
$message = 'Device "' . $this->name . '" created successfully' . $traccarSyncMessage;
session()->flash('message', $message);
$this->dispatch('device-created');
}
$this->resetForm();
$this->showForm = false;
} catch (\Exception $e) {
$this->dispatch('device-error', ['message' => 'Failed to save device: ' . $e->getMessage()]);
}
}
public function deleteDevice($deviceId)
{
$device = Device::findOrFail($deviceId);
if ($device->user_id !== Auth::id()) {
abort(403);
}
try {
// Delete from Traccar if it exists
if ($device->traccar_device_id) {
try {
$this->getTraccarService()->deleteDevice($device->traccar_device_id);
} catch (\Exception $e) {
// Log Traccar error but continue with local deletion
\Log::warning('Failed to delete device from Traccar: ' . $e->getMessage());
}
}
$device->delete();
session()->flash('message', 'Device deleted successfully' . ($device->traccar_device_id ? ' and removed from Traccar' : ''));
$this->dispatch('device-deleted');
} catch (\Exception $e) {
session()->flash('error', 'Failed to delete device: ' . $e->getMessage());
}
}
public function syncWithTraccar()
{
try {
$traccarDevices = $this->getTraccarService()->getDevices();
$user = Auth::user();
$syncedCount = 0;
foreach ($traccarDevices as $traccarDevice) {
Device::updateOrCreate(
['traccar_device_id' => $traccarDevice['id']],
[
'user_id' => $user->id,
'name' => $traccarDevice['name'],
'unique_id' => $traccarDevice['uniqueId'],
'phone' => $traccarDevice['phone'] ?? null,
'model' => $traccarDevice['model'] ?? null,
'contact' => $traccarDevice['contact'] ?? null,
'category' => $traccarDevice['category'] ?? 'default',
'attributes' => $traccarDevice['attributes'] ?? null,
]
);
$syncedCount++;
}
session()->flash('message', "Successfully synced {$syncedCount} devices from Traccar");
$this->dispatch('devices-synced');
} catch (\Exception $e) {
session()->flash('error', 'Failed to sync with Traccar: ' . $e->getMessage());
}
}
public function syncUnsyncedDevicesToTraccar()
{
try {
$unsyncedDevices = Device::whereNull('traccar_device_id')->get();
$syncedCount = 0;
$errorCount = 0;
foreach ($unsyncedDevices as $device) {
try {
// Prepare attributes for Traccar
$attributes = [];
if ($device->attributes) {
if (is_string($device->attributes)) {
$decoded = json_decode($device->attributes, true);
if (is_array($decoded)) {
$attributes = $decoded;
}
} elseif (is_array($device->attributes)) {
$attributes = $device->attributes;
}
}
$deviceData = [
'name' => $device->name,
'uniqueId' => $device->unique_id,
'phone' => $device->phone ?: null,
'model' => $device->model ?: null,
'contact' => $device->contact ?: null,
'category' => $device->category ?: 'default',
'attributes' => (object)$attributes,
];
$traccarDevice = $this->getTraccarService()->createDevice($deviceData);
if (isset($traccarDevice['id']) && !empty($traccarDevice['id'])) {
$device->update(['traccar_device_id' => $traccarDevice['id']]);
$syncedCount++;
} else {
$errorCount++;
}
} catch (\Exception $e) {
\Log::warning("Failed to sync device {$device->name} to Traccar: " . $e->getMessage());
$errorCount++;
}
}
if ($syncedCount > 0) {
session()->flash('message', "Successfully synced {$syncedCount} unsynced devices to Traccar" . ($errorCount > 0 ? ". {$errorCount} devices failed to sync." : ""));
} else {
session()->flash('error', "No devices were synced. " . ($errorCount > 0 ? "{$errorCount} devices failed." : "All devices are already synced."));
}
$this->dispatch('devices-synced');
} catch (\Exception $e) {
session()->flash('error', 'Failed to sync unsynced devices: ' . $e->getMessage());
}
}
public function cancelForm()
{
$this->resetForm();
$this->showForm = false;
}
private function resetForm()
{
$this->editingDevice = null;
$this->name = '';
$this->unique_id = '';
$this->imei = '';
$this->phone = '';
$this->model = '';
$this->contact = '';
$this->category = 'default';
$this->protocol = '';
$this->group_id = '';
$this->driver_id = '';
$this->is_active = true;
$this->deviceAttributes = '';
$this->resetValidation();
}
public function render()
{
$query = Device::where('user_id', Auth::id())
->with(['group', 'driver', 'currentPosition']);
// Apply filters
if ($this->search) {
$query->where(function ($q) {
$q->where('name', 'like', '%' . $this->search . '%')
->orWhere('unique_id', 'like', '%' . $this->search . '%')
->orWhere('imei', 'like', '%' . $this->search . '%');
});
}
if ($this->statusFilter !== 'all') {
$query->where('status', $this->statusFilter);
}
if ($this->groupFilter !== 'all') {
$query->where('group_id', $this->groupFilter);
}
$devices = $query->orderBy('name')->paginate(10);
$deviceGroups = DeviceGroup::active()->get();
$drivers = Driver::active()->get();
return view('livewire.device-management', [
'devices' => $devices,
'deviceGroups' => $deviceGroups,
'drivers' => $drivers,
]);
}
public function getStatsProperty()
{
$userId = Auth::id();
return [
'total_devices' => Device::where('user_id', $userId)->count(),
'active_devices' => Device::where('user_id', $userId)->where('is_active', true)->count(),
'online_devices' => Device::where('user_id', $userId)->where('status', 'online')->count(),
'with_traccar' => Device::where('user_id', $userId)->whereNotNull('traccar_device_id')->count(),
];
}
}

View File

@ -0,0 +1,289 @@
<?php
namespace App\Livewire;
use App\Models\Driver;
use Livewire\Component;
use Livewire\WithPagination;
class DriverManagement extends Component
{
use WithPagination;
public $filters = [
'search' => '',
'status' => '',
'license_status' => '',
'vehicle_assigned' => '',
];
public $showModal = false;
public $showPerformanceModal = false;
public $editingDriver = null;
public $selectedDriver = null;
public $performancePeriod = 30;
public $driverMetrics = [];
public $form = [
'name' => '',
'driver_id' => '',
'user_id' => '',
'phone' => '',
'email' => '',
'license_number' => '',
'license_type' => '',
'license_expiry_date' => '',
'assigned_vehicle' => '',
'vehicle_plate' => '',
'performance_score' => '',
'status' => 'active',
'is_active' => true,
'notes' => '',
];
public function mount()
{
// Initialize component without parameters
}
public function getStatsProperty()
{
return [
'total_drivers' => Driver::count(),
'active_drivers' => Driver::where('status', 'active')->count(),
'expiring_licenses' => Driver::where('license_expiry_date', '<=', now()->addDays(30))->count(),
'assigned_vehicles' => Driver::whereNotNull('assigned_vehicle')->count(),
];
}
public function render()
{
$drivers = Driver::query()
->with('user')
->when($this->filters['search'], function ($query) {
$query->where(function ($q) {
$q->where('name', 'like', '%' . $this->filters['search'] . '%')
->orWhere('driver_id', 'like', '%' . $this->filters['search'] . '%')
->orWhere('phone', 'like', '%' . $this->filters['search'] . '%');
});
})
->when($this->filters['status'], function ($query) {
$query->where('status', $this->filters['status']);
})
->when($this->filters['license_status'], function ($query) {
if ($this->filters['license_status'] === 'valid') {
$query->where('license_expiry_date', '>', now());
} elseif ($this->filters['license_status'] === 'expiring') {
$query->whereBetween('license_expiry_date', [now(), now()->addDays(30)]);
} elseif ($this->filters['license_status'] === 'expired') {
$query->where('license_expiry_date', '<', now());
}
})
->when($this->filters['vehicle_assigned'], function ($query) {
if ($this->filters['vehicle_assigned'] === 'yes') {
$query->whereNotNull('assigned_vehicle');
} elseif ($this->filters['vehicle_assigned'] === 'no') {
$query->whereNull('assigned_vehicle');
}
})
->paginate(15);
$users = \App\Models\User::select('id', 'name', 'email')
->whereDoesntHave('driver')
->orWhere('id', $this->editingDriver?->user_id)
->orderBy('name')
->get();
return view('livewire.driver-management', [
'drivers' => $drivers,
'users' => $users,
]);
}
public function editDriver($driverId)
{
$this->editingDriver = Driver::find($driverId);
$this->form = [
'name' => $this->editingDriver->name,
'driver_id' => $this->editingDriver->driver_id,
'user_id' => $this->editingDriver->user_id,
'phone' => $this->editingDriver->phone,
'email' => $this->editingDriver->email,
'license_number' => $this->editingDriver->license_number,
'license_type' => $this->editingDriver->license_type,
'license_expiry_date' => $this->editingDriver->license_expiry_date?->format('Y-m-d'),
'assigned_vehicle' => $this->editingDriver->assigned_vehicle,
'vehicle_plate' => $this->editingDriver->vehicle_plate,
'performance_score' => $this->editingDriver->performance_score,
'status' => $this->editingDriver->status,
'is_active' => $this->editingDriver->is_active,
'notes' => $this->editingDriver->notes,
];
$this->showModal = true;
}
public function openCreateModal()
{
$this->resetForm();
$this->editingDriver = null;
$this->showModal = true;
}
private function resetForm()
{
$this->form = [
'name' => '',
'driver_id' => '',
'user_id' => '',
'phone' => '',
'email' => '',
'license_number' => '',
'license_type' => '',
'license_expiry_date' => '',
'assigned_vehicle' => '',
'vehicle_plate' => '',
'performance_score' => '',
'status' => 'active',
'is_active' => true,
'notes' => '',
];
}
public function createDriver()
{
$this->validate([
'form.name' => 'required|string|max:255',
'form.driver_id' => 'required|string|unique:drivers,driver_id',
'form.user_id' => 'nullable|exists:users,id|unique:drivers,user_id',
'form.phone' => 'nullable|string|max:20',
'form.email' => 'nullable|email|unique:drivers,email',
'form.license_number' => 'required|string|max:50',
'form.license_type' => 'nullable|string|max:50',
'form.license_expiry_date' => 'nullable|date|after:today',
'form.assigned_vehicle' => 'nullable|string|max:255',
'form.vehicle_plate' => 'nullable|string|max:20',
'form.performance_score' => 'nullable|integer|min:0|max:100',
'form.status' => 'required|in:active,inactive,suspended',
'form.is_active' => 'boolean',
'form.notes' => 'nullable|string',
]);
Driver::create([
'name' => $this->form['name'],
'driver_id' => $this->form['driver_id'],
'user_id' => $this->form['user_id'] ?: null,
'phone' => $this->form['phone'],
'email' => $this->form['email'],
'license_number' => $this->form['license_number'],
'license_type' => $this->form['license_type'],
'license_expiry_date' => $this->form['license_expiry_date'] ? \Carbon\Carbon::parse($this->form['license_expiry_date']) : null,
'assigned_vehicle' => $this->form['assigned_vehicle'],
'vehicle_plate' => $this->form['vehicle_plate'],
'performance_score' => $this->form['performance_score'] ?: null,
'status' => $this->form['status'],
'is_active' => $this->form['is_active'],
'notes' => $this->form['notes'],
]);
$this->closeModal();
session()->flash('message', 'Driver created successfully.');
}
public function updateDriver()
{
$this->validate([
'form.name' => 'required|string|max:255',
'form.driver_id' => 'required|string|unique:drivers,driver_id,' . $this->editingDriver->id,
'form.user_id' => 'nullable|exists:users,id|unique:drivers,user_id,' . $this->editingDriver->id,
'form.phone' => 'nullable|string|max:20',
'form.email' => 'nullable|email|unique:drivers,email,' . $this->editingDriver->id,
'form.license_number' => 'required|string|max:50',
'form.license_type' => 'nullable|string|max:50',
'form.license_expiry_date' => 'nullable|date',
'form.assigned_vehicle' => 'nullable|string|max:255',
'form.vehicle_plate' => 'nullable|string|max:20',
'form.performance_score' => 'nullable|integer|min:0|max:100',
'form.status' => 'required|in:active,inactive,suspended',
'form.is_active' => 'boolean',
'form.notes' => 'nullable|string',
]);
$this->editingDriver->update([
'name' => $this->form['name'],
'driver_id' => $this->form['driver_id'],
'user_id' => $this->form['user_id'] ?: null,
'phone' => $this->form['phone'],
'email' => $this->form['email'],
'license_number' => $this->form['license_number'],
'license_type' => $this->form['license_type'],
'license_expiry_date' => $this->form['license_expiry_date'] ? \Carbon\Carbon::parse($this->form['license_expiry_date']) : null,
'assigned_vehicle' => $this->form['assigned_vehicle'],
'vehicle_plate' => $this->form['vehicle_plate'],
'performance_score' => $this->form['performance_score'] ?: null,
'status' => $this->form['status'],
'is_active' => $this->form['is_active'],
'notes' => $this->form['notes'],
]);
$this->closeModal();
session()->flash('message', 'Driver updated successfully.');
}
public function deleteDriver($driverId)
{
$driver = Driver::find($driverId);
if ($driver) {
// Check if driver has any active assignments
if ($driver->assigned_vehicle) {
session()->flash('error', 'Cannot delete driver with active vehicle assignment. Please unassign vehicle first.');
return;
}
$driver->delete();
session()->flash('message', 'Driver deleted successfully.');
}
}
public function suspendDriver($driverId)
{
Driver::find($driverId)->update(['status' => 'suspended']);
session()->flash('message', 'Driver suspended successfully.');
}
public function activateDriver($driverId)
{
Driver::find($driverId)->update(['status' => 'active']);
session()->flash('message', 'Driver activated successfully.');
}
public function viewPerformance($driverId)
{
$this->selectedDriver = Driver::find($driverId);
$this->driverMetrics = [
'total_trips' => rand(50, 200),
'avg_speed' => rand(45, 65),
'violations' => rand(0, 5),
'fuel_efficiency' => rand(15, 25),
];
$this->showPerformanceModal = true;
}
public function closeModal()
{
$this->showModal = false;
$this->editingDriver = null;
$this->reset('form');
}
public function closePerformanceModal()
{
$this->showPerformanceModal = false;
$this->selectedDriver = null;
$this->driverMetrics = [];
}
public function updatingFilters()
{
$this->resetPage();
}
}

View File

@ -0,0 +1,416 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use Livewire\WithPagination;
use App\Models\Event;
use App\Models\Device;
use App\Models\Geofence;
use App\Models\NotificationPreference;
use App\Services\TraccarService;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
class EventsAndAlerts extends Component
{
use WithPagination;
public $search = '';
public $deviceFilter = 'all';
public $eventTypeFilter = 'all';
public $dateRange = '7'; // days
public $selectedEvent = null;
public $showNotificationSettings = false;
// Filter properties
public $filterType = '';
public $filterDevice = '';
public $filterDateFrom = '';
public $filterDateTo = '';
public $filterStatus = '';
public $filterAcknowledged = '';
// Notification settings
public $emailNotifications = true;
public $pushNotifications = true;
public $smsNotifications = false;
public $notificationTypes = [];
protected $traccarService;
public function boot(TraccarService $traccarService)
{
$this->traccarService = $traccarService;
}
public function mount()
{
$this->loadNotificationSettings();
}
public function loadNotificationSettings()
{
$user = Auth::user();
$preferences = NotificationPreference::where('user_id', $user->id)->first();
if ($preferences) {
$this->emailNotifications = $preferences->email_enabled;
$this->pushNotifications = $preferences->push_enabled;
$this->smsNotifications = $preferences->sms_enabled;
$this->notificationTypes = $preferences->event_types ?? [];
}
}
public function saveNotificationSettings()
{
try {
$user = Auth::user();
NotificationPreference::updateOrCreate(
['user_id' => $user->id],
[
'email_enabled' => $this->emailNotifications,
'push_enabled' => $this->pushNotifications,
'sms_enabled' => $this->smsNotifications,
'event_types' => $this->notificationTypes,
]
);
$this->showNotificationSettings = false;
session()->flash('success', 'Notification settings saved successfully!');
} catch (\Exception $e) {
session()->flash('error', 'Failed to save notification settings: ' . $e->getMessage());
}
}
public function viewEventDetails($eventId)
{
$this->selectedEvent = Event::with(['device', 'geofence'])->findOrFail($eventId);
}
public function closeEventDetails()
{
$this->selectedEvent = null;
}
public function markAsRead($eventId)
{
try {
$event = Event::findOrFail($eventId);
$event->update(['acknowledged' => true, 'acknowledged_at' => now()]);
session()->flash('success', 'Event marked as read');
} catch (\Exception $e) {
session()->flash('error', 'Failed to mark event as read: ' . $e->getMessage());
}
}
public function syncEvents()
{
try {
// Get events from Traccar
$traccarEvents = $this->traccarService->getEvents();
if ($traccarEvents) {
foreach ($traccarEvents as $traccarEvent) {
$device = Device::where('traccar_id', $traccarEvent['deviceId'])->first();
$geofence = null;
if (isset($traccarEvent['geofenceId'])) {
$geofence = Geofence::where('traccar_id', $traccarEvent['geofenceId'])->first();
}
if ($device) {
Event::updateOrCreate(
['traccar_id' => $traccarEvent['id']],
[
'device_id' => $device->id,
'geofence_id' => $geofence?->id,
'event_type' => $traccarEvent['type'],
'event_time' => Carbon::parse($traccarEvent['eventTime']),
'position_id' => null, // Would need to match position
'attributes' => $traccarEvent['attributes'] ?? null,
'acknowledged' => false,
]
);
}
}
}
session()->flash('success', 'Events synchronized successfully!');
} catch (\Exception $e) {
session()->flash('error', 'Failed to sync events: ' . $e->getMessage());
}
}
public function getEventIcon($eventType)
{
return match ($eventType) {
'deviceOnline' => 'signal',
'deviceOffline' => 'signal-slash',
'deviceOverspeed' => 'exclamation-triangle',
'deviceFuelDrop' => 'fire',
'geofenceEnter' => 'map-pin',
'geofenceExit' => 'map-pin',
'alarm' => 'bell',
'ignitionOn' => 'key',
'ignitionOff' => 'key',
'maintenance' => 'wrench',
'textMessage' => 'chat-bubble-left',
'driverChanged' => 'user',
default => 'information-circle'
};
}
public function getEventColor($eventType)
{
return match ($eventType) {
'deviceOnline' => 'text-green-600 dark:text-green-400',
'deviceOffline' => 'text-red-600 dark:text-red-400',
'deviceOverspeed' => 'text-orange-600 dark:text-orange-400',
'deviceFuelDrop' => 'text-red-600 dark:text-red-400',
'geofenceEnter' => 'text-blue-600 dark:text-blue-400',
'geofenceExit' => 'text-purple-600 dark:text-purple-400',
'alarm' => 'text-red-600 dark:text-red-400',
'ignitionOn' => 'text-green-600 dark:text-green-400',
'ignitionOff' => 'text-gray-600 dark:text-gray-400',
'maintenance' => 'text-yellow-600 dark:text-yellow-400',
'textMessage' => 'text-blue-600 dark:text-blue-400',
'driverChanged' => 'text-indigo-600 dark:text-indigo-400',
default => 'text-gray-600 dark:text-gray-400'
};
}
public function refreshEvents()
{
$this->resetPage();
session()->flash('success', 'Events refreshed!');
}
public function markAllAsRead()
{
try {
Event::whereHas('device', function($q) {
$q->where('user_id', Auth::id());
})
->where('acknowledged', false)
->update(['acknowledged' => true, 'acknowledged_at' => now()]);
session()->flash('success', 'All events marked as read');
} catch (\Exception $e) {
session()->flash('error', 'Failed to mark all events as read: ' . $e->getMessage());
}
}
public function acknowledgeEvent($eventId)
{
try {
$event = Event::findOrFail($eventId);
$event->update([
'acknowledged' => true,
'acknowledged_at' => now(),
'acknowledged_by' => Auth::id()
]);
session()->flash('success', 'Event acknowledged');
} catch (\Exception $e) {
session()->flash('error', 'Failed to acknowledge event: ' . $e->getMessage());
}
}
public function showEventDetails($eventId)
{
$this->selectedEvent = Event::with(['device', 'geofence', 'position'])->findOrFail($eventId);
}
public function showOnMap($latitude, $longitude)
{
// Emit event to show location on map
$this->dispatch('showLocationOnMap', ['lat' => $latitude, 'lng' => $longitude]);
session()->flash('info', "Location: {$latitude}, {$longitude}");
}
public function clearFilters()
{
$this->filterType = '';
$this->filterDevice = '';
$this->filterDateFrom = '';
$this->filterDateTo = '';
$this->filterStatus = '';
$this->filterAcknowledged = '';
$this->resetPage();
}
public function hasActiveFilters()
{
return !empty($this->filterType) ||
!empty($this->filterDevice) ||
!empty($this->filterDateFrom) ||
!empty($this->filterDateTo) ||
!empty($this->filterStatus) ||
!empty($this->filterAcknowledged);
}
// Computed properties for stats
public function getCriticalCountProperty()
{
$criticalTypes = ['alarm', 'deviceOffline', 'deviceOverspeed', 'panic', 'accident', 'deviceFuelDrop'];
return Event::whereHas('device', function($q) {
$q->where('user_id', Auth::id());
})
->whereIn('type', $criticalTypes)
->where('event_time', '>=', now()->subDays(30))
->count();
}
public function getWarningCountProperty()
{
$warningTypes = ['maintenance', 'batteryLow', 'deviceMoving', 'deviceStopped', 'geofenceExit', 'ignitionOff'];
return Event::whereHas('device', function($q) {
$q->where('user_id', Auth::id());
})
->whereIn('type', $warningTypes)
->where('event_time', '>=', now()->subDays(30))
->count();
}
public function getInfoCountProperty()
{
$infoTypes = ['deviceOnline', 'geofenceEnter', 'ignitionOn', 'driverChanged', 'textMessage'];
return Event::whereHas('device', function($q) {
$q->where('user_id', Auth::id());
})
->whereIn('type', $infoTypes)
->where('event_time', '>=', now()->subDays(30))
->count();
}
public function getTotalCountProperty()
{
return Event::whereHas('device', function($q) {
$q->where('user_id', Auth::id());
})
->where('event_time', '>=', now()->subDays(30))
->count();
}
public function getEventSeverity($eventType)
{
$criticalTypes = ['alarm', 'deviceOffline', 'deviceOverspeed', 'panic', 'accident', 'deviceFuelDrop'];
$warningTypes = ['maintenance', 'batteryLow', 'deviceMoving', 'deviceStopped', 'geofenceExit', 'ignitionOff'];
if (in_array($eventType, $criticalTypes)) {
return 'critical';
} elseif (in_array($eventType, $warningTypes)) {
return 'warning';
} else {
return 'info';
}
}
public function render()
{
// Build query with filters
$query = Event::with(['device', 'geofence', 'position'])
->whereHas('device', function($q) {
$q->where('user_id', Auth::id());
});
// Apply new filters
if ($this->filterType) {
$query->where('type', $this->filterType);
}
if ($this->filterDevice) {
$query->where('device_id', $this->filterDevice);
}
if ($this->filterDateFrom) {
$query->where('event_time', '>=', $this->filterDateFrom);
}
if ($this->filterDateTo) {
$query->where('event_time', '<=', $this->filterDateTo);
}
if ($this->filterStatus) {
if ($this->filterStatus === 'unread') {
$query->where('acknowledged', false);
} elseif ($this->filterStatus === 'read') {
$query->where('acknowledged', true);
}
}
if ($this->filterAcknowledged) {
if ($this->filterAcknowledged === 'yes') {
$query->where('acknowledged', true);
} elseif ($this->filterAcknowledged === 'no') {
$query->where('acknowledged', false);
}
}
// Apply legacy search filter
if ($this->search) {
$query->where(function($q) {
$q->whereHas('device', function($deviceQuery) {
$deviceQuery->where('name', 'like', '%' . $this->search . '%');
})->orWhere('type', 'like', '%' . $this->search . '%');
});
}
// Apply legacy filters for backward compatibility
if ($this->deviceFilter !== 'all') {
$query->where('device_id', $this->deviceFilter);
}
if ($this->eventTypeFilter !== 'all') {
$query->where('type', $this->eventTypeFilter);
}
if ($this->dateRange !== 'all') {
$query->where('event_time', '>=', now()->subDays((int)$this->dateRange));
}
$events = $query->orderBy('event_time', 'desc')->paginate(20);
$deviceQuery = Device::where('user_id', Auth::id());
// Check if device groups have users relationship (safely handle missing relationship)
if (method_exists(\App\Models\DeviceGroup::class, 'users')) {
$deviceQuery->orWhereHas('group.users', function($subQuery) {
$subQuery->where('user_id', Auth::id());
});
}
$devices = $deviceQuery->get();
$eventTypes = Event::whereHas('device', function($q) {
$q->where('user_id', Auth::id());
})
->distinct()
->pluck('type')
->sort();
$unreadCount = Event::whereHas('device', function($q) {
$q->where('user_id', Auth::id());
})
->where('acknowledged', false)
->count();
return view('livewire.events-and-alerts', [
'events' => $events,
'devices' => $devices,
'eventTypes' => $eventTypes,
'unreadCount' => $unreadCount
]);
}
}

View File

@ -0,0 +1,255 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use Livewire\WithPagination;
use App\Models\Geofence;
use App\Models\Device;
use App\Services\TraccarService;
use Illuminate\Support\Facades\Auth;
class GeofenceManagement extends Component
{
use WithPagination;
public $search = '';
public $showModal = false;
public $editingGeofence = null;
public $selectedGeofence = null;
// Form fields
public $geofenceName = '';
public $geofenceDescription = '';
public $geofenceType = 'entry';
public $geofenceActive = true;
public $selectedDevicesForGeofence = [];
public $tempCoordinates = null;
// Map settings
public $mapCenter = ['lat' => 40.7128, 'lng' => -74.0060];
public $zoomLevel = 13;
protected $traccarService;
public function boot(TraccarService $traccarService)
{
$this->traccarService = $traccarService;
}
protected $rules = [
'geofenceName' => 'required|string|max:255',
'geofenceDescription' => 'nullable|string|max:1000',
'geofenceType' => 'required|string|in:entry,exit,both,zone',
'selectedDevicesForGeofence' => 'array',
'selectedDevicesForGeofence.*' => 'exists:devices,id',
];
public function mount()
{
// Set default map center based on user's devices
$userDevices = Device::where('user_id', Auth::id())->get();
if ($userDevices->isNotEmpty()) {
$avgLat = $userDevices->avg('last_position_lat') ?: 40.7128;
$avgLng = $userDevices->avg('last_position_lng') ?: -74.0060;
$this->mapCenter = ['lat' => $avgLat, 'lng' => $avgLng];
}
}
public function showGeofenceModal()
{
$this->resetForm();
$this->showModal = true;
}
public function closeModal()
{
$this->showModal = false;
$this->resetForm();
$this->dispatch('clearDrawnShape');
}
public function selectGeofence($geofenceId)
{
$this->selectedGeofence = $geofenceId;
}
public function editGeofence($geofenceId)
{
$geofence = Geofence::findOrFail($geofenceId);
$this->editingGeofence = $geofence;
$this->geofenceName = $geofence->name;
$this->geofenceDescription = $geofence->description;
$this->geofenceType = $geofence->type ?? 'entry';
$this->geofenceActive = $geofence->is_active;
$this->selectedDevicesForGeofence = $geofence->devices->pluck('id')->toArray();
$this->showModal = true;
}
public function saveGeofence()
{
$this->validate();
if (!$this->tempCoordinates && !$this->editingGeofence) {
session()->flash('error', 'Please draw a geofence area on the map first.');
return;
}
try {
$geofenceData = [
'name' => $this->geofenceName,
'description' => $this->geofenceDescription,
'type' => $this->geofenceType,
'is_active' => $this->geofenceActive,
'coordinates' => $this->tempCoordinates ? json_encode($this->tempCoordinates) : null,
];
if ($this->editingGeofence) {
// Update existing geofence
$this->editingGeofence->update($geofenceData);
$geofence = $this->editingGeofence;
// Update in Traccar if it has traccar_id
if ($geofence->traccar_id) {
$traccarData = [
'id' => $geofence->traccar_id,
'name' => $this->geofenceName,
'description' => $this->geofenceDescription,
'area' => $this->tempCoordinates ? $this->convertCoordinatesToTraccarFormat($this->tempCoordinates) : $geofence->area,
];
$this->traccarService->updateGeofence($geofence->traccar_id, $traccarData);
}
} else {
// Create new geofence
$geofenceData['user_id'] = Auth::id();
// Create in Traccar first
$traccarData = [
'name' => $this->geofenceName,
'description' => $this->geofenceDescription,
'area' => $this->convertCoordinatesToTraccarFormat($this->tempCoordinates),
];
$traccarGeofence = $this->traccarService->createGeofence($traccarData);
if ($traccarGeofence && isset($traccarGeofence['id'])) {
$geofenceData['traccar_id'] = $traccarGeofence['id'];
}
$geofence = Geofence::create($geofenceData);
}
// Sync device associations
$geofence->devices()->sync($this->selectedDevicesForGeofence);
// Sync device-geofence associations in Traccar
foreach ($this->selectedDevicesForGeofence as $deviceId) {
$device = Device::find($deviceId);
if ($device && $device->traccar_id && $geofence->traccar_id) {
$this->traccarService->linkGeofenceToDevice($geofence->traccar_id, $device->traccar_id);
}
}
$this->closeModal();
$this->dispatch('geofencesUpdated');
session()->flash('success', $this->editingGeofence ? 'Geofence updated successfully!' : 'Geofence created successfully!');
} catch (\Exception $e) {
session()->flash('error', 'Failed to save geofence: ' . $e->getMessage());
}
}
private function convertCoordinatesToTraccarFormat($coordinates)
{
if (!$coordinates) return '';
if ($coordinates['type'] === 'circle') {
// For circles, create a polygon approximation
$center = $coordinates['center'];
$radius = $coordinates['radius'];
$points = [];
for ($i = 0; $i < 16; $i++) {
$angle = ($i * 360 / 16) * (M_PI / 180);
$lat = $center[0] + ($radius / 111000) * cos($angle);
$lng = $center[1] + ($radius / (111000 * cos($center[0] * M_PI / 180))) * sin($angle);
$points[] = "$lat $lng";
}
return "POLYGON((" . implode(', ', $points) . "))";
} else {
// For polygons
$points = array_map(function($coord) {
return $coord[0] . ' ' . $coord[1];
}, $coordinates['coordinates']);
return "POLYGON((" . implode(', ', $points) . "))";
}
}
public function deleteGeofence($geofenceId)
{
try {
$geofence = Geofence::findOrFail($geofenceId);
// Delete from Traccar if it exists
if ($geofence->traccar_id) {
$this->traccarService->deleteGeofence($geofence->traccar_id);
}
$geofence->delete();
$this->dispatch('geofencesUpdated');
session()->flash('success', 'Geofence deleted successfully!');
} catch (\Exception $e) {
session()->flash('error', 'Failed to delete geofence: ' . $e->getMessage());
}
}
public function refreshGeofences()
{
$this->dispatch('geofencesUpdated');
session()->flash('success', 'Geofences refreshed!');
}
private function resetForm()
{
$this->editingGeofence = null;
$this->geofenceName = '';
$this->geofenceDescription = '';
$this->geofenceType = 'entry';
$this->geofenceActive = true;
$this->selectedDevicesForGeofence = [];
$this->tempCoordinates = null;
}
public function render()
{
$geofences = Geofence::where('user_id', Auth::id())
->when($this->search, function($query) {
$query->where('name', 'like', '%' . $this->search . '%')
->orWhere('description', 'like', '%' . $this->search . '%');
})
->with('devices')
->orderBy('created_at', 'desc')
->get();
$query = Device::where('user_id', Auth::id());
// Check if device groups have users relationship (safely handle missing relationship)
if (method_exists(\App\Models\DeviceGroup::class, 'users')) {
$query->orWhereHas('group.users', function($subQuery) {
$subQuery->where('user_id', Auth::id());
});
}
$devices = $query->get();
return view('livewire.geofence-management', [
'geofences' => $geofences,
'devices' => $devices
]);
}
}

View File

@ -0,0 +1,405 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use App\Models\Device;
use App\Models\Position;
use App\Services\TraccarService;
use App\Services\MapService;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;
class LiveTracking extends Component
{
public $selectedDevices = [];
public $allDevicesSelected = true;
public $mapCenter = ['lat' => 40.7128, 'lng' => -74.0060]; // Default to NYC
public $zoomLevel = 10;
public $refreshInterval = 15; // seconds - automatic background refresh
public $showTrails = false;
public $trailDuration = 24; // hours
public $selectedDevice = null;
public $deviceDetails = [];
public $mapProvider = 'openstreetmap';
public $mapStyle = 'standard';
public $followDevice = null;
public $showOfflineDevices = true;
public $lastUpdate = null;
public $sidebarCollapsed = false;
public $availableProviders = [];
public $availableStyles = [];
public function mount()
{
$mapService = app(MapService::class);
$this->mapProvider = $mapService->getDefaultProvider();
$this->availableProviders = $mapService->getAvailableProviders();
$this->availableStyles = $mapService->getMapStyles($this->mapProvider);
$this->loadUserDevices();
$this->loadRealTimePositions();
$this->lastUpdate = now()->toTimeString();
}
public function loadUserDevices()
{
$devices = Device::where('user_id', Auth::id())
->whereNotNull('traccar_device_id')
->get();
$this->selectedDevices = $devices->pluck('id')->toArray();
$this->allDevicesSelected = true;
}
public function loadRealTimePositions()
{
try {
// Get devices with their Traccar IDs
$devices = Device::where('user_id', Auth::id())
->whereIn('id', $this->selectedDevices)
->whereNotNull('traccar_device_id')
->get();
if ($devices->isEmpty()) {
$this->deviceDetails = [];
return;
}
$traccarDeviceIds = $devices->pluck('traccar_device_id')->toArray();
// Get real-time positions from Traccar
$traccarPositions = app(TraccarService::class)->getRealTimePositions($traccarDeviceIds);
// Process positions
$positions = [];
foreach ($devices as $device) {
$traccarPosition = collect($traccarPositions)->firstWhere('deviceId', $device->traccar_device_id);
if ($traccarPosition) {
$positions[] = [
'device_id' => $device->id,
'traccar_device_id' => $device->traccar_device_id,
'device_name' => $device->name,
'unique_id' => $device->unique_id,
'latitude' => $traccarPosition['latitude'],
'longitude' => $traccarPosition['longitude'],
'speed' => $this->convertSpeed($traccarPosition['speed'] ?? 0),
'course' => $traccarPosition['course'] ?? 0,
'altitude' => $traccarPosition['altitude'] ?? 0,
'accuracy' => $traccarPosition['accuracy'] ?? 0,
'status' => $this->calculateDeviceStatus($traccarPosition),
'last_update' => $traccarPosition['deviceTime'] ?? $traccarPosition['serverTime'],
'address' => $traccarPosition['address'] ?? 'Unknown location',
'attributes' => $traccarPosition['attributes'] ?? [],
'valid' => $traccarPosition['valid'] ?? false,
'protocol' => $traccarPosition['protocol'] ?? '',
];
} elseif ($this->showOfflineDevices) {
// Include offline devices
$positions[] = [
'device_id' => $device->id,
'traccar_device_id' => $device->traccar_device_id,
'device_name' => $device->name,
'unique_id' => $device->unique_id,
'latitude' => null,
'longitude' => null,
'speed' => 0,
'course' => 0,
'altitude' => 0,
'accuracy' => 0,
'status' => 'offline',
'last_update' => null,
'address' => 'No signal',
'attributes' => [],
'valid' => false,
'protocol' => '',
];
}
}
$this->deviceDetails = $positions;
$this->lastUpdate = now()->toTimeString();
// Auto-center map if devices are found and no device is being followed
if (!empty($positions) && !$this->followDevice) {
$this->autoCenter();
}
// If following a device, update map center
if ($this->followDevice) {
$this->updateFollowedDevice();
}
// Sync positions to local database (optional for history)
$this->syncPositionsToLocal($positions);
} catch (\Exception $e) {
Log::error('Failed to load real-time positions: ' . $e->getMessage());
session()->flash('error', 'Failed to load device positions: ' . $e->getMessage());
}
}
private function convertSpeed($speedKnots)
{
// Convert from knots to km/h
return round($speedKnots * 1.852, 1);
}
private function calculateDeviceStatus($position)
{
if (!isset($position['deviceTime']) && !isset($position['serverTime'])) {
return 'offline';
}
$lastUpdate = Carbon::parse($position['deviceTime'] ?? $position['serverTime']);
$minutesAgo = $lastUpdate->diffInMinutes(now());
// If position is not valid, consider offline
if (!($position['valid'] ?? false)) {
return 'offline';
}
if ($minutesAgo <= 5) {
return 'online';
} elseif ($minutesAgo <= 30) {
return 'idle';
} else {
return 'offline';
}
}
private function syncPositionsToLocal($positions)
{
try {
foreach ($positions as $positionData) {
if ($positionData['latitude'] && $positionData['longitude']) {
Position::updateOrCreate(
[
'device_id' => $positionData['device_id'],
'device_time' => $positionData['last_update']
],
[
'latitude' => $positionData['latitude'],
'longitude' => $positionData['longitude'],
'speed' => $positionData['speed'] / 1.852, // Convert back to knots for storage
'course' => $positionData['course'],
'altitude' => $positionData['altitude'],
'accuracy' => $positionData['accuracy'],
'address' => $positionData['address'],
'valid' => $positionData['valid'],
'protocol' => $positionData['protocol'],
'attributes' => $positionData['attributes'],
'server_time' => now(),
'fix_time' => $positionData['last_update'],
]
);
}
}
} catch (\Exception $e) {
Log::warning('Failed to sync positions to local database: ' . $e->getMessage());
}
}
public function refreshPositions()
{
$this->loadRealTimePositions();
$this->dispatch('positions-updated', $this->deviceDetails);
}
public function refreshData()
{
$this->loadRealTimePositions();
$this->dispatch('map-updated', [
'devices' => $this->deviceDetails,
'center' => $this->mapCenter,
'zoom' => $this->zoomLevel
]);
}
public function toggleDevice($deviceId)
{
if (in_array($deviceId, $this->selectedDevices)) {
$this->selectedDevices = array_filter($this->selectedDevices, fn($id) => $id !== $deviceId);
} else {
$this->selectedDevices[] = $deviceId;
}
$this->allDevicesSelected = false;
$this->loadRealTimePositions();
}
public function toggleAllDevices()
{
if ($this->allDevicesSelected) {
$this->selectedDevices = [];
$this->allDevicesSelected = false;
} else {
$this->loadUserDevices();
$this->allDevicesSelected = true;
}
$this->loadRealTimePositions();
}
public function selectDevice($deviceId)
{
$this->selectedDevice = $deviceId;
$device = collect($this->deviceDetails)->firstWhere('device_id', $deviceId);
if ($device && $device['latitude'] && $device['longitude']) {
$this->mapCenter = [
'lat' => $device['latitude'],
'lng' => $device['longitude']
];
$this->zoomLevel = 15;
}
}
public function followDevice($deviceId)
{
$this->followDevice = $this->followDevice === $deviceId ? null : $deviceId;
if ($this->followDevice) {
$this->updateFollowedDevice();
}
}
private function updateFollowedDevice()
{
$device = collect($this->deviceDetails)->firstWhere('device_id', $this->followDevice);
if ($device && $device['latitude'] && $device['longitude']) {
$this->mapCenter = [
'lat' => $device['latitude'],
'lng' => $device['longitude']
];
}
}
public function autoCenter()
{
$validPositions = collect($this->deviceDetails)
->filter(fn($device) => $device['latitude'] && $device['longitude']);
if ($validPositions->isEmpty()) {
return;
}
if ($validPositions->count() === 1) {
$device = $validPositions->first();
$this->mapCenter = [
'lat' => $device['latitude'],
'lng' => $device['longitude']
];
$this->zoomLevel = 15;
} else {
// Calculate bounds for multiple devices
$lats = $validPositions->pluck('latitude');
$lngs = $validPositions->pluck('longitude');
$this->mapCenter = [
'lat' => ($lats->min() + $lats->max()) / 2,
'lng' => ($lngs->min() + $lngs->max()) / 2
];
// Determine zoom level based on spread
$latSpread = $lats->max() - $lats->min();
$lngSpread = $lngs->max() - $lngs->min();
$maxSpread = max($latSpread, $lngSpread);
if ($maxSpread > 1) {
$this->zoomLevel = 8;
} elseif ($maxSpread > 0.1) {
$this->zoomLevel = 10;
} elseif ($maxSpread > 0.01) {
$this->zoomLevel = 12;
} else {
$this->zoomLevel = 15;
}
}
}
public function toggleSidebar()
{
$this->sidebarCollapsed = !$this->sidebarCollapsed;
$this->dispatch('sidebar-toggled', $this->sidebarCollapsed);
}
public function toggleShowTrails()
{
$this->showTrails = !$this->showTrails;
$this->dispatch('trails-toggled', $this->showTrails);
}
public function toggleOfflineDevices()
{
$this->showOfflineDevices = !$this->showOfflineDevices;
$this->loadRealTimePositions();
}
public function updateRefreshInterval($interval)
{
$this->refreshInterval = max(5, min(300, $interval)); // Between 5 seconds and 5 minutes
}
public function changeMapStyle($style)
{
$this->mapStyle = $style;
$this->dispatch('map-style-changed', $style);
}
public function changeMapProvider($provider)
{
$mapService = app(MapService::class);
if ($mapService->isProviderEnabled($provider)) {
$this->mapProvider = $provider;
$this->availableStyles = $mapService->getMapStyles($provider);
$this->mapStyle = array_key_first($this->availableStyles);
$mapConfig = $mapService->getMapConfig($provider);
$this->dispatch('map-provider-changed', [
'provider' => $provider,
'styles' => $mapConfig['styles'],
'config' => $mapConfig['config'],
'enabled' => $mapConfig['enabled']
]);
}
}
public function getOnlineDevicesCount()
{
return collect($this->deviceDetails)->where('status', 'online')->count();
}
public function getOfflineDevicesCount()
{
return collect($this->deviceDetails)->whereIn('status', ['offline', 'idle'])->count();
}
public function getTotalDevicesCount()
{
return count($this->deviceDetails);
}
// Real-time updates using polling (can be enhanced with WebSockets later)
public function getListeners()
{
return [
'refresh-positions' => 'refreshPositions',
];
}
public function render()
{
return view('livewire.live-tracking', [
'devices' => Device::where('user_id', Auth::id())->get(),
'onlineCount' => $this->getOnlineDevicesCount(),
'offlineCount' => $this->getOfflineDevicesCount(),
'totalCount' => $this->getTotalDevicesCount(),
]);
}
}

View File

@ -0,0 +1,393 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use App\Models\Device;
use App\Models\Position;
use App\Services\TraccarService;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;
class LiveTracking extends Component
{
public $selectedDevices = [];
public $allDevicesSelected = true;
public $mapCenter = ['lat' => 40.7128, 'lng' => -74.0060]; // Default to NYC
public $zoomLevel = 10;
public $autoRefresh = true;
public $refreshInterval = 15; // seconds
public $showTrails = false;
public $trailDuration = 24; // hours
public $selectedDevice = null;
public $deviceDetails = [];
public $mapStyle = 'streets';
public $followDevice = null;
public $showOfflineDevices = true;
public $lastUpdate = null;
protected $traccarService;
public function boot(TraccarService $traccarService)
{
$this->traccarService = $traccarService;
}
public function mount()
{
$this->loadUserDevices();
$this->loadRealTimePositions();
$this->lastUpdate = now()->toTimeString();
}
public function loadUserDevices()
{
$devices = Device::where('user_id', Auth::id())
->whereNotNull('traccar_device_id')
->get();
$this->selectedDevices = $devices->pluck('id')->toArray();
$this->allDevicesSelected = true;
}
public function loadRealTimePositions()
{
try {
// Get devices with their Traccar IDs
$devices = Device::where('user_id', Auth::id())
->whereIn('id', $this->selectedDevices)
->whereNotNull('traccar_device_id')
->get();
if ($devices->isEmpty()) {
$this->deviceDetails = [];
return;
}
$traccarDeviceIds = $devices->pluck('traccar_device_id')->toArray();
// Get real-time positions from Traccar
$traccarPositions = $this->traccarService->getRealTimePositions($traccarDeviceIds);
// Process positions
$positions = [];
foreach ($devices as $device) {
$traccarPosition = collect($traccarPositions)->firstWhere('deviceId', $device->traccar_device_id);
if ($traccarPosition) {
$positions[] = [
'device_id' => $device->id,
'traccar_device_id' => $device->traccar_device_id,
'device_name' => $device->name,
'unique_id' => $device->unique_id,
'latitude' => $traccarPosition['latitude'],
'longitude' => $traccarPosition['longitude'],
'speed' => $this->convertSpeed($traccarPosition['speed'] ?? 0),
'course' => $traccarPosition['course'] ?? 0,
'altitude' => $traccarPosition['altitude'] ?? 0,
'accuracy' => $traccarPosition['accuracy'] ?? 0,
'status' => $this->calculateDeviceStatus($traccarPosition),
'last_update' => $traccarPosition['deviceTime'] ?? $traccarPosition['serverTime'],
'address' => $traccarPosition['address'] ?? 'Unknown location',
'attributes' => $traccarPosition['attributes'] ?? [],
'valid' => $traccarPosition['valid'] ?? false,
'protocol' => $traccarPosition['protocol'] ?? '',
];
} elseif ($this->showOfflineDevices) {
// Include offline devices
$positions[] = [
'device_id' => $device->id,
'traccar_device_id' => $device->traccar_device_id,
'device_name' => $device->name,
'unique_id' => $device->unique_id,
'latitude' => null,
'longitude' => null,
'speed' => 0,
'course' => 0,
'altitude' => 0,
'accuracy' => 0,
'status' => 'offline',
'last_update' => null,
'address' => 'No signal',
'attributes' => [],
'valid' => false,
'protocol' => '',
];
}
}
$this->deviceDetails = $positions;
$this->lastUpdate = now()->toTimeString();
// Auto-center map if devices are found and no device is being followed
if (!empty($positions) && !$this->followDevice) {
$this->autoCenter();
}
// If following a device, update map center
if ($this->followDevice) {
$this->updateFollowedDevice();
}
// Sync positions to local database (optional for history)
$this->syncPositionsToLocal($positions);
} catch (\Exception $e) {
Log::error('Failed to load real-time positions: ' . $e->getMessage());
session()->flash('error', 'Failed to load device positions: ' . $e->getMessage());
}
}
private function convertSpeed($speedKnots)
{
// Convert from knots to km/h
return round($speedKnots * 1.852, 1);
}
private function calculateDeviceStatus($position)
{
if (!isset($position['deviceTime']) && !isset($position['serverTime'])) {
return 'offline';
}
$lastUpdate = Carbon::parse($position['deviceTime'] ?? $position['serverTime']);
$minutesAgo = $lastUpdate->diffInMinutes(now());
// If position is not valid, consider offline
if (!($position['valid'] ?? false)) {
return 'offline';
}
if ($minutesAgo <= 5) {
return 'online';
} elseif ($minutesAgo <= 30) {
return 'idle';
} else {
return 'offline';
}
}
private function syncPositionsToLocal($positions)
{
try {
foreach ($positions as $positionData) {
if ($positionData['latitude'] && $positionData['longitude']) {
Position::updateOrCreate(
[
'device_id' => $positionData['device_id'],
'device_time' => $positionData['last_update']
],
[
'latitude' => $positionData['latitude'],
'longitude' => $positionData['longitude'],
'speed' => $positionData['speed'] / 1.852, // Convert back to knots for storage
'course' => $positionData['course'],
'altitude' => $positionData['altitude'],
'accuracy' => $positionData['accuracy'],
'address' => $positionData['address'],
'valid' => $positionData['valid'],
'protocol' => $positionData['protocol'],
'attributes' => $positionData['attributes'],
'server_time' => now(),
'fix_time' => $positionData['last_update'],
]
);
}
}
} catch (\Exception $e) {
Log::warning('Failed to sync positions to local database: ' . $e->getMessage());
}
}
public function refreshPositions()
{
$this->loadRealTimePositions();
$this->dispatch('positions-updated', $this->deviceDetails);
}
public function toggleDevice($deviceId)
{
if (in_array($deviceId, $this->selectedDevices)) {
$this->selectedDevices = array_filter($this->selectedDevices, fn($id) => $id !== $deviceId);
} else {
$this->selectedDevices[] = $deviceId;
}
$this->allDevicesSelected = false;
$this->loadRealTimePositions();
}
public function toggleAllDevices()
{
if ($this->allDevicesSelected) {
$this->selectedDevices = [];
$this->allDevicesSelected = false;
} else {
$this->loadUserDevices();
$this->allDevicesSelected = true;
}
$this->loadRealTimePositions();
}
public function selectDevice($deviceId)
{
$this->selectedDevice = $deviceId;
$device = collect($this->deviceDetails)->firstWhere('device_id', $deviceId);
if ($device && $device['latitude'] && $device['longitude']) {
$this->mapCenter = [
'lat' => $device['latitude'],
'lng' => $device['longitude']
];
$this->zoomLevel = 15;
}
}
public function followDevice($deviceId)
{
$this->followDevice = $this->followDevice === $deviceId ? null : $deviceId;
if ($this->followDevice) {
$this->updateFollowedDevice();
}
}
private function updateFollowedDevice()
{
$device = collect($this->deviceDetails)->firstWhere('device_id', $this->followDevice);
if ($device && $device['latitude'] && $device['longitude']) {
$this->mapCenter = [
'lat' => $device['latitude'],
'lng' => $device['longitude']
];
}
}
public function autoCenter()
{
$validPositions = collect($this->deviceDetails)
->filter(fn($device) => $device['latitude'] && $device['longitude']);
if ($validPositions->isEmpty()) {
return;
}
if ($validPositions->count() === 1) {
$device = $validPositions->first();
$this->mapCenter = [
'lat' => $device['latitude'],
'lng' => $device['longitude']
];
$this->zoomLevel = 15;
} else {
// Calculate bounds for multiple devices
$lats = $validPositions->pluck('latitude');
$lngs = $validPositions->pluck('longitude');
$this->mapCenter = [
'lat' => ($lats->min() + $lats->max()) / 2,
'lng' => ($lngs->min() + $lngs->max()) / 2
];
// Determine zoom level based on spread
$latSpread = $lats->max() - $lats->min();
$lngSpread = $lngs->max() - $lngs->min();
$maxSpread = max($latSpread, $lngSpread);
if ($maxSpread > 1) {
$this->zoomLevel = 8;
} elseif ($maxSpread > 0.1) {
$this->zoomLevel = 10;
} elseif ($maxSpread > 0.01) {
$this->zoomLevel = 12;
} else {
$this->zoomLevel = 15;
}
}
}
public function toggleAutoRefresh()
{
$this->autoRefresh = !$this->autoRefresh;
}
public function toggleShowTrails()
{
$this->showTrails = !$this->showTrails;
}
public function toggleOfflineDevices()
{
$this->showOfflineDevices = !$this->showOfflineDevices;
$this->loadRealTimePositions();
}
public function updateRefreshInterval($interval)
{
$this->refreshInterval = max(5, min(300, $interval)); // Between 5 seconds and 5 minutes
}
public function changeMapStyle($style)
{
$this->mapStyle = $style;
}
public function getOnlineDevicesCount()
{
return collect($this->deviceDetails)->where('status', 'online')->count();
}
public function getOfflineDevicesCount()
{
return collect($this->deviceDetails)->whereIn('status', ['offline', 'idle'])->count();
}
public function getTotalDevicesCount()
{
return count($this->deviceDetails);
}
// Real-time updates using polling (can be enhanced with WebSockets later)
public function getListeners()
{
return [
'refresh-positions' => 'refreshPositions',
];
}
public function render()
{
$query = Device::where('user_id', Auth::id());
// Check if device groups have users relationship (safely handle missing relationship)
if (method_exists(\App\Models\DeviceGroup::class, 'users')) {
$query->orWhereHas('group.users', function($subQuery) {
$subQuery->where('user_id', Auth::id());
});
}
$devices = $query->get();
return view('livewire.live-tracking', [
'devices' => $devices,
'onlineCount' => $this->getOnlineDevicesCount(),
'offlineCount' => $this->getOfflineDevicesCount(),
'totalCount' => $this->getTotalDevicesCount(),
]);
}
public function toggleTrails()
{
$this->showTrails = !$this->showTrails;
$this->dispatch('toggleTrails', $this->showTrails);
}
}
$devices = $query->get();
return view('livewire.live-tracking', [
'devices' => $devices
]);
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use App\Services\MapService;
use Illuminate\Support\Facades\Config;
class MapSettings extends Component
{
public $googleMapsApiKey = '';
public $mapboxApiKey = '';
public $defaultProvider = 'openstreetmap';
public $providersStatus = [];
public function mount()
{
$this->googleMapsApiKey = config('services.maps.providers.google.api_key', '');
$this->mapboxApiKey = config('services.maps.providers.mapbox.api_key', '');
$this->defaultProvider = config('services.maps.default_provider', 'openstreetmap');
$this->loadProvidersStatus();
}
public function loadProvidersStatus()
{
$mapService = app(MapService::class);
$this->providersStatus = $mapService->getAllProvidersStatus();
}
public function saveSettings()
{
// Note: In a real application, you would save these to a database
// or update the .env file programmatically
session()->flash('success', 'Settings saved! Please update your .env file with the API keys and restart the application.');
}
public function testProvider($provider)
{
$mapService = app(MapService::class);
if ($mapService->isProviderEnabled($provider)) {
session()->flash('success', "{$provider} is configured and working!");
} else {
session()->flash('error', "{$provider} requires API key configuration.");
}
$this->loadProvidersStatus();
}
public function render()
{
return view('livewire.map-settings');
}
}

View File

@ -0,0 +1,234 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use App\Models\User;
class NotificationCenter extends Component
{
protected $layout = 'layouts.app';
public $showSettingsModal = false;
public $showUserSettingsModal = false;
public $editingUserSettings = null;
public $userSettingsForm = [
'email_enabled' => true,
'sms_enabled' => true,
'push_enabled' => false,
'notification_frequency' => 'immediate',
'quiet_hours_start' => '22:00',
'quiet_hours_end' => '08:00',
'event_types' => [],
];
public $filters = [
'search' => '',
'channel' => '',
'status' => '',
'event_type' => '',
'date_range' => '',
];
public $globalSettings = [
'email_enabled' => true,
'sms_enabled' => true,
'push_enabled' => false,
'webhook_enabled' => false,
'max_daily_notifications' => 100,
'quiet_hours_start' => '22:00',
'quiet_hours_end' => '08:00',
'webhook_url' => '',
];
public function getStatsProperty()
{
return [
'unread_count' => 3,
'recent_alerts' => collect([
(object) ['message' => 'Device offline', 'time' => now()->subMinutes(5)],
(object) ['message' => 'Speed exceeded', 'time' => now()->subMinutes(15)],
(object) ['message' => 'Geofence exit', 'time' => now()->subMinutes(30)],
]),
'system_notifications' => collect([
(object) ['message' => 'System maintenance scheduled', 'time' => now()->subHours(2)],
(object) ['message' => 'New feature available', 'time' => now()->subHours(5)],
]),
'notifications_sent_today' => 47,
'email_notifications_sent' => 32,
'sms_notifications_sent' => 15,
'pending_notifications' => 3,
'failed_notifications' => 1,
'notification_rate' => 96.2,
'delivery_success_rate' => 95.5,
'average_delivery_time' => 2.3,
'total_notifications_sent' => 1247,
'webhook_notifications_sent' => 0,
];
}
public function render()
{
// Mock user settings with proper user objects
$userSettings = collect([
(object) [
'id' => 1,
'user_id' => 1,
'email_enabled' => true,
'sms_enabled' => true,
'push_enabled' => false,
'notification_frequency' => 'immediate',
'quiet_hours_start' => '22:00',
'quiet_hours_end' => '08:00',
'user' => (object) [
'id' => 1,
'name' => 'John Doe',
'email' => 'john@example.com'
]
],
(object) [
'id' => 2,
'user_id' => 2,
'email_enabled' => false,
'sms_enabled' => true,
'push_enabled' => true,
'notification_frequency' => 'daily',
'quiet_hours_start' => '23:00',
'quiet_hours_end' => '07:00',
'user' => (object) [
'id' => 2,
'name' => 'Jane Smith',
'email' => 'jane@example.com'
]
]
]);
// Mock notifications with user data to prevent property access errors
$notifications = collect([
(object) [
'id' => 1,
'type' => 'email',
'message' => 'Device offline alert',
'created_at' => now()->subMinutes(5),
'status' => 'sent',
'user' => (object) [
'id' => 1,
'name' => 'John Doe',
'email' => 'john@example.com'
]
],
(object) [
'id' => 2,
'type' => 'sms',
'message' => 'Speed limit exceeded',
'created_at' => now()->subMinutes(15),
'status' => 'pending',
'user' => (object) [
'id' => 2,
'name' => 'Jane Smith',
'email' => 'jane@example.com'
]
]
]);
// Mock recent notifications for the activity feed
$recentNotifications = collect([
(object) [
'id' => 1,
'channel' => 'email',
'event_type' => 'Vehicle Maintenance',
'message' => 'Vehicle maintenance reminder sent to John Doe',
'created_at' => now()->subMinutes(5),
'status' => 'delivered',
'recipient' => 'john@example.com'
],
(object) [
'id' => 2,
'channel' => 'sms',
'event_type' => 'Speed Violation',
'message' => 'Speed violation alert sent to Jane Smith',
'created_at' => now()->subMinutes(15),
'status' => 'delivered',
'recipient' => '+1234567890'
],
(object) [
'id' => 3,
'channel' => 'push',
'event_type' => 'Device Offline',
'message' => 'Device offline notification',
'created_at' => now()->subMinutes(30),
'status' => 'pending',
'recipient' => 'Mobile App'
],
(object) [
'id' => 4,
'channel' => 'email',
'event_type' => 'Weekly Report',
'message' => 'Weekly report sent to admin users',
'created_at' => now()->subHours(2),
'status' => 'delivered',
'recipient' => 'admin@example.com'
]
]);
return view('livewire.notification-center', [
'userSettings' => $userSettings,
'notifications' => $notifications,
'recentNotifications' => $recentNotifications,
]);
}
public function editUserSettings($userId)
{
$this->editingUserSettings = (object) [
'id' => $userId,
'user_id' => $userId,
'email_enabled' => true,
'sms_enabled' => true,
'push_enabled' => false,
'notification_frequency' => 'immediate',
'quiet_hours_start' => '22:00',
'quiet_hours_end' => '08:00',
'user' => (object) [
'id' => $userId,
'name' => $userId == 1 ? 'John Doe' : 'Jane Smith',
'email' => $userId == 1 ? 'john@example.com' : 'jane@example.com'
]
];
$this->showUserSettingsModal = true;
}
public function closeSettingsModal()
{
$this->showSettingsModal = false;
}
public function closeUserSettingsModal()
{
$this->showUserSettingsModal = false;
$this->editingUserSettings = null;
}
// Handle legacy method names (backward compatibility)
public function showGlobalsettingsModal()
{
$this->showSettingsModal = true;
}
public function updateGlobalSettings()
{
// Here you would typically save to database
// For now, just keep the in-memory changes
session()->flash('message', 'Global settings updated successfully.');
}
public function showUsersettingsModal($userId = null)
{
if ($userId) {
$this->editUserSettings($userId);
} else {
$this->showUserSettingsModal = true;
}
}
}

View File

@ -0,0 +1,406 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use App\Models\Device;
use App\Models\Position;
use App\Models\Event;
use App\Services\TraccarService;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
class ReportsAndHistory extends Component
{
public $selectedDevices = [];
public $startDate = '';
public $endDate = '';
public $reportType = 'route';
public $showMap = true;
public $includeMap = true;
public $exportFormat = 'pdf';
// Report data
public $reportData = [];
public $routeData = [];
public $summaryStats = [];
public $eventsData = [];
public $stopsData = [];
// Report settings
public $includeStops = true;
public $minStopDuration = 5; // minutes
public $maxSpeed = 120; // km/h for overspeed detection
public $groupByDate = false;
protected $traccarService;
public function boot(TraccarService $traccarService)
{
$this->traccarService = $traccarService;
}
public function mount()
{
// Set default date range (last 7 days)
$this->endDate = now()->format('Y-m-d');
$this->startDate = now()->subDays(7)->format('Y-m-d');
// Select all user devices by default
$deviceQuery = Device::where('user_id', Auth::id());
// Check if device groups have users relationship (safely handle missing relationship)
if (method_exists(\App\Models\DeviceGroup::class, 'users')) {
$deviceQuery->orWhereHas('group.users', function($subQuery) {
$subQuery->where('user_id', Auth::id());
});
}
$this->selectedDevices = $deviceQuery->pluck('id')->toArray();
}
public function generateReport()
{
$this->validate([
'selectedDevices' => 'required|array|min:1',
'startDate' => 'required|date',
'endDate' => 'required|date|after_or_equal:startDate',
]);
try {
switch ($this->reportType) {
case 'route':
$this->generateRouteReport();
break;
case 'summary':
$this->generateSummaryReport();
break;
case 'events':
$this->generateEventsReport();
break;
case 'stops':
$this->generateStopsReport();
break;
case 'trips':
$this->generateTripsReport();
break;
}
session()->flash('success', 'Report generated successfully!');
} catch (\Exception $e) {
session()->flash('error', 'Failed to generate report: ' . $e->getMessage());
}
}
private function generateRouteReport()
{
$startDateTime = Carbon::parse($this->startDate)->startOfDay();
$endDateTime = Carbon::parse($this->endDate)->endOfDay();
$this->routeData = [];
foreach ($this->selectedDevices as $deviceId) {
$device = Device::find($deviceId);
if (!$device) continue;
$positions = Position::where('device_id', $deviceId)
->whereBetween('device_time', [$startDateTime, $endDateTime])
->orderBy('device_time')
->get();
if ($positions->count() > 0) {
$route = [
'device' => $device,
'positions' => $positions,
'start_time' => $positions->first()->device_time,
'end_time' => $positions->last()->device_time,
'total_distance' => $this->calculateTotalDistance($positions),
'max_speed' => $positions->max('speed') ?? 0,
'avg_speed' => $positions->avg('speed') ?? 0,
];
if ($this->includeStops) {
$route['stops'] = $this->detectStops($positions);
}
$this->routeData[] = $route;
}
}
}
private function generateSummaryReport()
{
$startDateTime = Carbon::parse($this->startDate)->startOfDay();
$endDateTime = Carbon::parse($this->endDate)->endOfDay();
$this->summaryStats = [];
foreach ($this->selectedDevices as $deviceId) {
$device = Device::find($deviceId);
if (!$device) continue;
$positions = Position::where('device_id', $deviceId)
->whereBetween('device_time', [$startDateTime, $endDateTime])
->get();
$events = Event::where('device_id', $deviceId)
->whereBetween('event_time', [$startDateTime, $endDateTime])
->get();
$totalDistance = $this->calculateTotalDistance($positions);
$totalTime = $positions->count() > 1 ?
$positions->last()->device_time->diffInMinutes($positions->first()->device_time) : 0;
$this->summaryStats[] = [
'device' => $device,
'total_distance' => $totalDistance,
'total_time' => $totalTime,
'avg_speed' => $totalTime > 0 ? ($totalDistance / ($totalTime / 60)) : 0,
'max_speed' => $positions->max('speed') ?? 0,
'total_events' => $events->count(),
'overspeed_events' => $events->where('type', 'deviceOverspeed')->count(),
'online_time' => $this->calculateOnlineTime($positions),
'fuel_consumption' => $this->calculateFuelConsumption($positions),
];
}
}
private function generateEventsReport()
{
$startDateTime = Carbon::parse($this->startDate)->startOfDay();
$endDateTime = Carbon::parse($this->endDate)->endOfDay();
$this->eventsData = Event::with(['device', 'geofence'])
->whereIn('device_id', $this->selectedDevices)
->whereBetween('event_time', [$startDateTime, $endDateTime])
->orderBy('event_time', 'desc')
->get()
->groupBy(function($event) {
return $this->groupByDate ?
$event->event_time->format('Y-m-d') :
$event->device->name;
});
}
private function generateStopsReport()
{
$startDateTime = Carbon::parse($this->startDate)->startOfDay();
$endDateTime = Carbon::parse($this->endDate)->endOfDay();
$this->stopsData = [];
foreach ($this->selectedDevices as $deviceId) {
$device = Device::find($deviceId);
if (!$device) continue;
$positions = Position::where('device_id', $deviceId)
->whereBetween('device_time', [$startDateTime, $endDateTime])
->orderBy('device_time')
->get();
$stops = $this->detectStops($positions);
if (!empty($stops)) {
$this->stopsData[] = [
'device' => $device,
'stops' => $stops,
'total_stops' => count($stops),
'total_stop_time' => array_sum(array_column($stops, 'duration')),
];
}
}
}
private function generateTripsReport()
{
$startDateTime = Carbon::parse($this->startDate)->startOfDay();
$endDateTime = Carbon::parse($this->endDate)->endOfDay();
$this->reportData = [];
foreach ($this->selectedDevices as $deviceId) {
$device = Device::find($deviceId);
if (!$device) continue;
$positions = Position::where('device_id', $deviceId)
->whereBetween('device_time', [$startDateTime, $endDateTime])
->orderBy('device_time')
->get();
$trips = $this->detectTrips($positions);
$this->reportData[] = [
'device' => $device,
'trips' => $trips,
'total_trips' => count($trips),
'total_distance' => array_sum(array_column($trips, 'distance')),
'total_duration' => array_sum(array_column($trips, 'duration')),
];
}
}
private function calculateTotalDistance($positions)
{
$totalDistance = 0;
$previousPosition = null;
foreach ($positions as $position) {
if ($previousPosition) {
$distance = $this->calculateDistance(
$previousPosition->latitude,
$previousPosition->longitude,
$position->latitude,
$position->longitude
);
$totalDistance += $distance;
}
$previousPosition = $position;
}
return round($totalDistance, 2);
}
private function calculateDistance($lat1, $lon1, $lat2, $lon2)
{
$earthRadius = 6371; // kilometers
$dLat = deg2rad($lat2 - $lat1);
$dLon = deg2rad($lon2 - $lon1);
$a = sin($dLat/2) * sin($dLat/2) +
cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
sin($dLon/2) * sin($dLon/2);
$c = 2 * atan2(sqrt($a), sqrt(1-$a));
return $earthRadius * $c;
}
private function detectStops($positions)
{
$stops = [];
$currentStop = null;
$speedThreshold = 2; // km/h
foreach ($positions as $position) {
$speed = $position->speed ?? 0;
if ($speed <= $speedThreshold) {
if (!$currentStop) {
$currentStop = [
'start_time' => $position->device_time,
'latitude' => $position->latitude,
'longitude' => $position->longitude,
'address' => $position->address,
];
}
} else {
if ($currentStop) {
$currentStop['end_time'] = $position->device_time;
$currentStop['duration'] = $currentStop['start_time']->diffInMinutes($currentStop['end_time']);
if ($currentStop['duration'] >= $this->minStopDuration) {
$stops[] = $currentStop;
}
$currentStop = null;
}
}
}
return $stops;
}
private function detectTrips($positions)
{
$trips = [];
$currentTrip = null;
$speedThreshold = 2; // km/h
foreach ($positions as $position) {
$speed = $position->speed ?? 0;
if ($speed > $speedThreshold) {
if (!$currentTrip) {
$currentTrip = [
'start_time' => $position->device_time,
'start_latitude' => $position->latitude,
'start_longitude' => $position->longitude,
'start_address' => $position->address,
'positions' => [$position],
];
} else {
$currentTrip['positions'][] = $position;
}
} else {
if ($currentTrip && count($currentTrip['positions']) > 1) {
$lastPosition = end($currentTrip['positions']);
$currentTrip['end_time'] = $lastPosition->device_time;
$currentTrip['end_latitude'] = $lastPosition->latitude;
$currentTrip['end_longitude'] = $lastPosition->longitude;
$currentTrip['end_address'] = $lastPosition->address;
$currentTrip['duration'] = $currentTrip['start_time']->diffInMinutes($currentTrip['end_time']);
$currentTrip['distance'] = $this->calculateTotalDistance(collect($currentTrip['positions']));
$currentTrip['max_speed'] = collect($currentTrip['positions'])->max('speed') ?? 0;
unset($currentTrip['positions']); // Remove positions to save memory
$trips[] = $currentTrip;
$currentTrip = null;
}
}
}
return $trips;
}
private function calculateOnlineTime($positions)
{
if ($positions->count() < 2) return 0;
return $positions->first()->device_time->diffInMinutes($positions->last()->device_time);
}
private function calculateFuelConsumption($positions)
{
// This is a simplified calculation - in reality you'd use more sophisticated methods
$totalDistance = $this->calculateTotalDistance($positions);
$avgConsumption = 8; // liters per 100km (default)
return round(($totalDistance / 100) * $avgConsumption, 2);
}
public function exportReport()
{
// This would implement actual export functionality
session()->flash('success', 'Report export started. You will receive a download link shortly.');
}
public function render()
{
$deviceQuery = Device::where('user_id', Auth::id());
// Check if device groups have users relationship (safely handle missing relationship)
if (method_exists(\App\Models\DeviceGroup::class, 'users')) {
$deviceQuery->orWhereHas('group.users', function($subQuery) {
$subQuery->where('user_id', Auth::id());
});
}
$devices = $deviceQuery->get();
// Get recent events for the current user
$recentEvents = Event::with(['device', 'position'])
->whereHas('device', function($q) {
$q->where('user_id', Auth::id());
})
->orderBy('event_time', 'desc')
->limit(10)
->get();
return view('livewire.reports-and-history', [
'devices' => $devices,
'recentEvents' => $recentEvents,
]);
}
}

View File

@ -0,0 +1,256 @@
<?php
namespace App\Livewire;
use App\Models\Subscription;
use App\Models\User;
use Livewire\Component;
use Livewire\WithPagination;
class SubscriptionManagement extends Component
{
use WithPagination;
public $filters = [
'search' => '',
'status' => '',
'plan' => '',
'billing_cycle' => '',
'payment_status' => '',
];
public $showCreateSubscriptionModal = false;
public $showEditSubscriptionModal = false;
public $showBillingModal = false;
public $editingSubscription = null;
public $selectedSubscription = null;
public $billingHistory = [];
public $subscriptionForm = [
'user_id' => '',
'plan' => '',
'billing_cycle' => '',
'amount' => '',
'device_limit' => '',
'status' => 'active',
'starts_at' => '',
'ends_at' => '',
'next_billing_date' => '',
'stripe_id' => '',
'notes' => '',
];
protected $layout = 'layouts.app';
public function mount()
{
// Initialize component without parameters
}
public function getStatsProperty()
{
$activeSubscriptions = Subscription::where('status', 'active')->count();
$monthlyRevenue = Subscription::where('status', 'active')
->where('billing_cycle', 'monthly')
->sum('amount') +
(Subscription::where('status', 'active')
->where('billing_cycle', 'yearly')
->sum('amount') / 12);
$expiringSubscriptions = Subscription::where('status', 'active')
->where('ends_at', '<=', now()->addDays(30))
->count();
$newSubscriptions = Subscription::where('created_at', '>=', now()->startOfMonth())->count();
$avgSubscriptionValue = Subscription::where('status', 'active')->avg('amount');
return [
'monthly_revenue' => $monthlyRevenue,
'revenue_change' => 12.5, // Mock percentage change
'active_subscriptions' => $activeSubscriptions,
'new_subscriptions' => $newSubscriptions,
'expiring_soon' => $expiringSubscriptions,
'avg_subscription_value' => $avgSubscriptionValue,
];
}
public function render()
{
$subscriptions = Subscription::query()
->with(['user'])
->when($this->filters['search'], function ($query) {
$query->whereHas('user', function ($q) {
$q->where('name', 'like', '%' . $this->filters['search'] . '%')
->orWhere('email', 'like', '%' . $this->filters['search'] . '%');
});
})
->when($this->filters['status'], function ($query) {
$query->where('status', $this->filters['status']);
})
->when($this->filters['plan'], function ($query) {
$query->where('plan', $this->filters['plan']);
})
->when($this->filters['billing_cycle'], function ($query) {
$query->where('billing_cycle', $this->filters['billing_cycle']);
})
->when($this->filters['payment_status'], function ($query) {
$query->where('payment_status', $this->filters['payment_status']);
})
->orderBy('created_at', 'desc')
->paginate(15);
$users = User::whereDoesntHave('activeSubscription')->get();
return view('livewire.subscription-management', [
'subscriptions' => $subscriptions,
'users' => $users,
]);
}
public function editSubscription($subscriptionId)
{
$this->editingSubscription = Subscription::with('user')->find($subscriptionId);
$this->subscriptionForm = [
'user_id' => $this->editingSubscription->user_id,
'plan' => $this->editingSubscription->plan,
'billing_cycle' => $this->editingSubscription->billing_cycle,
'amount' => $this->editingSubscription->amount,
'device_limit' => $this->editingSubscription->device_limit,
'status' => $this->editingSubscription->status,
'starts_at' => $this->editingSubscription->starts_at?->format('Y-m-d'),
'ends_at' => $this->editingSubscription->ends_at?->format('Y-m-d'),
'next_billing_date' => $this->editingSubscription->next_billing_date?->format('Y-m-d'),
'stripe_id' => $this->editingSubscription->stripe_id,
'notes' => $this->editingSubscription->notes,
];
$this->showEditSubscriptionModal = true;
}
public function createSubscription()
{
$this->validate([
'subscriptionForm.user_id' => 'required|exists:users,id',
'subscriptionForm.plan' => 'required|string',
'subscriptionForm.billing_cycle' => 'required|string',
'subscriptionForm.amount' => 'required|numeric|min:0',
'subscriptionForm.device_limit' => 'required|integer|min:1',
]);
Subscription::create([
'user_id' => $this->subscriptionForm['user_id'],
'plan' => $this->subscriptionForm['plan'],
'billing_cycle' => $this->subscriptionForm['billing_cycle'],
'amount' => $this->subscriptionForm['amount'],
'device_limit' => $this->subscriptionForm['device_limit'],
'status' => $this->subscriptionForm['status'],
'starts_at' => $this->subscriptionForm['starts_at'] ? \Carbon\Carbon::parse($this->subscriptionForm['starts_at']) : now(),
'ends_at' => $this->subscriptionForm['ends_at'] ? \Carbon\Carbon::parse($this->subscriptionForm['ends_at']) : null,
'next_billing_date' => $this->subscriptionForm['next_billing_date'] ? \Carbon\Carbon::parse($this->subscriptionForm['next_billing_date']) : null,
'stripe_id' => $this->subscriptionForm['stripe_id'],
'notes' => $this->subscriptionForm['notes'],
]);
$this->closeSubscriptionModal();
$this->reset('subscriptionForm');
session()->flash('message', 'Subscription created successfully.');
}
public function updateSubscription()
{
$this->validate([
'subscriptionForm.plan' => 'required|string',
'subscriptionForm.billing_cycle' => 'required|string',
'subscriptionForm.amount' => 'required|numeric|min:0',
'subscriptionForm.device_limit' => 'required|integer|min:1',
]);
$this->editingSubscription->update([
'plan' => $this->subscriptionForm['plan'],
'billing_cycle' => $this->subscriptionForm['billing_cycle'],
'amount' => $this->subscriptionForm['amount'],
'device_limit' => $this->subscriptionForm['device_limit'],
'status' => $this->subscriptionForm['status'],
'starts_at' => $this->subscriptionForm['starts_at'] ? \Carbon\Carbon::parse($this->subscriptionForm['starts_at']) : $this->editingSubscription->starts_at,
'ends_at' => $this->subscriptionForm['ends_at'] ? \Carbon\Carbon::parse($this->subscriptionForm['ends_at']) : null,
'next_billing_date' => $this->subscriptionForm['next_billing_date'] ? \Carbon\Carbon::parse($this->subscriptionForm['next_billing_date']) : null,
'stripe_id' => $this->subscriptionForm['stripe_id'],
'notes' => $this->subscriptionForm['notes'],
]);
$this->closeSubscriptionModal();
$this->reset('subscriptionForm');
session()->flash('message', 'Subscription updated successfully.');
}
public function cancelSubscription($subscriptionId)
{
Subscription::find($subscriptionId)->update(['status' => 'cancelled']);
session()->flash('message', 'Subscription cancelled successfully.');
}
public function renewSubscription($subscriptionId)
{
Subscription::find($subscriptionId)->update(['status' => 'active']);
session()->flash('message', 'Subscription renewed successfully.');
}
public function viewBilling($subscriptionId)
{
$this->selectedSubscription = Subscription::with('user')->find($subscriptionId);
// Mock billing history - in real app, this would come from payment provider
$this->billingHistory = collect([
(object) [
'created_at' => now()->subMonth(),
'amount' => $this->selectedSubscription->amount,
'status' => 'paid',
'invoice_url' => null,
],
(object) [
'created_at' => now()->subMonths(2),
'amount' => $this->selectedSubscription->amount,
'status' => 'paid',
'invoice_url' => null,
],
]);
$this->showBillingModal = true;
}
public function closeSubscriptionModal()
{
$this->showCreateSubscriptionModal = false;
$this->showEditSubscriptionModal = false;
$this->editingSubscription = null;
$this->reset('subscriptionForm');
}
public function closeBillingModal()
{
$this->showBillingModal = false;
$this->selectedSubscription = null;
$this->billingHistory = [];
}
// Handle legacy method name (backward compatibility)
public function showSubscriptionformModal($subscriptionId = null)
{
if ($subscriptionId) {
$this->editSubscription($subscriptionId);
} else {
$this->showCreateModal = true;
}
}
// Handle legacy method name (backward compatibility)
public function showBillinghistoryModal($subscriptionId = null)
{
if ($subscriptionId) {
$this->viewBilling($subscriptionId);
}
}
public function updatingFilters()
{
$this->resetPage();
}
}

View File

@ -0,0 +1,208 @@
<?php
namespace App\Livewire;
use App\Models\User;
use Spatie\Permission\Models\Role;
use Livewire\Component;
use Livewire\WithPagination;
class UserManagement extends Component
{
use WithPagination;
public $filters = [
'search' => '',
'status' => '',
'role' => '',
'subscription' => '',
];
public $showModal = false;
public $editingUser = null;
public $form = [
'name' => '',
'email' => '',
'password' => '',
'company' => '',
'status' => 'active',
'roles' => [],
'notes' => '',
];
public function mount()
{
//
}
public function render()
{
$users = User::query()
->when($this->filters['search'], function ($query) {
$query->where(function ($q) {
$q->where('name', 'like', '%' . $this->filters['search'] . '%')
->orWhere('email', 'like', '%' . $this->filters['search'] . '%');
});
})
->when($this->filters['status'], function ($query) {
$query->where('status', $this->filters['status']);
})
->when($this->filters['role'], function ($query) {
$query->role($this->filters['role']);
})
->when($this->filters['subscription'], function ($query) {
if ($this->filters['subscription'] === 'active') {
$query->whereHas('activeSubscription');
} elseif ($this->filters['subscription'] === 'expired') {
$query->whereHas('subscriptions', function ($q) {
$q->where('status', 'expired');
});
} elseif ($this->filters['subscription'] === 'none') {
$query->whereDoesntHave('subscriptions');
}
})
->with(['roles', 'activeSubscription'])
->paginate(15);
$roles = Role::all();
return view('livewire.user-management', [
'users' => $users,
'roles' => $roles,
]);
}
public function editUser($userId)
{
$this->editingUser = User::with('roles')->find($userId);
$this->form = [
'name' => $this->editingUser->name,
'email' => $this->editingUser->email,
'password' => '',
'company' => $this->editingUser->company,
'status' => $this->editingUser->status,
'roles' => $this->editingUser->roles->pluck('name')->toArray(),
'notes' => $this->editingUser->notes,
];
$this->showModal = true;
}
public function openCreateModal()
{
$this->resetForm();
$this->editingUser = null;
$this->showModal = true;
}
private function resetForm()
{
$this->form = [
'name' => '',
'email' => '',
'password' => '',
'company' => '',
'status' => 'active',
'roles' => [],
'notes' => ''
];
}
public function createUser()
{
$this->validate([
'form.name' => 'required|string|max:255',
'form.email' => 'required|email|unique:users,email',
'form.password' => 'required|string|min:8',
'form.company' => 'nullable|string|max:255',
'form.status' => 'required|in:active,inactive,suspended',
'form.notes' => 'nullable|string',
]);
$user = User::create([
'name' => $this->form['name'],
'email' => $this->form['email'],
'password' => bcrypt($this->form['password']),
'company' => $this->form['company'],
'status' => $this->form['status'],
'notes' => $this->form['notes'],
]);
if (!empty($this->form['roles'])) {
$user->assignRole($this->form['roles']);
}
$this->closeModal();
session()->flash('message', 'User created successfully.');
}
public function updateUser()
{
$this->validate([
'form.name' => 'required|string|max:255',
'form.email' => 'required|email|unique:users,email,' . $this->editingUser->id,
'form.password' => 'nullable|string|min:8',
'form.company' => 'nullable|string|max:255',
'form.status' => 'required|in:active,inactive,suspended',
'form.notes' => 'nullable|string',
]);
$this->editingUser->update([
'name' => $this->form['name'],
'email' => $this->form['email'],
'company' => $this->form['company'],
'status' => $this->form['status'],
'notes' => $this->form['notes'],
]);
if (!empty($this->form['password'])) {
$this->editingUser->update(['password' => bcrypt($this->form['password'])]);
}
$this->editingUser->syncRoles($this->form['roles']);
$this->closeModal();
session()->flash('message', 'User updated successfully.');
}
public function suspendUser($userId)
{
User::find($userId)->update(['status' => 'suspended']);
session()->flash('message', 'User suspended successfully.');
}
public function activateUser($userId)
{
User::find($userId)->update(['status' => 'active']);
session()->flash('message', 'User activated successfully.');
}
public function deleteUser($userId)
{
// Prevent deleting the current user
if ($userId == auth()->id()) {
session()->flash('error', 'You cannot delete your own account.');
return;
}
$user = User::find($userId);
if ($user) {
// Remove all roles before deleting
$user->syncRoles([]);
$user->delete();
session()->flash('message', 'User deleted successfully.');
}
}
public function closeModal()
{
$this->showModal = false;
$this->editingUser = null;
$this->reset('form');
}
public function updatingFilters()
{
$this->resetPage();
}
}

83
app/Models/Command.php Normal file
View File

@ -0,0 +1,83 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Command extends Model
{
use HasFactory;
protected $fillable = [
'device_id',
'user_id',
'traccar_command_id',
'type',
'description',
'parameters',
'attributes',
'sent_at',
'delivered_at',
'executed_at',
'expires_at',
'status',
'response',
'error_message',
];
protected $casts = [
'attributes' => 'array',
'parameters' => 'array',
'sent_at' => 'datetime',
'delivered_at' => 'datetime',
'executed_at' => 'datetime',
'expires_at' => 'datetime',
];
/**
* Get the device this command was sent to
*/
public function device(): BelongsTo
{
return $this->belongsTo(Device::class);
}
/**
* Get the user who sent this command
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Scope for pending commands
*/
public function scopePending($query)
{
return $query->where('status', 'pending');
}
/**
* Scope for successful commands
*/
public function scopeSuccessful($query)
{
return $query->where('status', 'success');
}
/**
* Get status color
*/
public function getStatusColor(): string
{
return match ($this->status) {
'success' => 'green',
'failed' => 'red',
'pending' => 'yellow',
default => 'gray'
};
}
}

172
app/Models/Device.php Normal file
View File

@ -0,0 +1,172 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Device extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'traccar_device_id',
'name',
'unique_id',
'imei',
'phone',
'model',
'contact',
'category',
'protocol',
'status',
'last_update',
'position_id',
'group_id',
'attributes',
'is_active',
];
protected $casts = [
'last_update' => 'datetime',
'attributes' => 'array',
'is_active' => 'boolean',
];
/**
* Get the user that owns the device
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Get the device group
*/
public function group(): BelongsTo
{
return $this->belongsTo(DeviceGroup::class, 'group_id');
}
/**
* Get the current position
*/
public function currentPosition(): BelongsTo
{
return $this->belongsTo(Position::class, 'position_id');
}
/**
* Get all positions for this device
*/
public function positions(): HasMany
{
return $this->hasMany(Position::class);
}
/**
* Get the latest position for this device
*/
public function latestPosition(): HasOne
{
return $this->hasOne(Position::class)->latestOfMany('device_time');
}
/**
* Get the assigned driver
*/
public function driver(): BelongsTo
{
return $this->belongsTo(Driver::class);
}
/**
* Get device events
*/
public function events(): HasMany
{
return $this->hasMany(Event::class);
}
/**
* Get assigned geofences
*/
public function geofences(): BelongsToMany
{
return $this->belongsToMany(Geofence::class, 'device_geofences');
}
/**
* Get device commands
*/
public function commands(): HasMany
{
return $this->hasMany(Command::class);
}
/**
* Scope for active devices
*/
public function scopeActive($query)
{
return $query->where('is_active', true);
}
/**
* Scope for devices by status
*/
public function scopeByStatus($query, $status)
{
return $query->where('status', $status);
}
/**
* Check if device is online
*/
public function isOnline(): bool
{
if (!$this->last_update) {
return false;
}
return $this->last_update->diffInMinutes(now()) <= 5;
}
/**
* Get device status badge color
*/
public function getStatusColor(): string
{
return match ($this->status) {
'online' => 'green',
'offline' => 'red',
'unknown' => 'gray',
default => 'gray'
};
}
/**
* Get latest position data
*/
public function getLatestPosition(): ?array
{
if (!$this->currentPosition) {
return null;
}
return [
'latitude' => $this->currentPosition->latitude,
'longitude' => $this->currentPosition->longitude,
'speed' => $this->currentPosition->speed,
'course' => $this->currentPosition->course,
'address' => $this->currentPosition->address,
'timestamp' => $this->currentPosition->device_time,
];
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class DeviceGroup extends Model
{
use HasFactory;
protected $fillable = [
'traccar_group_id',
'name',
'description',
'attributes',
'is_active',
];
protected $casts = [
'attributes' => 'array',
'is_active' => 'boolean',
];
/**
* Get devices in this group
*/
public function devices(): HasMany
{
return $this->hasMany(Device::class, 'group_id');
}
/**
* Get users who have access to this group
*/
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class, 'device_group_user', 'device_group_id', 'user_id');
}
/**
* Scope for active groups
*/
public function scopeActive($query)
{
return $query->where('is_active', true);
}
/**
* Get device count in this group
*/
public function getDeviceCountAttribute(): int
{
return $this->devices()->count();
}
}

61
app/Models/Driver.php Normal file
View File

@ -0,0 +1,61 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Driver extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'driver_id',
'name',
'license_number',
'license_type',
'license_expiry_date',
'phone',
'email',
'assigned_vehicle',
'vehicle_plate',
'performance_score',
'status',
'attributes',
'notes',
'is_active',
];
protected $casts = [
'attributes' => 'array',
'is_active' => 'boolean',
'license_expiry_date' => 'date',
];
/**
* Get the user associated with this driver
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Get devices assigned to this driver
*/
public function devices(): HasMany
{
return $this->hasMany(Device::class);
}
/**
* Scope for active drivers
*/
public function scopeActive($query)
{
return $query->where('is_active', true);
}
}

134
app/Models/Event.php Normal file
View File

@ -0,0 +1,134 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Event extends Model
{
use HasFactory;
protected $fillable = [
'device_id',
'position_id',
'geofence_id',
'traccar_event_id',
'type',
'event_time',
'attributes',
'acknowledged',
'acknowledged_by',
'acknowledged_at',
];
protected $casts = [
'event_time' => 'datetime',
'attributes' => 'array',
'acknowledged' => 'boolean',
'acknowledged_at' => 'datetime',
];
/**
* Accessor for event_type to map to type column
*/
public function getEventTypeAttribute()
{
return $this->type;
}
/**
* Get the device that triggered this event
*/
public function device(): BelongsTo
{
return $this->belongsTo(Device::class);
}
/**
* Get the position where this event occurred
*/
public function position(): BelongsTo
{
return $this->belongsTo(Position::class);
}
/**
* Get the geofence related to this event
*/
public function geofence(): BelongsTo
{
return $this->belongsTo(Geofence::class);
}
/**
* Get the user who acknowledged this event
*/
public function acknowledgedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'acknowledged_by');
}
/**
* Scope for unacknowledged events
*/
public function scopeUnacknowledged($query)
{
return $query->where('acknowledged', false);
}
/**
* Scope for events by type
*/
public function scopeByType($query, $type)
{
return $query->where('type', $type);
}
/**
* Get event severity level
*/
public function getSeverity(): string
{
return match ($this->type) {
'alarm' => 'high',
'geofenceEnter', 'geofenceExit' => 'medium',
'overspeed' => 'medium',
'deviceOnline', 'deviceOffline' => 'low',
default => 'low'
};
}
/**
* Get event color based on type
*/
public function getEventColor(): string
{
return match ($this->type) {
'alarm' => 'red',
'geofenceEnter' => 'green',
'geofenceExit' => 'orange',
'overspeed' => 'red',
'deviceOnline' => 'green',
'deviceOffline' => 'red',
default => 'gray'
};
}
/**
* Get human readable event type
*/
public function getReadableType(): string
{
return match ($this->type) {
'geofenceEnter' => 'Geofence Enter',
'geofenceExit' => 'Geofence Exit',
'overspeed' => 'Overspeed',
'deviceOnline' => 'Device Online',
'deviceOffline' => 'Device Offline',
'alarm' => 'Alarm',
default => ucfirst($this->type)
};
}
}

83
app/Models/Geofence.php Normal file
View File

@ -0,0 +1,83 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Geofence extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'traccar_geofence_id',
'traccar_id',
'name',
'description',
'area',
'coordinates',
'type',
'latitude',
'longitude',
'radius',
'attributes',
'calendar_id',
'is_active',
];
protected $casts = [
'latitude' => 'decimal:8',
'longitude' => 'decimal:8',
'radius' => 'decimal:2',
'attributes' => 'array',
'is_active' => 'boolean',
];
/**
* Get the user that owns the geofence
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Get devices assigned to this geofence
*/
public function devices(): BelongsToMany
{
return $this->belongsToMany(Device::class, 'device_geofences');
}
/**
* Get events related to this geofence
*/
public function events(): HasMany
{
return $this->hasMany(Event::class);
}
/**
* Scope for active geofences
*/
public function scopeActive($query)
{
return $query->where('is_active', true);
}
/**
* Get geofence type color
*/
public function getTypeColor(): string
{
return match ($this->type) {
'circle' => 'blue',
'polygon' => 'green',
default => 'gray'
};
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class NotificationPreference extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'email_notifications',
'sms_notifications',
'push_notifications',
'geofence_alerts',
'overspeed_alerts',
'offline_alerts',
'sos_alerts',
'maintenance_alerts',
];
protected $casts = [
'email_notifications' => 'boolean',
'sms_notifications' => 'boolean',
'push_notifications' => 'boolean',
'geofence_alerts' => 'boolean',
'overspeed_alerts' => 'boolean',
'offline_alerts' => 'boolean',
'sos_alerts' => 'boolean',
'maintenance_alerts' => 'boolean',
];
/**
* Get the user that owns these preferences
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@ -0,0 +1,102 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class NotificationSetting extends Model
{
protected $fillable = [
'user_id',
'email_enabled',
'sms_enabled',
'push_enabled',
'webhook_enabled',
'webhook_url',
'sms_provider',
'sms_config',
'event_types',
'device_filters',
'quiet_hours_start',
'quiet_hours_end',
'allowed_days',
];
protected $casts = [
'email_enabled' => 'boolean',
'sms_enabled' => 'boolean',
'push_enabled' => 'boolean',
'webhook_enabled' => 'boolean',
'sms_config' => 'array',
'event_types' => 'array',
'device_filters' => 'array',
'allowed_days' => 'array',
'quiet_hours_start' => 'datetime:H:i',
'quiet_hours_end' => 'datetime:H:i',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Check if notifications are allowed at the current time
*/
public function areNotificationsAllowed(): bool
{
// Check quiet hours
if ($this->quiet_hours_start && $this->quiet_hours_end) {
$now = now()->format('H:i');
$start = $this->quiet_hours_start->format('H:i');
$end = $this->quiet_hours_end->format('H:i');
if ($start <= $end) {
// Same day range
if ($now >= $start && $now <= $end) {
return false;
}
} else {
// Overnight range
if ($now >= $start || $now <= $end) {
return false;
}
}
}
// Check allowed days
if ($this->allowed_days && !empty($this->allowed_days)) {
$today = now()->dayOfWeek; // 0 = Sunday, 6 = Saturday
if (!in_array($today, $this->allowed_days)) {
return false;
}
}
return true;
}
/**
* Check if notifications are enabled for a specific event type
*/
public function isEventTypeEnabled(string $eventType): bool
{
if (!$this->event_types || empty($this->event_types)) {
return true; // If no filter, allow all
}
return in_array($eventType, $this->event_types);
}
/**
* Check if notifications are enabled for a specific device
*/
public function isDeviceEnabled(int $deviceId): bool
{
if (!$this->device_filters || empty($this->device_filters)) {
return true; // If no filter, allow all
}
return in_array($deviceId, $this->device_filters);
}
}

10
app/Models/Permission.php Normal file
View File

@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Permission extends Model
{
//
}

128
app/Models/Position.php Normal file
View File

@ -0,0 +1,128 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Position extends Model
{
use HasFactory;
protected $fillable = [
'device_id',
'traccar_position_id',
'protocol',
'device_time',
'fix_time',
'server_time',
'outdated',
'valid',
'latitude',
'longitude',
'altitude',
'speed',
'course',
'address',
'accuracy',
'network',
'attributes',
];
protected $casts = [
'device_time' => 'datetime',
'fix_time' => 'datetime',
'server_time' => 'datetime',
'outdated' => 'boolean',
'valid' => 'boolean',
'latitude' => 'decimal:8',
'longitude' => 'decimal:8',
'altitude' => 'decimal:2',
'speed' => 'decimal:2',
'course' => 'decimal:2',
'accuracy' => 'decimal:2',
'attributes' => 'array',
];
/**
* Get the device that owns this position
*/
public function device(): BelongsTo
{
return $this->belongsTo(Device::class);
}
/**
* Scope for recent positions
*/
public function scopeRecent($query, $hours = 24)
{
return $query->where('device_time', '>=', now()->subHours($hours));
}
/**
* Scope for valid positions
*/
public function scopeValid($query)
{
return $query->where('valid', true);
}
/**
* Get formatted speed with unit
*/
public function getFormattedSpeed(): string
{
$speedKmh = $this->speed * 1.852; // Convert knots to km/h
return round($speedKmh, 1) . ' km/h';
}
/**
* Get formatted coordinates
*/
public function getCoordinates(): string
{
return number_format($this->latitude, 6) . ', ' . number_format($this->longitude, 6);
}
/**
* Check if position is in a specific geofence
*/
public function isInGeofence(Geofence $geofence): bool
{
// Simple point-in-polygon check for circular geofences
if ($geofence->type === 'circle') {
$distance = $this->calculateDistance(
$this->latitude,
$this->longitude,
$geofence->latitude,
$geofence->longitude
);
return $distance <= $geofence->radius;
}
// For polygon geofences, you would implement a point-in-polygon algorithm
return false;
}
/**
* Calculate distance between two points in meters
*/
private function calculateDistance($lat1, $lon1, $lat2, $lon2): float
{
$earthRadius = 6371000; // Earth's radius in meters
$dLat = deg2rad($lat2 - $lat1);
$dLon = deg2rad($lon2 - $lon1);
$a = sin($dLat / 2) * sin($dLat / 2) +
cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
sin($dLon / 2) * sin($dLon / 2);
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
return $earthRadius * $c;
}
}

10
app/Models/Role.php Normal file
View File

@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
//
}

129
app/Models/Subscription.php Normal file
View File

@ -0,0 +1,129 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Carbon\Carbon;
class Subscription extends Model
{
protected $fillable = [
'user_id',
'plan_name',
'plan_type',
'price',
'currency',
'status',
'device_limit',
'user_limit',
'has_reports',
'has_api_access',
'has_priority_support',
'starts_at',
'ends_at',
'cancelled_at',
'payment_provider',
'external_id',
'features',
];
protected $casts = [
'price' => 'decimal:2',
'has_reports' => 'boolean',
'has_api_access' => 'boolean',
'has_priority_support' => 'boolean',
'starts_at' => 'datetime',
'ends_at' => 'datetime',
'cancelled_at' => 'datetime',
'features' => 'array',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Check if subscription is active
*/
public function isActive(): bool
{
if ($this->status !== 'active') {
return false;
}
if ($this->ends_at && $this->ends_at->isPast()) {
return false;
}
return $this->starts_at->isPast();
}
/**
* Check if subscription is expired
*/
public function isExpired(): bool
{
return $this->ends_at && $this->ends_at->isPast();
}
/**
* Check if subscription is cancelled
*/
public function isCancelled(): bool
{
return $this->status === 'cancelled' || $this->cancelled_at !== null;
}
/**
* Get days until expiry
*/
public function daysUntilExpiry(): ?int
{
if (!$this->ends_at) {
return null;
}
return max(0, now()->diffInDays($this->ends_at, false));
}
/**
* Check if user has exceeded device limit
*/
public function hasExceededDeviceLimit(): bool
{
return $this->user->devices()->count() > $this->device_limit;
}
/**
* Check if user has a specific feature
*/
public function hasFeature(string $feature): bool
{
return in_array($feature, $this->features ?? []);
}
/**
* Cancel subscription
*/
public function cancel(): void
{
$this->update([
'status' => 'cancelled',
'cancelled_at' => now(),
]);
}
/**
* Renew subscription
*/
public function renew(Carbon $newEndDate): void
{
$this->update([
'status' => 'active',
'ends_at' => $newEndDate,
'cancelled_at' => null,
]);
}
}

186
app/Models/User.php Normal file
View File

@ -0,0 +1,186 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Str;
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable, HasRoles;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'name',
'email',
'password',
'traccar_user_id',
'traccar_username',
'traccar_password',
'phone',
'timezone',
'is_active',
'is_admin',
'status',
'last_login_at',
'company',
'notes',
'created_by',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'password',
'remember_token',
'traccar_password',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'is_active' => 'boolean',
'is_admin' => 'boolean',
'last_login_at' => 'datetime',
];
}
/**
* Get devices assigned to this user
*/
public function devices()
{
return $this->hasMany(Device::class);
}
/**
* Get device groups this user has access to
*/
public function deviceGroups()
{
return $this->belongsToMany(DeviceGroup::class, 'device_group_user', 'user_id', 'device_group_id');
}
/**
* Get user's driver profile
*/
public function driver()
{
return $this->hasOne(Driver::class);
}
/**
* Get notification preferences
*/
public function notificationPreferences()
{
return $this->hasOne(NotificationPreference::class);
}
/**
* Get notification settings (new advanced system)
*/
public function notificationSettings()
{
return $this->hasOne(NotificationSetting::class);
}
/**
* Get user's subscription
*/
public function subscription()
{
return $this->hasOne(Subscription::class);
}
/**
* Get user's active subscription
*/
public function activeSubscription()
{
return $this->hasOne(Subscription::class)->where('status', 'active');
}
/**
* Get users created by this user (admin functionality)
*/
public function createdUsers()
{
return $this->hasMany(User::class, 'created_by');
}
/**
* Get user who created this user
*/
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
/**
* Get commands sent by this user
*/
public function commands()
{
return $this->hasMany(Command::class);
}
/**
* Check if user is admin
*/
public function isAdmin(): bool
{
return $this->is_admin || $this->hasRole('admin');
}
/**
* Check if user is active
*/
public function isActive(): bool
{
return $this->status === 'active' && $this->is_active;
}
/**
* Get user's device limit based on subscription
*/
public function getDeviceLimit(): int
{
if ($this->subscription && $this->subscription->isActive()) {
return $this->subscription->device_limit;
}
return 1; // Default limit
}
/**
* Get the user's initials
*/
public function initials(): string
{
return Str::of($this->name)
->explode(' ')
->take(2)
->map(fn ($word) => Str::substr($word, 0, 1))
->implode('');
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Event;
use Illuminate\Auth\Events\Login;
use App\Listeners\UpdateLastLoginTime;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
// Register event listeners
Event::listen(Login::class, UpdateLastLoginTime::class);
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Livewire\Volt\Volt;
class VoltServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
//
}
/**
* Bootstrap services.
*/
public function boot(): void
{
Volt::mount([
config('livewire.view_path', resource_path('views/livewire')),
resource_path('views/pages'),
]);
}
}

134
app/Services/MapService.php Normal file
View File

@ -0,0 +1,134 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Config;
class MapService
{
public function getDefaultProvider(): string
{
$default = Config::get('services.maps.default_provider', 'openstreetmap');
// Handle legacy config value
if ($default === 'leaflet') {
return 'openstreetmap';
}
return $default;
}
public function getAvailableProviders(): array
{
$providers = Config::get('services.maps.providers', []);
// Filter out providers that require API keys but don't have them
return array_filter($providers, function ($provider, $key) {
if (!$provider['requires_api_key']) {
return true;
}
return !empty($provider['api_key']) && ($provider['enabled'] ?? false);
}, ARRAY_FILTER_USE_BOTH);
}
public function getProviderConfig(string $provider): ?array
{
$providers = Config::get('services.maps.providers', []);
return $providers[$provider] ?? null;
}
public function isProviderEnabled(string $provider): bool
{
$config = $this->getProviderConfig($provider);
if (!$config) {
return false;
}
if (!$config['requires_api_key']) {
return true;
}
return !empty($config['api_key']) && ($config['enabled'] ?? false);
}
public function getMapStyles(string $provider): array
{
$config = $this->getProviderConfig($provider);
switch ($provider) {
case 'openstreetmap':
return [
'standard' => 'Standard',
];
case 'google':
return [
'roadmap' => 'Roadmap',
'satellite' => 'Satellite',
'hybrid' => 'Hybrid',
'terrain' => 'Terrain',
];
case 'mapbox':
return [
'streets-v11' => 'Streets',
'satellite-v9' => 'Satellite',
'outdoors-v11' => 'Outdoors',
'light-v10' => 'Light',
'dark-v10' => 'Dark',
];
case 'cartodb':
return [
'light' => 'Light',
'dark' => 'Dark',
'voyager' => 'Voyager',
];
case 'satellite':
return [
'satellite' => 'Satellite',
];
default:
return ['standard' => 'Standard'];
}
}
public function getMapConfig(string $provider = null): array
{
$provider = $provider ?: $this->getDefaultProvider();
$config = $this->getProviderConfig($provider);
if (!$config) {
throw new \InvalidArgumentException("Map provider '{$provider}' not found");
}
return [
'provider' => $provider,
'config' => $config,
'styles' => $this->getMapStyles($provider),
'enabled' => $this->isProviderEnabled($provider),
];
}
public function getAllProvidersStatus(): array
{
$providers = Config::get('services.maps.providers', []);
$status = [];
foreach ($providers as $key => $provider) {
$status[$key] = [
'name' => $provider['name'],
'free' => $provider['free'],
'requires_api_key' => $provider['requires_api_key'],
'enabled' => $this->isProviderEnabled($key),
'has_api_key' => !$provider['requires_api_key'] || !empty($provider['api_key']),
];
}
return $status;
}
}

View File

@ -0,0 +1,385 @@
<?php
namespace App\Services;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
class TraccarService
{
private Client $client;
private string $baseUrl;
private string $username;
private string $password;
public function __construct()
{
$this->baseUrl = config('services.traccar.api_url');
$this->username = config('services.traccar.admin_username');
$this->password = config('services.traccar.admin_password');
$this->client = new Client([
'timeout' => 30,
'headers' => [
'Accept' => 'application/json',
'User-Agent' => 'Laravel-TraccarService/1.0',
],
'verify' => false, // For development only
]);
}
/**
* Make authenticated request to Traccar API
*/
private function makeRequest(string $method, string $endpoint, array $data = []): array
{
try {
$fullUrl = $this->baseUrl . $endpoint;
$options = [
'auth' => [$this->username, $this->password, 'basic'],
'headers' => [
'Accept' => 'application/json',
'User-Agent' => 'Laravel-TraccarService/1.0',
],
];
if (!empty($data)) {
$options['json'] = $data;
$options['headers']['Content-Type'] = 'application/json';
}
$response = $this->client->request($method, $fullUrl, $options);
$responseBody = $response->getBody()->getContents();
$result = json_decode($responseBody, true);
Log::info("Traccar API Request", [
'method' => $method,
'endpoint' => $endpoint,
'full_url' => $fullUrl,
'status' => $response->getStatusCode(),
'response_length' => strlen($responseBody),
'response_preview' => substr($responseBody, 0, 200),
'content_type' => $response->getHeaderLine('Content-Type'),
]);
// Handle empty responses or null JSON decode results
if ($result === null && !empty($responseBody)) {
Log::warning("Failed to decode JSON response", [
'raw_response' => $responseBody,
'content_type' => $response->getHeaderLine('Content-Type'),
]);
return [];
}
return $result ?? [];
} catch (GuzzleException $e) {
Log::error("Traccar API Error", [
'method' => $method,
'endpoint' => $endpoint,
'full_url' => $this->baseUrl . $endpoint,
'error' => $e->getMessage(),
]);
throw $e;
}
}
// ===== SERVER METHODS =====
/**
* Get server information
*/
public function getServerInfo(): array
{
return $this->makeRequest('GET', '/server');
}
// ===== USER METHODS =====
/**
* Get all users
*/
public function getUsers(): array
{
return $this->makeRequest('GET', '/users');
}
/**
* Create a new user in Traccar
*/
public function createUser(array $userData): array
{
return $this->makeRequest('POST', '/users', $userData);
}
/**
* Update user in Traccar
*/
public function updateUser(int $userId, array $userData): array
{
return $this->makeRequest('PUT', "/users/{$userId}", $userData);
}
/**
* Delete user from Traccar
*/
public function deleteUser(int $userId): bool
{
$this->makeRequest('DELETE', "/users/{$userId}");
return true;
}
// ===== DEVICE METHODS =====
/**
* Get all devices
*/
public function getDevices(?int $userId = null): array
{
$endpoint = '/devices';
if ($userId) {
$endpoint .= "?userId={$userId}";
}
return $this->makeRequest('GET', $endpoint);
}
/**
* Create a new device
*/
public function createDevice(array $deviceData): array
{
return $this->makeRequest('POST', '/devices', $deviceData);
}
/**
* Update device
*/
public function updateDevice(int $deviceId, array $deviceData): array
{
return $this->makeRequest('PUT', "/devices/{$deviceId}", $deviceData);
}
/**
* Delete device
*/
public function deleteDevice(int $deviceId): bool
{
$this->makeRequest('DELETE', "/devices/{$deviceId}");
return true;
}
// ===== POSITION METHODS =====
/**
* Get latest positions for all devices
*/
public function getPositions(): array
{
return $this->makeRequest('GET', '/positions');
}
/**
* Get latest positions for specific devices
*/
public function getDevicePositions(int $deviceId, ?string $from = null, ?string $to = null): array
{
$params = ['deviceId' => $deviceId];
if ($from) $params['from'] = $from;
if ($to) $params['to'] = $to;
$query = http_build_query($params);
$endpoint = "/positions?{$query}";
return $this->makeRequest('GET', $endpoint);
}
/**
* Get latest positions for multiple devices
*/
public function getMultipleDevicePositions(array $deviceIds): array
{
if (empty($deviceIds)) {
return [];
}
// Build query string for multiple device IDs
$params = [];
foreach ($deviceIds as $deviceId) {
$params[] = "deviceId={$deviceId}";
}
$query = implode('&', $params);
$endpoint = "/positions?{$query}";
return $this->makeRequest('GET', $endpoint);
}
/**
* Get real-time positions (latest for all user devices)
*/
public function getRealTimePositions(array $deviceIds = []): array
{
if (empty($deviceIds)) {
// Get latest positions for all devices
return $this->getPositions();
}
// Get positions for specific devices
return $this->getMultipleDevicePositions($deviceIds);
}
// ===== GEOFENCE METHODS =====
/**
* Get all geofences
*/
public function getGeofences(): array
{
return $this->makeRequest('GET', '/geofences');
}
/**
* Create geofence
*/
public function createGeofence(array $geofenceData): array
{
return $this->makeRequest('POST', '/geofences', $geofenceData);
}
/**
* Update geofence
*/
public function updateGeofence(int $geofenceId, array $geofenceData): array
{
return $this->makeRequest('PUT', "/geofences/{$geofenceId}", $geofenceData);
}
/**
* Delete geofence
*/
public function deleteGeofence(int $geofenceId): bool
{
$this->makeRequest('DELETE', "/geofences/{$geofenceId}");
return true;
}
// ===== EVENT METHODS =====
/**
* Get events
*/
public function getEvents(?int $deviceId = null, ?string $type = null): array
{
$params = [];
if ($deviceId) $params['deviceId'] = $deviceId;
if ($type) $params['type'] = $type;
$query = http_build_query($params);
$endpoint = '/events' . ($query ? "?{$query}" : '');
return $this->makeRequest('GET', $endpoint);
}
// ===== REPORT METHODS =====
/**
* Get trip reports
*/
public function getTripReports(array $deviceIds, string $from, string $to): array
{
$params = [
'deviceId' => $deviceIds,
'from' => $from,
'to' => $to
];
$query = http_build_query($params);
return $this->makeRequest('GET', "/reports/trips?{$query}");
}
/**
* Get summary reports
*/
public function getSummaryReports(array $deviceIds, string $from, string $to): array
{
$params = [
'deviceId' => $deviceIds,
'from' => $from,
'to' => $to
];
$query = http_build_query($params);
return $this->makeRequest('GET', "/reports/summary?{$query}");
}
// ===== COMMAND METHODS =====
/**
* Send command to device
*/
public function sendCommand(array $commandData): array
{
return $this->makeRequest('POST', '/commands/send', $commandData);
}
/**
* Get available command types
*/
public function getCommandTypes(): array
{
return $this->makeRequest('GET', '/commands/types');
}
// ===== PERMISSION METHODS =====
/**
* Link user to device
*/
public function linkUserDevice(int $userId, int $deviceId): bool
{
$this->makeRequest('POST', '/permissions', [
'userId' => $userId,
'deviceId' => $deviceId
]);
return true;
}
/**
* Unlink user from device
*/
public function unlinkUserDevice(int $userId, int $deviceId): bool
{
$this->makeRequest('DELETE', '/permissions', [
'userId' => $userId,
'deviceId' => $deviceId
]);
return true;
}
// ===== UTILITY METHODS =====
/**
* Test API connection
*/
public function testConnection(): bool
{
try {
$this->getServerInfo();
return true;
} catch (\Exception $e) {
return false;
}
}
/**
* Clear cached data
*/
public function clearCache(): void
{
Cache::forget('traccar_positions');
}
}

18
artisan Executable file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env php
<?php
use Illuminate\Foundation\Application;
use Symfony\Component\Console\Input\ArgvInput;
define('LARAVEL_START', microtime(true));
// Register the Composer autoloader...
require __DIR__.'/vendor/autoload.php';
// Bootstrap Laravel and handle the command...
/** @var Application $app */
$app = require_once __DIR__.'/bootstrap/app.php';
$status = $app->handleCommand(new ArgvInput);
exit($status);

28
bootstrap/app.php Normal file
View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'role' => \Spatie\Permission\Middleware\RoleMiddleware::class,
'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class,
'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class,
'active' => \App\Http\Middleware\EnsureUserIsActive::class,
]);
// Apply the active user middleware to all authenticated routes
$middleware->web(append: [
\App\Http\Middleware\EnsureUserIsActive::class,
]);
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();

2
bootstrap/cache/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

6
bootstrap/providers.php Normal file
View File

@ -0,0 +1,6 @@
<?php
return [
App\Providers\AppServiceProvider::class,
App\Providers\VoltServiceProvider::class,
];

85
composer.json Normal file
View File

@ -0,0 +1,85 @@
{
"$schema": "https://getcomposer.org/schema.json",
"name": "laravel/livewire-starter-kit",
"type": "project",
"description": "The official Laravel starter kit for Livewire.",
"keywords": [
"laravel",
"framework"
],
"license": "MIT",
"require": {
"php": "^8.2",
"guzzlehttp/guzzle": "^7.10",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1",
"livewire/flux": "^2.3",
"livewire/volt": "^1.7.0",
"spatie/laravel-permission": "^6.21"
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.16",
"fakerphp/faker": "^1.23",
"laravel/boost": "^1.1",
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.18",
"laravel/sail": "^1.41",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"pestphp/pest": "^4.0",
"pestphp/pest-plugin-laravel": "^4.0"
},
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi",
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
"@php artisan migrate --graceful --ansi"
],
"dev": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
],
"test": [
"@php artisan config:clear --ansi",
"@php artisan test"
]
},
"extra": {
"laravel": {
"dont-discover": []
}
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true,
"php-http/discovery": true
}
},
"minimum-stability": "stable",
"prefer-stable": true
}

9938
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

126
config/app.php Normal file
View File

@ -0,0 +1,126 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Application Name
|--------------------------------------------------------------------------
|
| This value is the name of your application, which will be used when the
| framework needs to place the application's name in a notification or
| other UI elements where an application name needs to be displayed.
|
*/
'name' => env('APP_NAME', 'Laravel'),
/*
|--------------------------------------------------------------------------
| Application Environment
|--------------------------------------------------------------------------
|
| This value determines the "environment" your application is currently
| running in. This may determine how you prefer to configure various
| services the application utilizes. Set this in your ".env" file.
|
*/
'env' => env('APP_ENV', 'production'),
/*
|--------------------------------------------------------------------------
| Application Debug Mode
|--------------------------------------------------------------------------
|
| When your application is in debug mode, detailed error messages with
| stack traces will be shown on every error that occurs within your
| application. If disabled, a simple generic error page is shown.
|
*/
'debug' => (bool) env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
|
| This URL is used by the console to properly generate URLs when using
| the Artisan command line tool. You should set this to the root of
| the application so that it's available within Artisan commands.
|
*/
'url' => env('APP_URL', 'http://localhost'),
/*
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. The timezone
| is set to "UTC" by default as it is suitable for most use cases.
|
*/
'timezone' => 'UTC',
/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by Laravel's translation / localization methods. This option can be
| set to any locale for which you plan to have translation strings.
|
*/
'locale' => env('APP_LOCALE', 'en'),
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is utilized by Laravel's encryption services and should be set
| to a random, 32 character string to ensure that all encrypted values
| are secure. You should do this prior to deploying the application.
|
*/
'cipher' => 'AES-256-CBC',
'key' => env('APP_KEY'),
'previous_keys' => [
...array_filter(
explode(',', env('APP_PREVIOUS_KEYS', ''))
),
],
/*
|--------------------------------------------------------------------------
| Maintenance Mode Driver
|--------------------------------------------------------------------------
|
| These configuration options determine the driver used to determine and
| manage Laravel's "maintenance mode" status. The "cache" driver will
| allow maintenance mode to be controlled across multiple machines.
|
| Supported drivers: "file", "cache"
|
*/
'maintenance' => [
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
'store' => env('APP_MAINTENANCE_STORE', 'database'),
],
];

115
config/auth.php Normal file
View File

@ -0,0 +1,115 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option defines the default authentication "guard" and password
| reset "broker" for your application. You may change these values
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [
'guard' => env('AUTH_GUARD', 'web'),
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| which utilizes session storage plus the Eloquent user provider.
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| Supported: "session"
|
*/
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| If you have multiple user tables or models you may configure multiple
| providers to represent the model / table. These providers may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => env('AUTH_MODEL', App\Models\User::class),
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
/*
|--------------------------------------------------------------------------
| Resetting Passwords
|--------------------------------------------------------------------------
|
| These configuration options specify the behavior of Laravel's password
| reset functionality, including the table utilized for token storage
| and the user provider that is invoked to actually retrieve users.
|
| The expiry time is the number of minutes that each reset token will be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
| The throttle setting is the number of seconds a user must wait before
| generating more password reset tokens. This prevents the user from
| quickly generating a very large amount of password reset tokens.
|
*/
'passwords' => [
'users' => [
'provider' => 'users',
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
'expire' => 60,
'throttle' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Password Confirmation Timeout
|--------------------------------------------------------------------------
|
| Here you may define the amount of seconds before a password confirmation
| window expires and users are asked to re-enter their password via the
| confirmation screen. By default, the timeout lasts for three hours.
|
*/
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
];

32
config/boost.php Normal file
View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
return [
/*
|--------------------------------------------------------------------------
| Boost Master Switch
|--------------------------------------------------------------------------
|
| This option may be used to disable all Boost functionality - which
| will prevent Boost's routes from being registered and will also
| disable Boost's browser logging functionality from operating.
|
*/
'enabled' => env('BOOST_ENABLED', true),
/*
|--------------------------------------------------------------------------
| Boost Browser Logs Watcher
|--------------------------------------------------------------------------
|
| The following option may be used to enable or disable the browser logs
| watcher feature within Laravel Boost. The log watcher will read any
| errors within the browser's console to give Boost better context.
*/
'browser_logs_watcher' => env('BOOST_BROWSER_LOGS_WATCHER', true),
];

108
config/cache.php Normal file
View File

@ -0,0 +1,108 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Cache Store
|--------------------------------------------------------------------------
|
| This option controls the default cache store that will be used by the
| framework. This connection is utilized if another isn't explicitly
| specified when running a cache operation inside the application.
|
*/
'default' => env('CACHE_STORE', 'database'),
/*
|--------------------------------------------------------------------------
| Cache Stores
|--------------------------------------------------------------------------
|
| Here you may define all of the cache "stores" for your application as
| well as their drivers. You may even define multiple stores for the
| same cache driver to group types of items stored in your caches.
|
| Supported drivers: "array", "database", "file", "memcached",
| "redis", "dynamodb", "octane", "null"
|
*/
'stores' => [
'array' => [
'driver' => 'array',
'serialize' => false,
],
'database' => [
'driver' => 'database',
'connection' => env('DB_CACHE_CONNECTION'),
'table' => env('DB_CACHE_TABLE', 'cache'),
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
],
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache/data'),
'lock_path' => storage_path('framework/cache/data'),
],
'memcached' => [
'driver' => 'memcached',
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
'sasl' => [
env('MEMCACHED_USERNAME'),
env('MEMCACHED_PASSWORD'),
],
'options' => [
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
],
'servers' => [
[
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
'port' => env('MEMCACHED_PORT', 11211),
'weight' => 100,
],
],
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
],
'dynamodb' => [
'driver' => 'dynamodb',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
'endpoint' => env('DYNAMODB_ENDPOINT'),
],
'octane' => [
'driver' => 'octane',
],
],
/*
|--------------------------------------------------------------------------
| Cache Key Prefix
|--------------------------------------------------------------------------
|
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
| stores, there might be other applications using the same cache. For
| that reason, you may prefix every cache key to avoid collisions.
|
*/
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'),
];

182
config/database.php Normal file
View File

@ -0,0 +1,182 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Database Connection Name
|--------------------------------------------------------------------------
|
| Here you may specify which of the database connections below you wish
| to use as your default connection for database operations. This is
| the connection which will be utilized unless another connection
| is explicitly specified when you execute a query / statement.
|
*/
'default' => env('DB_CONNECTION', 'sqlite'),
/*
|--------------------------------------------------------------------------
| Database Connections
|--------------------------------------------------------------------------
|
| Below are all of the database connections defined for your application.
| An example configuration is provided for each database system which
| is supported by Laravel. You're free to add / remove connections.
|
*/
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DB_URL'),
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
'busy_timeout' => null,
'journal_mode' => null,
'synchronous' => null,
],
'mysql' => [
'driver' => 'mysql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'mariadb' => [
'driver' => 'mariadb',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => 'prefer',
],
'sqlsrv' => [
'driver' => 'sqlsrv',
'url' => env('DB_URL'),
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', '1433'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
],
],
/*
|--------------------------------------------------------------------------
| Migration Repository Table
|--------------------------------------------------------------------------
|
| This table keeps track of all the migrations that have already run for
| your application. Using this information, we can determine which of
| the migrations on disk haven't actually been run on the database.
|
*/
'migrations' => [
'table' => 'migrations',
'update_date_on_publish' => true,
],
/*
|--------------------------------------------------------------------------
| Redis Databases
|--------------------------------------------------------------------------
|
| Redis is an open source, fast, and advanced key-value store that also
| provides a richer body of commands than a typical key-value system
| such as Memcached. You may define your connection settings here.
|
*/
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
'persistent' => env('REDIS_PERSISTENT', false),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
'max_retries' => env('REDIS_MAX_RETRIES', 3),
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
'max_retries' => env('REDIS_MAX_RETRIES', 3),
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
],
],
];

338
config/debugbar.php Normal file
View File

@ -0,0 +1,338 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Debugbar Settings
|--------------------------------------------------------------------------
|
| Debugbar is enabled by default, when debug is set to true in app.php.
| You can override the value by setting enable to true or false instead of null.
|
| You can provide an array of URI's that must be ignored (eg. 'api/*')
|
*/
'enabled' => env('DEBUGBAR_ENABLED', null),
'hide_empty_tabs' => env('DEBUGBAR_HIDE_EMPTY_TABS', true), // Hide tabs until they have content
'except' => [
'telescope*',
'horizon*',
],
/*
|--------------------------------------------------------------------------
| Storage settings
|--------------------------------------------------------------------------
|
| Debugbar stores data for session/ajax requests.
| You can disable this, so the debugbar stores data in headers/session,
| but this can cause problems with large data collectors.
| By default, file storage (in the storage folder) is used. Redis and PDO
| can also be used. For PDO, run the package migrations first.
|
| Warning: Enabling storage.open will allow everyone to access previous
| request, do not enable open storage in publicly available environments!
| Specify a callback if you want to limit based on IP or authentication.
| Leaving it to null will allow localhost only.
*/
'storage' => [
'enabled' => env('DEBUGBAR_STORAGE_ENABLED', true),
'open' => env('DEBUGBAR_OPEN_STORAGE'), // bool/callback.
'driver' => env('DEBUGBAR_STORAGE_DRIVER', 'file'), // redis, file, pdo, socket, custom
'path' => env('DEBUGBAR_STORAGE_PATH', storage_path('debugbar')), // For file driver
'connection' => env('DEBUGBAR_STORAGE_CONNECTION', null), // Leave null for default connection (Redis/PDO)
'provider' => env('DEBUGBAR_STORAGE_PROVIDER', ''), // Instance of StorageInterface for custom driver
'hostname' => env('DEBUGBAR_STORAGE_HOSTNAME', '127.0.0.1'), // Hostname to use with the "socket" driver
'port' => env('DEBUGBAR_STORAGE_PORT', 2304), // Port to use with the "socket" driver
],
/*
|--------------------------------------------------------------------------
| Editor
|--------------------------------------------------------------------------
|
| Choose your preferred editor to use when clicking file name.
|
| Supported: "phpstorm", "vscode", "vscode-insiders", "vscode-remote",
| "vscode-insiders-remote", "vscodium", "textmate", "emacs",
| "sublime", "atom", "nova", "macvim", "idea", "netbeans",
| "xdebug", "espresso"
|
*/
'editor' => env('DEBUGBAR_EDITOR') ?: env('IGNITION_EDITOR', 'phpstorm'),
/*
|--------------------------------------------------------------------------
| Remote Path Mapping
|--------------------------------------------------------------------------
|
| If you are using a remote dev server, like Laravel Homestead, Docker, or
| even a remote VPS, it will be necessary to specify your path mapping.
|
| Leaving one, or both of these, empty or null will not trigger the remote
| URL changes and Debugbar will treat your editor links as local files.
|
| "remote_sites_path" is an absolute base path for your sites or projects
| in Homestead, Vagrant, Docker, or another remote development server.
|
| Example value: "/home/vagrant/Code"
|
| "local_sites_path" is an absolute base path for your sites or projects
| on your local computer where your IDE or code editor is running on.
|
| Example values: "/Users/<name>/Code", "C:\Users\<name>\Documents\Code"
|
*/
'remote_sites_path' => env('DEBUGBAR_REMOTE_SITES_PATH'),
'local_sites_path' => env('DEBUGBAR_LOCAL_SITES_PATH', env('IGNITION_LOCAL_SITES_PATH')),
/*
|--------------------------------------------------------------------------
| Vendors
|--------------------------------------------------------------------------
|
| Vendor files are included by default, but can be set to false.
| This can also be set to 'js' or 'css', to only include javascript or css vendor files.
| Vendor files are for css: font-awesome (including fonts) and highlight.js (css files)
| and for js: jquery and highlight.js
| So if you want syntax highlighting, set it to true.
| jQuery is set to not conflict with existing jQuery scripts.
|
*/
'include_vendors' => env('DEBUGBAR_INCLUDE_VENDORS', true),
/*
|--------------------------------------------------------------------------
| Capture Ajax Requests
|--------------------------------------------------------------------------
|
| The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors),
| you can use this option to disable sending the data through the headers.
|
| Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools.
|
| Note for your request to be identified as ajax requests they must either send the header
| X-Requested-With with the value XMLHttpRequest (most JS libraries send this), or have application/json as a Accept header.
|
| By default `ajax_handler_auto_show` is set to true allowing ajax requests to be shown automatically in the Debugbar.
| Changing `ajax_handler_auto_show` to false will prevent the Debugbar from reloading.
|
| You can defer loading the dataset, so it will be loaded with ajax after the request is done. (Experimental)
*/
'capture_ajax' => env('DEBUGBAR_CAPTURE_AJAX', true),
'add_ajax_timing' => env('DEBUGBAR_ADD_AJAX_TIMING', false),
'ajax_handler_auto_show' => env('DEBUGBAR_AJAX_HANDLER_AUTO_SHOW', true),
'ajax_handler_enable_tab' => env('DEBUGBAR_AJAX_HANDLER_ENABLE_TAB', true),
'defer_datasets' => env('DEBUGBAR_DEFER_DATASETS', false),
/*
|--------------------------------------------------------------------------
| Custom Error Handler for Deprecated warnings
|--------------------------------------------------------------------------
|
| When enabled, the Debugbar shows deprecated warnings for Symfony components
| in the Messages tab.
|
*/
'error_handler' => env('DEBUGBAR_ERROR_HANDLER', false),
/*
|--------------------------------------------------------------------------
| Clockwork integration
|--------------------------------------------------------------------------
|
| The Debugbar can emulate the Clockwork headers, so you can use the Chrome
| Extension, without the server-side code. It uses Debugbar collectors instead.
|
*/
'clockwork' => env('DEBUGBAR_CLOCKWORK', false),
/*
|--------------------------------------------------------------------------
| DataCollectors
|--------------------------------------------------------------------------
|
| Enable/disable DataCollectors
|
*/
'collectors' => [
'phpinfo' => env('DEBUGBAR_COLLECTORS_PHPINFO', false), // Php version
'messages' => env('DEBUGBAR_COLLECTORS_MESSAGES', true), // Messages
'time' => env('DEBUGBAR_COLLECTORS_TIME', true), // Time Datalogger
'memory' => env('DEBUGBAR_COLLECTORS_MEMORY', true), // Memory usage
'exceptions' => env('DEBUGBAR_COLLECTORS_EXCEPTIONS', true), // Exception displayer
'log' => env('DEBUGBAR_COLLECTORS_LOG', true), // Logs from Monolog (merged in messages if enabled)
'db' => env('DEBUGBAR_COLLECTORS_DB', true), // Show database (PDO) queries and bindings
'views' => env('DEBUGBAR_COLLECTORS_VIEWS', true), // Views with their data
'route' => env('DEBUGBAR_COLLECTORS_ROUTE', false), // Current route information
'auth' => env('DEBUGBAR_COLLECTORS_AUTH', false), // Display Laravel authentication status
'gate' => env('DEBUGBAR_COLLECTORS_GATE', true), // Display Laravel Gate checks
'session' => env('DEBUGBAR_COLLECTORS_SESSION', false), // Display session data
'symfony_request' => env('DEBUGBAR_COLLECTORS_SYMFONY_REQUEST', true), // Only one can be enabled..
'mail' => env('DEBUGBAR_COLLECTORS_MAIL', true), // Catch mail messages
'laravel' => env('DEBUGBAR_COLLECTORS_LARAVEL', true), // Laravel version and environment
'events' => env('DEBUGBAR_COLLECTORS_EVENTS', false), // All events fired
'default_request' => env('DEBUGBAR_COLLECTORS_DEFAULT_REQUEST', false), // Regular or special Symfony request logger
'logs' => env('DEBUGBAR_COLLECTORS_LOGS', false), // Add the latest log messages
'files' => env('DEBUGBAR_COLLECTORS_FILES', false), // Show the included files
'config' => env('DEBUGBAR_COLLECTORS_CONFIG', false), // Display config settings
'cache' => env('DEBUGBAR_COLLECTORS_CACHE', false), // Display cache events
'models' => env('DEBUGBAR_COLLECTORS_MODELS', true), // Display models
'livewire' => env('DEBUGBAR_COLLECTORS_LIVEWIRE', true), // Display Livewire (when available)
'jobs' => env('DEBUGBAR_COLLECTORS_JOBS', false), // Display dispatched jobs
'pennant' => env('DEBUGBAR_COLLECTORS_PENNANT', false), // Display Pennant feature flags
],
/*
|--------------------------------------------------------------------------
| Extra options
|--------------------------------------------------------------------------
|
| Configure some DataCollectors
|
*/
'options' => [
'time' => [
'memory_usage' => env('DEBUGBAR_OPTIONS_TIME_MEMORY_USAGE', false), // Calculated by subtracting memory start and end, it may be inaccurate
],
'messages' => [
'trace' => env('DEBUGBAR_OPTIONS_MESSAGES_TRACE', true), // Trace the origin of the debug message
'capture_dumps' => env('DEBUGBAR_OPTIONS_MESSAGES_CAPTURE_DUMPS', false), // Capture laravel `dump();` as message
],
'memory' => [
'reset_peak' => env('DEBUGBAR_OPTIONS_MEMORY_RESET_PEAK', false), // run memory_reset_peak_usage before collecting
'with_baseline' => env('DEBUGBAR_OPTIONS_MEMORY_WITH_BASELINE', false), // Set boot memory usage as memory peak baseline
'precision' => (int) env('DEBUGBAR_OPTIONS_MEMORY_PRECISION', 0), // Memory rounding precision
],
'auth' => [
'show_name' => env('DEBUGBAR_OPTIONS_AUTH_SHOW_NAME', true), // Also show the users name/email in the debugbar
'show_guards' => env('DEBUGBAR_OPTIONS_AUTH_SHOW_GUARDS', true), // Show the guards that are used
],
'gate' => [
'trace' => false, // Trace the origin of the Gate checks
],
'db' => [
'with_params' => env('DEBUGBAR_OPTIONS_WITH_PARAMS', true), // Render SQL with the parameters substituted
'exclude_paths' => [ // Paths to exclude entirely from the collector
//'vendor/laravel/framework/src/Illuminate/Session', // Exclude sessions queries
],
'backtrace' => env('DEBUGBAR_OPTIONS_DB_BACKTRACE', true), // Use a backtrace to find the origin of the query in your files.
'backtrace_exclude_paths' => [], // Paths to exclude from backtrace. (in addition to defaults)
'timeline' => env('DEBUGBAR_OPTIONS_DB_TIMELINE', false), // Add the queries to the timeline
'duration_background' => env('DEBUGBAR_OPTIONS_DB_DURATION_BACKGROUND', true), // Show shaded background on each query relative to how long it took to execute.
'explain' => [ // Show EXPLAIN output on queries
'enabled' => env('DEBUGBAR_OPTIONS_DB_EXPLAIN_ENABLED', false),
],
'hints' => env('DEBUGBAR_OPTIONS_DB_HINTS', false), // Show hints for common mistakes
'show_copy' => env('DEBUGBAR_OPTIONS_DB_SHOW_COPY', true), // Show copy button next to the query,
'slow_threshold' => env('DEBUGBAR_OPTIONS_DB_SLOW_THRESHOLD', false), // Only track queries that last longer than this time in ms
'memory_usage' => env('DEBUGBAR_OPTIONS_DB_MEMORY_USAGE', false), // Show queries memory usage
'soft_limit' => (int) env('DEBUGBAR_OPTIONS_DB_SOFT_LIMIT', 100), // After the soft limit, no parameters/backtrace are captured
'hard_limit' => (int) env('DEBUGBAR_OPTIONS_DB_HARD_LIMIT', 500), // After the hard limit, queries are ignored
],
'mail' => [
'timeline' => env('DEBUGBAR_OPTIONS_MAIL_TIMELINE', true), // Add mails to the timeline
'show_body' => env('DEBUGBAR_OPTIONS_MAIL_SHOW_BODY', true),
],
'views' => [
'timeline' => env('DEBUGBAR_OPTIONS_VIEWS_TIMELINE', true), // Add the views to the timeline
'data' => env('DEBUGBAR_OPTIONS_VIEWS_DATA', false), // True for all data, 'keys' for only names, false for no parameters.
'group' => (int) env('DEBUGBAR_OPTIONS_VIEWS_GROUP', 50), // Group duplicate views. Pass value to auto-group, or true/false to force
'inertia_pages' => env('DEBUGBAR_OPTIONS_VIEWS_INERTIA_PAGES', 'js/Pages'), // Path for Inertia views
'exclude_paths' => [ // Add the paths which you don't want to appear in the views
'vendor/filament' // Exclude Filament components by default
],
],
'route' => [
'label' => env('DEBUGBAR_OPTIONS_ROUTE_LABEL', true), // Show complete route on bar
],
'session' => [
'hiddens' => [], // Hides sensitive values using array paths
],
'symfony_request' => [
'label' => env('DEBUGBAR_OPTIONS_SYMFONY_REQUEST_LABEL', true), // Show route on bar
'hiddens' => [], // Hides sensitive values using array paths, example: request_request.password
],
'events' => [
'data' => env('DEBUGBAR_OPTIONS_EVENTS_DATA', false), // Collect events data, listeners
'excluded' => [], // Example: ['eloquent.*', 'composing', Illuminate\Cache\Events\CacheHit::class]
],
'logs' => [
'file' => env('DEBUGBAR_OPTIONS_LOGS_FILE', null),
],
'cache' => [
'values' => env('DEBUGBAR_OPTIONS_CACHE_VALUES', true), // Collect cache values
],
],
/*
|--------------------------------------------------------------------------
| Inject Debugbar in Response
|--------------------------------------------------------------------------
|
| Usually, the debugbar is added just before </body>, by listening to the
| Response after the App is done. If you disable this, you have to add them
| in your template yourself. See http://phpdebugbar.com/docs/rendering.html
|
*/
'inject' => env('DEBUGBAR_INJECT', true),
/*
|--------------------------------------------------------------------------
| Debugbar route prefix
|--------------------------------------------------------------------------
|
| Sometimes you want to set route prefix to be used by Debugbar to load
| its resources from. Usually the need comes from misconfigured web server or
| from trying to overcome bugs like this: http://trac.nginx.org/nginx/ticket/97
|
*/
'route_prefix' => env('DEBUGBAR_ROUTE_PREFIX', '_debugbar'),
/*
|--------------------------------------------------------------------------
| Debugbar route middleware
|--------------------------------------------------------------------------
|
| Additional middleware to run on the Debugbar routes
*/
'route_middleware' => [],
/*
|--------------------------------------------------------------------------
| Debugbar route domain
|--------------------------------------------------------------------------
|
| By default Debugbar route served from the same domain that request served.
| To override default domain, specify it as a non-empty value.
*/
'route_domain' => env('DEBUGBAR_ROUTE_DOMAIN', null),
/*
|--------------------------------------------------------------------------
| Debugbar theme
|--------------------------------------------------------------------------
|
| Switches between light and dark theme. If set to auto it will respect system preferences
| Possible values: auto, light, dark
*/
'theme' => env('DEBUGBAR_THEME', 'auto'),
/*
|--------------------------------------------------------------------------
| Backtrace stack limit
|--------------------------------------------------------------------------
|
| By default, the Debugbar limits the number of frames returned by the 'debug_backtrace()' function.
| If you need larger stacktraces, you can increase this number. Setting it to 0 will result in no limit.
*/
'debug_backtrace_limit' => (int) env('DEBUGBAR_DEBUG_BACKTRACE_LIMIT', 50),
];

80
config/filesystems.php Normal file
View File

@ -0,0 +1,80 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Filesystem Disk
|--------------------------------------------------------------------------
|
| Here you may specify the default filesystem disk that should be used
| by the framework. The "local" disk, as well as a variety of cloud
| based disks are available to your application for file storage.
|
*/
'default' => env('FILESYSTEM_DISK', 'local'),
/*
|--------------------------------------------------------------------------
| Filesystem Disks
|--------------------------------------------------------------------------
|
| Below you may configure as many filesystem disks as necessary, and you
| may even configure multiple disks for the same driver. Examples for
| most supported storage drivers are configured here for reference.
|
| Supported drivers: "local", "ftp", "sftp", "s3"
|
*/
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app/private'),
'serve' => true,
'throw' => false,
'report' => false,
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
'throw' => false,
'report' => false,
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
'report' => false,
],
],
/*
|--------------------------------------------------------------------------
| Symbolic Links
|--------------------------------------------------------------------------
|
| Here you may configure the symbolic links that will be created when the
| `storage:link` Artisan command is executed. The array keys should be
| the locations of the links and the values should be their targets.
|
*/
'links' => [
public_path('storage') => storage_path('app/public'),
],
];

160
config/livewire.php Normal file
View File

@ -0,0 +1,160 @@
<?php
return [
/*
|---------------------------------------------------------------------------
| Class Namespace
|---------------------------------------------------------------------------
|
| This value sets the root class namespace for Livewire component classes in
| your application. This value will change where component auto-discovery
| finds components. It's also referenced by the file creation commands.
|
*/
'class_namespace' => 'App\\Livewire',
/*
|---------------------------------------------------------------------------
| View Path
|---------------------------------------------------------------------------
|
| This value is used to specify where Livewire component Blade templates are
| stored when running file creation commands like `artisan make:livewire`.
| It is also used if you choose to omit a component's render() method.
|
*/
'view_path' => resource_path('views/livewire'),
/*
|---------------------------------------------------------------------------
| Layout
|---------------------------------------------------------------------------
| The view that will be used as the layout when rendering a single component
| as an entire page via `Route::get('/post/create', CreatePost::class);`.
| In this case, the view returned by CreatePost will render into $slot.
|
*/
'layout' => 'components.layouts.app',
/*
|---------------------------------------------------------------------------
| Lazy Loading Placeholder
|---------------------------------------------------------------------------
| Livewire allows you to lazy load components that would otherwise slow down
| the initial page load. Every component can have a custom placeholder or
| you can define the default placeholder view for all components below.
|
*/
'lazy_placeholder' => null,
/*
|---------------------------------------------------------------------------
| Temporary File Uploads
|---------------------------------------------------------------------------
|
| Livewire handles file uploads by storing uploads in a temporary directory
| before the file is stored permanently. All file uploads are directed to
| a global endpoint for temporary storage. You may configure this below:
|
*/
'temporary_file_upload' => [
'disk' => null, // Example: 'local', 's3' | Default: 'default'
'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB)
'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp'
'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1'
'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs...
'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',
'mov', 'avi', 'wmv', 'mp3', 'm4a',
'jpg', 'jpeg', 'mpga', 'webp', 'wma',
],
'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated...
'cleanup' => true, // Should cleanup temporary uploads older than 24 hrs...
],
/*
|---------------------------------------------------------------------------
| Render On Redirect
|---------------------------------------------------------------------------
|
| This value determines if Livewire will run a component's `render()` method
| after a redirect has been triggered using something like `redirect(...)`
| Setting this to true will render the view once more before redirecting
|
*/
'render_on_redirect' => false,
/*
|---------------------------------------------------------------------------
| Eloquent Model Binding
|---------------------------------------------------------------------------
|
| Previous versions of Livewire supported binding directly to eloquent model
| properties using wire:model by default. However, this behavior has been
| deemed too "magical" and has therefore been put under a feature flag.
|
*/
'legacy_model_binding' => false,
/*
|---------------------------------------------------------------------------
| Auto-inject Frontend Assets
|---------------------------------------------------------------------------
|
| By default, Livewire automatically injects its JavaScript and CSS into the
| <head> and <body> of pages containing Livewire components. By disabling
| this behavior, you need to use @livewireStyles and @livewireScripts.
|
*/
'inject_assets' => true,
/*
|---------------------------------------------------------------------------
| Navigate (SPA mode)
|---------------------------------------------------------------------------
|
| By adding `wire:navigate` to links in your Livewire application, Livewire
| will prevent the default link handling and instead request those pages
| via AJAX, creating an SPA-like effect. Configure this behavior here.
|
*/
'navigate' => [
'show_progress_bar' => true,
'progress_bar_color' => '#2299dd',
],
/*
|---------------------------------------------------------------------------
| HTML Morph Markers
|---------------------------------------------------------------------------
|
| Livewire intelligently "morphs" existing HTML into the newly rendered HTML
| after each update. To make this process more reliable, Livewire injects
| "markers" into the rendered Blade surrounding @if, @class & @foreach.
|
*/
'inject_morph_markers' => true,
/*
|---------------------------------------------------------------------------
| Pagination Theme
|---------------------------------------------------------------------------
|
| When enabling Livewire's pagination feature by using the `WithPagination`
| trait, Livewire will use Tailwind templates to render pagination views
| on the page. If you want Bootstrap CSS, you can specify: "bootstrap"
|
*/
'pagination_theme' => 'tailwind',
];

132
config/logging.php Normal file
View File

@ -0,0 +1,132 @@
<?php
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;
use Monolog\Processor\PsrLogMessageProcessor;
return [
/*
|--------------------------------------------------------------------------
| Default Log Channel
|--------------------------------------------------------------------------
|
| This option defines the default log channel that is utilized to write
| messages to your logs. The value provided here should match one of
| the channels present in the list of "channels" configured below.
|
*/
'default' => env('LOG_CHANNEL', 'stack'),
/*
|--------------------------------------------------------------------------
| Deprecations Log Channel
|--------------------------------------------------------------------------
|
| This option controls the log channel that should be used to log warnings
| regarding deprecated PHP and library features. This allows you to get
| your application ready for upcoming major versions of dependencies.
|
*/
'deprecations' => [
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
],
/*
|--------------------------------------------------------------------------
| Log Channels
|--------------------------------------------------------------------------
|
| Here you may configure the log channels for your application. Laravel
| utilizes the Monolog PHP logging library, which includes a variety
| of powerful log handlers and formatters that you're free to use.
|
| Available drivers: "single", "daily", "slack", "syslog",
| "errorlog", "monolog", "custom", "stack"
|
*/
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => explode(',', env('LOG_STACK', 'single')),
'ignore_exceptions' => false,
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => env('LOG_DAILY_DAYS', 14),
'replace_placeholders' => true,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
'level' => env('LOG_LEVEL', 'critical'),
'replace_placeholders' => true,
],
'papertrail' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
'handler_with' => [
'host' => env('PAPERTRAIL_URL'),
'port' => env('PAPERTRAIL_PORT'),
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
],
'processors' => [PsrLogMessageProcessor::class],
],
'stderr' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => StreamHandler::class,
'formatter' => env('LOG_STDERR_FORMATTER'),
'with' => [
'stream' => 'php://stderr',
],
'processors' => [PsrLogMessageProcessor::class],
],
'syslog' => [
'driver' => 'syslog',
'level' => env('LOG_LEVEL', 'debug'),
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
'replace_placeholders' => true,
],
'errorlog' => [
'driver' => 'errorlog',
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
],
'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
],
];

116
config/mail.php Normal file
View File

@ -0,0 +1,116 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Mailer
|--------------------------------------------------------------------------
|
| This option controls the default mailer that is used to send all email
| messages unless another mailer is explicitly specified when sending
| the message. All additional mailers can be configured within the
| "mailers" array. Examples of each type of mailer are provided.
|
*/
'default' => env('MAIL_MAILER', 'log'),
/*
|--------------------------------------------------------------------------
| Mailer Configurations
|--------------------------------------------------------------------------
|
| Here you may configure all of the mailers used by your application plus
| their respective settings. Several examples have been configured for
| you and you are free to add your own as your application requires.
|
| Laravel supports a variety of mail "transport" drivers that can be used
| when delivering an email. You may specify which one you're using for
| your mailers below. You may also add additional mailers if needed.
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
| "postmark", "resend", "log", "array",
| "failover", "roundrobin"
|
*/
'mailers' => [
'smtp' => [
'transport' => 'smtp',
'scheme' => env('MAIL_SCHEME'),
'url' => env('MAIL_URL'),
'host' => env('MAIL_HOST', '127.0.0.1'),
'port' => env('MAIL_PORT', 2525),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
],
'ses' => [
'transport' => 'ses',
],
'postmark' => [
'transport' => 'postmark',
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
// 'client' => [
// 'timeout' => 5,
// ],
],
'resend' => [
'transport' => 'resend',
],
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
],
'log' => [
'transport' => 'log',
'channel' => env('MAIL_LOG_CHANNEL'),
],
'array' => [
'transport' => 'array',
],
'failover' => [
'transport' => 'failover',
'mailers' => [
'smtp',
'log',
],
],
'roundrobin' => [
'transport' => 'roundrobin',
'mailers' => [
'ses',
'postmark',
],
],
],
/*
|--------------------------------------------------------------------------
| Global "From" Address
|--------------------------------------------------------------------------
|
| You may wish for all emails sent by your application to be sent from
| the same address. Here you may specify a name and address that is
| used globally for all emails that are sent by your application.
|
*/
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', 'Example'),
],
];

202
config/permission.php Normal file
View File

@ -0,0 +1,202 @@
<?php
return [
'models' => [
/*
* When using the "HasPermissions" trait from this package, we need to know which
* Eloquent model should be used to retrieve your permissions. Of course, it
* is often just the "Permission" model but you may use whatever you like.
*
* The model you want to use as a Permission model needs to implement the
* `Spatie\Permission\Contracts\Permission` contract.
*/
'permission' => Spatie\Permission\Models\Permission::class,
/*
* When using the "HasRoles" trait from this package, we need to know which
* Eloquent model should be used to retrieve your roles. Of course, it
* is often just the "Role" model but you may use whatever you like.
*
* The model you want to use as a Role model needs to implement the
* `Spatie\Permission\Contracts\Role` contract.
*/
'role' => Spatie\Permission\Models\Role::class,
],
'table_names' => [
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your roles. We have chosen a basic
* default value but you may easily change it to any table you like.
*/
'roles' => 'roles',
/*
* When using the "HasPermissions" trait from this package, we need to know which
* table should be used to retrieve your permissions. We have chosen a basic
* default value but you may easily change it to any table you like.
*/
'permissions' => 'permissions',
/*
* When using the "HasPermissions" trait from this package, we need to know which
* table should be used to retrieve your models permissions. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'model_has_permissions' => 'model_has_permissions',
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your models roles. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'model_has_roles' => 'model_has_roles',
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your roles permissions. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'role_has_permissions' => 'role_has_permissions',
],
'column_names' => [
/*
* Change this if you want to name the related pivots other than defaults
*/
'role_pivot_key' => null, // default 'role_id',
'permission_pivot_key' => null, // default 'permission_id',
/*
* Change this if you want to name the related model primary key other than
* `model_id`.
*
* For example, this would be nice if your primary keys are all UUIDs. In
* that case, name this `model_uuid`.
*/
'model_morph_key' => 'model_id',
/*
* Change this if you want to use the teams feature and your related model's
* foreign key is other than `team_id`.
*/
'team_foreign_key' => 'team_id',
],
/*
* When set to true, the method for checking permissions will be registered on the gate.
* Set this to false if you want to implement custom logic for checking permissions.
*/
'register_permission_check_method' => true,
/*
* When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered
* this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated
* NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it.
*/
'register_octane_reset_listener' => false,
/*
* Events will fire when a role or permission is assigned/unassigned:
* \Spatie\Permission\Events\RoleAttached
* \Spatie\Permission\Events\RoleDetached
* \Spatie\Permission\Events\PermissionAttached
* \Spatie\Permission\Events\PermissionDetached
*
* To enable, set to true, and then create listeners to watch these events.
*/
'events_enabled' => false,
/*
* Teams Feature.
* When set to true the package implements teams using the 'team_foreign_key'.
* If you want the migrations to register the 'team_foreign_key', you must
* set this to true before doing the migration.
* If you already did the migration then you must make a new migration to also
* add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions'
* (view the latest version of this package's migration file)
*/
'teams' => false,
/*
* The class to use to resolve the permissions team id
*/
'team_resolver' => \Spatie\Permission\DefaultTeamResolver::class,
/*
* Passport Client Credentials Grant
* When set to true the package will use Passports Client to check permissions
*/
'use_passport_client_credentials' => false,
/*
* When set to true, the required permission names are added to exception messages.
* This could be considered an information leak in some contexts, so the default
* setting is false here for optimum safety.
*/
'display_permission_in_exception' => false,
/*
* When set to true, the required role names are added to exception messages.
* This could be considered an information leak in some contexts, so the default
* setting is false here for optimum safety.
*/
'display_role_in_exception' => false,
/*
* By default wildcard permission lookups are disabled.
* See documentation to understand supported syntax.
*/
'enable_wildcard_permission' => false,
/*
* The class to use for interpreting wildcard permissions.
* If you need to modify delimiters, override the class and specify its name here.
*/
// 'wildcard_permission' => Spatie\Permission\WildcardPermission::class,
/* Cache-specific settings */
'cache' => [
/*
* By default all permissions are cached for 24 hours to speed up performance.
* When permissions or roles are updated the cache is flushed automatically.
*/
'expiration_time' => \DateInterval::createFromDateString('24 hours'),
/*
* The cache key used to store all permissions.
*/
'key' => 'spatie.permission.cache',
/*
* You may optionally indicate a specific cache driver to use for permission and
* role caching using any of the `store` drivers listed in the cache.php config
* file. Using 'default' here means to use the `default` set in cache.php.
*/
'store' => 'default',
],
];

112
config/queue.php Normal file
View File

@ -0,0 +1,112 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Queue Connection Name
|--------------------------------------------------------------------------
|
| Laravel's queue supports a variety of backends via a single, unified
| API, giving you convenient access to each backend using identical
| syntax for each. The default queue connection is defined below.
|
*/
'default' => env('QUEUE_CONNECTION', 'database'),
/*
|--------------------------------------------------------------------------
| Queue Connections
|--------------------------------------------------------------------------
|
| Here you may configure the connection options for every queue backend
| used by your application. An example configuration is provided for
| each backend supported by Laravel. You're also free to add more.
|
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null"
|
*/
'connections' => [
'sync' => [
'driver' => 'sync',
],
'database' => [
'driver' => 'database',
'connection' => env('DB_QUEUE_CONNECTION'),
'table' => env('DB_QUEUE_TABLE', 'jobs'),
'queue' => env('DB_QUEUE', 'default'),
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
'after_commit' => false,
],
'beanstalkd' => [
'driver' => 'beanstalkd',
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
'queue' => env('BEANSTALKD_QUEUE', 'default'),
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
'block_for' => 0,
'after_commit' => false,
],
'sqs' => [
'driver' => 'sqs',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
'queue' => env('SQS_QUEUE', 'default'),
'suffix' => env('SQS_SUFFIX'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'after_commit' => false,
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
'block_for' => null,
'after_commit' => false,
],
],
/*
|--------------------------------------------------------------------------
| Job Batching
|--------------------------------------------------------------------------
|
| The following options configure the database and table that store job
| batching information. These options can be updated to any database
| connection and table which has been defined by your application.
|
*/
'batching' => [
'database' => env('DB_CONNECTION', 'sqlite'),
'table' => 'job_batches',
],
/*
|--------------------------------------------------------------------------
| Failed Queue Jobs
|--------------------------------------------------------------------------
|
| These options configure the behavior of failed queue job logging so you
| can control how and where failed jobs are stored. Laravel ships with
| support for storing failed jobs in a simple file or in a database.
|
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
*/
'failed' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
'database' => env('DB_CONNECTION', 'sqlite'),
'table' => 'failed_jobs',
],
];

91
config/services.php Normal file
View File

@ -0,0 +1,91 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Third Party Services
|--------------------------------------------------------------------------
|
| This file is for storing the credentials for third party services such
| as Mailgun, Postmark, AWS and more. This file provides the de facto
| location for this type of information, allowing packages to have
| a conventional file to locate the various service credentials.
|
*/
'postmark' => [
'token' => env('POSTMARK_TOKEN'),
],
'ses' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
'resend' => [
'key' => env('RESEND_KEY'),
],
'slack' => [
'notifications' => [
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
],
],
'traccar' => [
'api_url' => env('TRACCAR_API_URL', 'https://demo.traccar.org/api'),
'admin_username' => env('TRACCAR_ADMIN_USERNAME', 'admin'),
'admin_password' => env('TRACCAR_ADMIN_PASSWORD', 'admin'),
],
'maps' => [
'default_provider' => env('MAP_PROVIDER', 'openstreetmap'),
'providers' => [
'openstreetmap' => [
'name' => 'OpenStreetMap',
'free' => true,
'requires_api_key' => false,
'tile_url' => 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
'attribution' => '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
],
'google' => [
'name' => 'Google Maps',
'free' => false,
'requires_api_key' => true,
'api_key' => env('GOOGLE_MAPS_API_KEY'),
'enabled' => !empty(env('GOOGLE_MAPS_API_KEY'))
],
'mapbox' => [
'name' => 'Mapbox',
'free' => false,
'requires_api_key' => true,
'api_key' => env('MAPBOX_API_KEY'),
'enabled' => !empty(env('MAPBOX_API_KEY')),
'tile_url' => 'https://api.mapbox.com/styles/v1/mapbox/{style}/tiles/{z}/{x}/{y}?access_token={accessToken}',
'attribution' => '&copy; <a href="https://www.mapbox.com/">Mapbox</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
],
'cartodb' => [
'name' => 'CartoDB',
'free' => true,
'requires_api_key' => false,
'styles' => [
'light' => 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
'dark' => 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',
'voyager' => 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png'
],
'attribution' => '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'
],
'satellite' => [
'name' => 'Satellite (Esri)',
'free' => true,
'requires_api_key' => false,
'tile_url' => 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
'attribution' => 'Tiles &copy; Esri &mdash; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
]
]
],
];

217
config/session.php Normal file
View File

@ -0,0 +1,217 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Session Driver
|--------------------------------------------------------------------------
|
| This option determines the default session driver that is utilized for
| incoming requests. Laravel supports a variety of storage options to
| persist session data. Database storage is a great default choice.
|
| Supported: "file", "cookie", "database", "apc",
| "memcached", "redis", "dynamodb", "array"
|
*/
'driver' => env('SESSION_DRIVER', 'database'),
/*
|--------------------------------------------------------------------------
| Session Lifetime
|--------------------------------------------------------------------------
|
| Here you may specify the number of minutes that you wish the session
| to be allowed to remain idle before it expires. If you want them
| to expire immediately when the browser is closed then you may
| indicate that via the expire_on_close configuration option.
|
*/
'lifetime' => (int) env('SESSION_LIFETIME', 120),
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
/*
|--------------------------------------------------------------------------
| Session Encryption
|--------------------------------------------------------------------------
|
| This option allows you to easily specify that all of your session data
| should be encrypted before it's stored. All encryption is performed
| automatically by Laravel and you may use the session like normal.
|
*/
'encrypt' => env('SESSION_ENCRYPT', false),
/*
|--------------------------------------------------------------------------
| Session File Location
|--------------------------------------------------------------------------
|
| When utilizing the "file" session driver, the session files are placed
| on disk. The default storage location is defined here; however, you
| are free to provide another location where they should be stored.
|
*/
'files' => storage_path('framework/sessions'),
/*
|--------------------------------------------------------------------------
| Session Database Connection
|--------------------------------------------------------------------------
|
| When using the "database" or "redis" session drivers, you may specify a
| connection that should be used to manage these sessions. This should
| correspond to a connection in your database configuration options.
|
*/
'connection' => env('SESSION_CONNECTION'),
/*
|--------------------------------------------------------------------------
| Session Database Table
|--------------------------------------------------------------------------
|
| When using the "database" session driver, you may specify the table to
| be used to store sessions. Of course, a sensible default is defined
| for you; however, you're welcome to change this to another table.
|
*/
'table' => env('SESSION_TABLE', 'sessions'),
/*
|--------------------------------------------------------------------------
| Session Cache Store
|--------------------------------------------------------------------------
|
| When using one of the framework's cache driven session backends, you may
| define the cache store which should be used to store the session data
| between requests. This must match one of your defined cache stores.
|
| Affects: "apc", "dynamodb", "memcached", "redis"
|
*/
'store' => env('SESSION_STORE'),
/*
|--------------------------------------------------------------------------
| Session Sweeping Lottery
|--------------------------------------------------------------------------
|
| Some session drivers must manually sweep their storage location to get
| rid of old sessions from storage. Here are the chances that it will
| happen on a given request. By default, the odds are 2 out of 100.
|
*/
'lottery' => [2, 100],
/*
|--------------------------------------------------------------------------
| Session Cookie Name
|--------------------------------------------------------------------------
|
| Here you may change the name of the session cookie that is created by
| the framework. Typically, you should not need to change this value
| since doing so does not grant a meaningful security improvement.
|
*/
'cookie' => env(
'SESSION_COOKIE',
Str::slug(env('APP_NAME', 'laravel'), '_').'_session'
),
/*
|--------------------------------------------------------------------------
| Session Cookie Path
|--------------------------------------------------------------------------
|
| The session cookie path determines the path for which the cookie will
| be regarded as available. Typically, this will be the root path of
| your application, but you're free to change this when necessary.
|
*/
'path' => env('SESSION_PATH', '/'),
/*
|--------------------------------------------------------------------------
| Session Cookie Domain
|--------------------------------------------------------------------------
|
| This value determines the domain and subdomains the session cookie is
| available to. By default, the cookie will be available to the root
| domain and all subdomains. Typically, this shouldn't be changed.
|
*/
'domain' => env('SESSION_DOMAIN'),
/*
|--------------------------------------------------------------------------
| HTTPS Only Cookies
|--------------------------------------------------------------------------
|
| By setting this option to true, session cookies will only be sent back
| to the server if the browser has a HTTPS connection. This will keep
| the cookie from being sent to you when it can't be done securely.
|
*/
'secure' => env('SESSION_SECURE_COOKIE'),
/*
|--------------------------------------------------------------------------
| HTTP Access Only
|--------------------------------------------------------------------------
|
| Setting this value to true will prevent JavaScript from accessing the
| value of the cookie and the cookie will only be accessible through
| the HTTP protocol. It's unlikely you should disable this option.
|
*/
'http_only' => env('SESSION_HTTP_ONLY', true),
/*
|--------------------------------------------------------------------------
| Same-Site Cookies
|--------------------------------------------------------------------------
|
| This option determines how your cookies behave when cross-site requests
| take place, and can be used to mitigate CSRF attacks. By default, we
| will set this value to "lax" to permit secure cross-site requests.
|
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
| Supported: "lax", "strict", "none", null
|
*/
'same_site' => env('SESSION_SAME_SITE', 'lax'),
/*
|--------------------------------------------------------------------------
| Partitioned Cookies
|--------------------------------------------------------------------------
|
| Setting this value to true will tie the cookie to the top-level site for
| a cross-site context. Partitioned cookies are accepted by the browser
| when flagged "secure" and the Same-Site attribute is set to "none".
|
*/
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
];

50
config/tinker.php Normal file
View File

@ -0,0 +1,50 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Console Commands
|--------------------------------------------------------------------------
|
| This option allows you to add additional Artisan commands that should
| be available within the Tinker environment. Once the command is in
| this array you may execute the command in Tinker using its name.
|
*/
'commands' => [
// App\Console\Commands\ExampleCommand::class,
],
/*
|--------------------------------------------------------------------------
| Auto Aliased Classes
|--------------------------------------------------------------------------
|
| Tinker will not automatically alias classes in your vendor namespaces
| but you may explicitly allow a subset of classes to get aliased by
| adding the names of each of those classes to the following list.
|
*/
'alias' => [
//
],
/*
|--------------------------------------------------------------------------
| Classes That Should Not Be Aliased
|--------------------------------------------------------------------------
|
| Typically, Tinker automatically aliases classes as you require them in
| Tinker. However, you may wish to never alias certain classes, which
| you may accomplish by listing the classes in the following array.
|
*/
'dont_alias' => [
'App\Nova',
],
];

1
database/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.sqlite*

View File

@ -0,0 +1,44 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
*/
class UserFactory extends Factory
{
/**
* The current password being used by the factory.
*/
protected static ?string $password;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
}

View File

@ -0,0 +1,49 @@
<?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::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('sessions');
}
};

View File

@ -0,0 +1,35 @@
<?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::create('cache', function (Blueprint $table) {
$table->string('key')->primary();
$table->mediumText('value');
$table->integer('expiration');
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->integer('expiration');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};

View File

@ -0,0 +1,57 @@
<?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::create('jobs', function (Blueprint $table) {
$table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
Schema::dropIfExists('job_batches');
Schema::dropIfExists('failed_jobs');
}
};

View File

@ -0,0 +1,136 @@
<?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
{
$teams = config('permission.teams');
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
throw_if(empty($tableNames), new Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.'));
throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), new Exception('Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.'));
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
// $table->engine('InnoDB');
$table->bigIncrements('id'); // permission id
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
$table->timestamps();
$table->unique(['name', 'guard_name']);
});
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
// $table->engine('InnoDB');
$table->bigIncrements('id'); // role id
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
}
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
$table->timestamps();
if ($teams || config('permission.testing')) {
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
} else {
$table->unique(['name', 'guard_name']);
}
});
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
$table->unsignedBigInteger($pivotPermission);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
} else {
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
}
});
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
$table->unsignedBigInteger($pivotRole);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
} else {
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
}
});
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
$table->unsignedBigInteger($pivotPermission);
$table->unsignedBigInteger($pivotRole);
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
});
app('cache')
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
->forget(config('permission.cache.key'));
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$tableNames = config('permission.table_names');
if (empty($tableNames)) {
throw new \Exception('Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
}
Schema::drop($tableNames['role_has_permissions']);
Schema::drop($tableNames['model_has_roles']);
Schema::drop($tableNames['model_has_permissions']);
Schema::drop($tableNames['roles']);
Schema::drop($tableNames['permissions']);
}
};

View File

@ -0,0 +1,34 @@
<?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::create('device_groups', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('traccar_group_id')->nullable();
$table->string('name');
$table->text('description')->nullable();
$table->json('attributes')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->index('traccar_group_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('device_groups');
}
};

View File

@ -0,0 +1,49 @@
<?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::create('devices', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->unsignedBigInteger('traccar_device_id')->nullable();
$table->string('name');
$table->string('unique_id')->unique();
$table->string('imei')->nullable();
$table->string('phone')->nullable();
$table->string('model')->nullable();
$table->string('contact')->nullable();
$table->string('category')->default('default');
$table->string('protocol')->nullable();
$table->enum('status', ['online', 'offline', 'unknown'])->default('unknown');
$table->timestamp('last_update')->nullable();
$table->unsignedBigInteger('position_id')->nullable();
$table->unsignedBigInteger('group_id')->nullable();
$table->unsignedBigInteger('driver_id')->nullable();
$table->json('attributes')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->index('traccar_device_id');
$table->index('unique_id');
$table->index('status');
$table->foreign('group_id')->references('id')->on('device_groups')->onDelete('set null');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('devices');
}
};

View File

@ -0,0 +1,49 @@
<?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::create('positions', function (Blueprint $table) {
$table->id();
$table->foreignId('device_id')->constrained()->onDelete('cascade');
$table->unsignedBigInteger('traccar_position_id')->nullable();
$table->string('protocol')->nullable();
$table->timestamp('device_time');
$table->timestamp('fix_time');
$table->timestamp('server_time');
$table->boolean('outdated')->default(false);
$table->boolean('valid')->default(true);
$table->decimal('latitude', 10, 8);
$table->decimal('longitude', 11, 8);
$table->decimal('altitude', 8, 2)->nullable();
$table->decimal('speed', 8, 2)->default(0);
$table->decimal('course', 5, 2)->default(0);
$table->string('address')->nullable();
$table->decimal('accuracy', 8, 2)->nullable();
$table->json('network')->nullable();
$table->json('attributes')->nullable();
$table->timestamps();
$table->index('device_id');
$table->index('device_time');
$table->index(['latitude', 'longitude']);
$table->index('traccar_position_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('positions');
}
};

View File

@ -0,0 +1,40 @@
<?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::create('geofences', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('traccar_geofence_id')->nullable();
$table->string('name');
$table->text('description')->nullable();
$table->text('area'); // WKT geometry
$table->enum('type', ['circle', 'polygon'])->default('circle');
$table->decimal('latitude', 10, 8)->nullable();
$table->decimal('longitude', 11, 8)->nullable();
$table->decimal('radius', 8, 2)->nullable();
$table->json('attributes')->nullable();
$table->unsignedBigInteger('calendar_id')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->index('traccar_geofence_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('geofences');
}
};

View File

@ -0,0 +1,36 @@
<?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::create('drivers', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->onDelete('set null');
$table->string('name');
$table->string('license_number')->nullable();
$table->string('phone')->nullable();
$table->string('email')->nullable();
$table->json('attributes')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->index('license_number');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('drivers');
}
};

View File

@ -0,0 +1,43 @@
<?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::create('events', function (Blueprint $table) {
$table->id();
$table->foreignId('device_id')->constrained()->onDelete('cascade');
$table->foreignId('position_id')->nullable()->constrained()->onDelete('cascade');
$table->foreignId('geofence_id')->nullable()->constrained()->onDelete('cascade');
$table->unsignedBigInteger('traccar_event_id')->nullable();
$table->string('type');
$table->timestamp('event_time');
$table->json('attributes')->nullable();
$table->boolean('acknowledged')->default(false);
$table->foreignId('acknowledged_by')->nullable()->constrained('users')->onDelete('set null');
$table->timestamp('acknowledged_at')->nullable();
$table->timestamps();
$table->index('device_id');
$table->index('type');
$table->index('event_time');
$table->index('acknowledged');
$table->index('traccar_event_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('events');
}
};

View File

@ -0,0 +1,40 @@
<?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::create('commands', function (Blueprint $table) {
$table->id();
$table->foreignId('device_id')->constrained()->onDelete('cascade');
$table->unsignedBigInteger('traccar_command_id')->nullable();
$table->string('type');
$table->text('description')->nullable();
$table->json('attributes')->nullable();
$table->timestamp('sent_at')->nullable();
$table->enum('status', ['pending', 'sent', 'success', 'failed'])->default('pending');
$table->text('response')->nullable();
$table->timestamps();
$table->index('device_id');
$table->index('status');
$table->index('type');
$table->index('traccar_command_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('commands');
}
};

View File

@ -0,0 +1,31 @@
<?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::create('device_geofences', function (Blueprint $table) {
$table->id();
$table->foreignId('device_id')->constrained()->onDelete('cascade');
$table->foreignId('geofence_id')->constrained()->onDelete('cascade');
$table->timestamps();
$table->unique(['device_id', 'geofence_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('device_geofences');
}
};

View File

@ -0,0 +1,38 @@
<?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::create('notification_preferences', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->boolean('email_notifications')->default(true);
$table->boolean('sms_notifications')->default(false);
$table->boolean('push_notifications')->default(true);
$table->boolean('geofence_alerts')->default(true);
$table->boolean('overspeed_alerts')->default(true);
$table->boolean('offline_alerts')->default(true);
$table->boolean('sos_alerts')->default(true);
$table->boolean('maintenance_alerts')->default(true);
$table->timestamps();
$table->unique('user_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('notification_preferences');
}
};

View File

@ -0,0 +1,40 @@
<?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::table('users', function (Blueprint $table) {
$table->unsignedBigInteger('traccar_user_id')->nullable();
$table->string('traccar_username')->nullable();
$table->string('traccar_password')->nullable();
$table->string('phone')->nullable();
$table->string('timezone')->default('UTC');
$table->boolean('is_active')->default(true);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn([
'traccar_user_id',
'traccar_username',
'traccar_password',
'phone',
'timezone',
'is_active'
]);
});
}
};

View File

@ -0,0 +1,28 @@
<?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::table('devices', function (Blueprint $table) {
$table->foreign('driver_id')->references('id')->on('drivers')->onDelete('set null');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('devices', function (Blueprint $table) {
$table->dropForeign(['driver_id']);
});
}
};

View File

@ -0,0 +1,32 @@
<?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::create('device_group_user', function (Blueprint $table) {
$table->id();
$table->foreignId('device_group_id')->constrained()->onDelete('cascade');
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->timestamps();
// Ensure unique combinations
$table->unique(['device_group_id', 'user_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('device_group_user');
}
};

View File

@ -0,0 +1,29 @@
<?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::table('geofences', function (Blueprint $table) {
$table->foreignId('user_id')->nullable()->constrained()->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('geofences', function (Blueprint $table) {
$table->dropForeign(['user_id']);
$table->dropColumn('user_id');
});
}
};

View File

@ -0,0 +1,32 @@
<?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::table('geofences', function (Blueprint $table) {
$table->json('coordinates')->nullable()->after('area');
$table->index(['is_active']);
$table->index(['type']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('geofences', function (Blueprint $table) {
$table->dropColumn('coordinates');
$table->dropIndex(['is_active']);
$table->dropIndex(['type']);
});
}
};

View File

@ -0,0 +1,31 @@
<?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::create('roles', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->string('display_name');
$table->text('description')->nullable();
$table->boolean('is_system')->default(false);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('roles');
}
};

View File

@ -0,0 +1,31 @@
<?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::create('permissions', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->string('display_name');
$table->text('description')->nullable();
$table->string('category')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('permissions');
}
};

View File

@ -0,0 +1,39 @@
<?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::create('drivers', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('first_name');
$table->string('last_name');
$table->string('license_number')->unique();
$table->date('license_expiry');
$table->string('phone')->nullable();
$table->string('email')->nullable();
$table->text('address')->nullable();
$table->date('date_of_birth')->nullable();
$table->enum('status', ['active', 'inactive', 'suspended'])->default('active');
$table->json('emergency_contacts')->nullable();
$table->string('profile_photo')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('drivers');
}
};

View File

@ -0,0 +1,33 @@
<?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::create('role_user', function (Blueprint $table) {
$table->id();
$table->foreignId('role_id')->constrained()->onDelete('cascade');
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->timestamp('assigned_at')->useCurrent();
$table->foreignId('assigned_by')->nullable()->constrained('users')->onDelete('set null');
$table->timestamps();
$table->unique(['role_id', 'user_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('role_user');
}
};

View File

@ -0,0 +1,31 @@
<?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::create('permission_role', function (Blueprint $table) {
$table->id();
$table->foreignId('permission_id')->constrained()->onDelete('cascade');
$table->foreignId('role_id')->constrained()->onDelete('cascade');
$table->timestamps();
$table->unique(['permission_id', 'role_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('permission_role');
}
};

View File

@ -0,0 +1,40 @@
<?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::create('commands', function (Blueprint $table) {
$table->id();
$table->foreignId('device_id')->constrained()->onDelete('cascade');
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('type'); // 'stop_engine', 'start_engine', 'lock_doors', 'unlock_doors', 'get_location', etc.
$table->text('description')->nullable();
$table->json('parameters')->nullable();
$table->enum('status', ['pending', 'sent', 'delivered', 'executed', 'failed', 'expired'])->default('pending');
$table->timestamp('sent_at')->nullable();
$table->timestamp('delivered_at')->nullable();
$table->timestamp('executed_at')->nullable();
$table->timestamp('expires_at')->nullable();
$table->text('response')->nullable();
$table->text('error_message')->nullable();
$table->string('traccar_command_id')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('commands');
}
};

View File

@ -0,0 +1,40 @@
<?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::create('notification_settings', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->boolean('email_enabled')->default(true);
$table->boolean('sms_enabled')->default(false);
$table->boolean('push_enabled')->default(true);
$table->boolean('webhook_enabled')->default(false);
$table->string('webhook_url')->nullable();
$table->string('sms_provider')->nullable(); // 'twilio', 'nexmo', etc.
$table->json('sms_config')->nullable();
$table->json('event_types')->nullable(); // Which events to notify about
$table->json('device_filters')->nullable(); // Which devices to notify about
$table->time('quiet_hours_start')->nullable();
$table->time('quiet_hours_end')->nullable();
$table->json('allowed_days')->nullable(); // Days of week for notifications
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('notification_settings');
}
};

View File

@ -0,0 +1,44 @@
<?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::create('subscriptions', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('plan_name');
$table->string('plan_type'); // 'monthly', 'yearly', 'lifetime'
$table->decimal('price', 8, 2);
$table->string('currency', 3)->default('USD');
$table->enum('status', ['active', 'cancelled', 'expired', 'suspended'])->default('active');
$table->integer('device_limit')->default(1);
$table->integer('user_limit')->default(1);
$table->boolean('has_reports')->default(true);
$table->boolean('has_api_access')->default(false);
$table->boolean('has_priority_support')->default(false);
$table->timestamp('starts_at');
$table->timestamp('ends_at')->nullable();
$table->timestamp('cancelled_at')->nullable();
$table->string('payment_provider')->nullable(); // 'stripe', 'paypal', etc.
$table->string('external_id')->nullable(); // Provider subscription ID
$table->json('features')->nullable(); // Additional features
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('subscriptions');
}
};

View File

@ -0,0 +1,31 @@
<?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::table('users', function (Blueprint $table) {
$table->string('company')->nullable()->after('phone');
$table->text('notes')->nullable()->after('company');
$table->foreignId('created_by')->nullable()->constrained('users')->onDelete('set null')->after('notes');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropForeign(['created_by']);
$table->dropColumn(['company', 'notes', 'created_by']);
});
}
};

View File

@ -0,0 +1,44 @@
<?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::table('drivers', function (Blueprint $table) {
$table->string('driver_id')->nullable()->after('id');
$table->string('status')->default('active')->after('is_active');
$table->string('license_type')->nullable()->after('license_number');
$table->date('license_expiry_date')->nullable()->after('license_type');
$table->string('assigned_vehicle')->nullable()->after('email');
$table->string('vehicle_plate')->nullable()->after('assigned_vehicle');
$table->integer('performance_score')->nullable()->after('vehicle_plate');
$table->text('notes')->nullable()->after('attributes');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('drivers', function (Blueprint $table) {
$table->dropColumn([
'driver_id',
'status',
'license_type',
'license_expiry_date',
'assigned_vehicle',
'vehicle_plate',
'performance_score',
'notes',
]);
});
}
};

View File

@ -0,0 +1,29 @@
<?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::table('subscriptions', function (Blueprint $table) {
$table->decimal('amount', 8, 2)->nullable()->after('price');
$table->enum('billing_cycle', ['monthly', 'yearly', 'lifetime'])->default('monthly')->after('amount');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('subscriptions', function (Blueprint $table) {
$table->dropColumn(['amount', 'billing_cycle']);
});
}
};

View File

@ -0,0 +1,29 @@
<?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::table('users', function (Blueprint $table) {
$table->timestamp('last_login_at')->nullable()->after('email_verified_at');
$table->string('status')->default('active')->after('last_login_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['last_login_at', 'status']);
});
}
};

View File

@ -0,0 +1,47 @@
<?php
namespace Database\Seeders;
use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*/
public function run(): void
{
$this->call([
RolePermissionSeeder::class,
]);
// Create test admin user
$admin = User::factory()->create([
'name' => 'Admin User',
'email' => 'admin@gps-tracker.com',
'password' => bcrypt('password'),
'is_active' => true,
]);
$admin->assignRole('super-admin');
// Create test regular user
$user = User::factory()->create([
'name' => 'Test User',
'email' => 'user@gps-tracker.com',
'password' => bcrypt('password'),
'is_active' => true,
]);
$user->assignRole('user');
// Create test fleet manager
$fleetManager = User::factory()->create([
'name' => 'Fleet Manager',
'email' => 'fleet@gps-tracker.com',
'password' => bcrypt('password'),
'is_active' => true,
]);
$fleetManager->assignRole('fleet-manager');
}
}

View File

@ -0,0 +1,141 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;
class RolePermissionSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// Reset cached roles and permissions
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
// Create permissions
$permissions = [
// Device permissions
'view devices',
'create devices',
'edit devices',
'delete devices',
// Tracking permissions
'view tracking',
'live tracking',
// Geofence permissions
'view geofences',
'create geofences',
'edit geofences',
'delete geofences',
// Event permissions
'view events',
'acknowledge events',
'delete events',
// Report permissions
'view reports',
'export reports',
// Command permissions
'send commands',
'view commands',
// User management
'view users',
'create users',
'edit users',
'delete users',
// Group management
'view groups',
'create groups',
'edit groups',
'delete groups',
// Driver management
'view drivers',
'create drivers',
'edit drivers',
'delete drivers',
// System administration
'admin panel',
'system settings',
'api management',
];
foreach ($permissions as $permission) {
Permission::create(['name' => $permission]);
}
// Create roles and assign permissions
// Super Admin Role
$superAdmin = Role::create(['name' => 'super-admin']);
$superAdmin->givePermissionTo(Permission::all());
// Admin Role
$admin = Role::create(['name' => 'admin']);
$admin->givePermissionTo([
'view devices', 'create devices', 'edit devices', 'delete devices',
'view tracking', 'live tracking',
'view geofences', 'create geofences', 'edit geofences', 'delete geofences',
'view events', 'acknowledge events',
'view reports', 'export reports',
'send commands', 'view commands',
'view users', 'create users', 'edit users',
'view groups', 'create groups', 'edit groups', 'delete groups',
'view drivers', 'create drivers', 'edit drivers', 'delete drivers',
]);
// Fleet Manager Role
$fleetManager = Role::create(['name' => 'fleet-manager']);
$fleetManager->givePermissionTo([
'view devices', 'edit devices',
'view tracking', 'live tracking',
'view geofences', 'create geofences', 'edit geofences',
'view events', 'acknowledge events',
'view reports', 'export reports',
'send commands', 'view commands',
'view drivers', 'create drivers', 'edit drivers',
'view groups',
]);
// Operator Role
$operator = Role::create(['name' => 'operator']);
$operator->givePermissionTo([
'view devices',
'view tracking', 'live tracking',
'view geofences',
'view events', 'acknowledge events',
'view reports',
'view commands',
'view drivers',
]);
// User Role (basic user with limited permissions)
$user = Role::create(['name' => 'user']);
$user->givePermissionTo([
'view devices',
'view tracking',
'view events',
'view reports',
]);
// Driver Role (for drivers who only need to see their assigned vehicle)
$driver = Role::create(['name' => 'driver']);
$driver->givePermissionTo([
'view devices',
'view tracking',
'view events',
]);
}
}

70
docker/8.0/Dockerfile Normal file
View File

@ -0,0 +1,70 @@
FROM ubuntu:24.04
LABEL maintainer="Taylor Otwell"
ARG WWWGROUP
ARG NODE_VERSION=22
ARG POSTGRES_VERSION=17
WORKDIR /var/www/html
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=UTC
ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80"
ENV SUPERVISOR_PHP_USER="sail"
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \
echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom
RUN apt-get update && apt-get upgrade -y \
&& mkdir -p /etc/apt/keyrings \
&& apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils librsvg2-bin fswatch ffmpeg nano \
&& curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xb8dc7e53946656efbce4c1dd71daeaab4ad4cab6' | gpg --dearmor | tee /usr/share/keyrings/ppa_ondrej_php.gpg > /dev/null \
&& echo "deb [signed-by=/usr/share/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
&& apt-get update \
&& apt-get install -y php8.0-cli php8.0-dev \
php8.0-pgsql php8.0-sqlite3 php8.0-gd php8.0-imagick \
php8.0-curl php8.0-memcached php8.0-mongodb \
php8.0-imap php8.0-mysql php8.0-mbstring \
php8.0-xml php8.0-zip php8.0-bcmath php8.0-soap \
php8.0-intl php8.0-readline php8.0-pcov \
php8.0-msgpack php8.0-igbinary php8.0-ldap \
php8.0-redis php8.0-swoole php8.0-xdebug \
&& curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
&& apt-get update \
&& apt-get install -y nodejs \
&& npm install -g npm \
&& npm install -g bun \
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /usr/share/keyrings/yarnkey.gpg >/dev/null \
&& echo "deb [signed-by=/usr/share/keyrings/yarnkey.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
&& curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/pgdg.gpg >/dev/null \
&& echo "deb [signed-by=/usr/share/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt noble-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
&& apt-get update \
&& apt-get install -y yarn \
&& apt-get install -y mysql-client \
&& apt-get install -y postgresql-client-$POSTGRES_VERSION \
&& apt-get -y autoremove \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN update-alternatives --set php /usr/bin/php8.0
RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.0
RUN userdel -r ubuntu
RUN groupadd --force -g $WWWGROUP sail
RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail
COPY start-container /usr/local/bin/start-container
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY php.ini /etc/php/8.0/cli/conf.d/99-sail.ini
RUN chmod +x /usr/local/bin/start-container
EXPOSE 80/tcp
ENTRYPOINT ["start-container"]

Some files were not shown because too many files have changed in this diff Show More