Initial commit
This commit is contained in:
18
.editorconfig
Normal file
18
.editorconfig
Normal 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
65
.env.example
Normal 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
10
.gitattributes
vendored
Normal 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
51
.github/prompt.yml
vendored
Normal 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 user’s 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
46
.github/workflows/lint.yml
vendored
Normal 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
54
.github/workflows/tests.yml
vendored
Normal 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
23
.gitignore
vendored
Normal 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
|
||||
55
app/Console/Commands/TestTraccarConnection.php
Normal file
55
app/Console/Commands/TestTraccarConnection.php
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
24
app/Http/Controllers/Auth/VerifyEmailController.php
Normal file
24
app/Http/Controllers/Auth/VerifyEmailController.php
Normal 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');
|
||||
}
|
||||
}
|
||||
8
app/Http/Controllers/Controller.php
Normal file
8
app/Http/Controllers/Controller.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
42
app/Http/Middleware/EnsureUserIsActive.php
Normal file
42
app/Http/Middleware/EnsureUserIsActive.php
Normal 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);
|
||||
}
|
||||
}
|
||||
28
app/Listeners/UpdateLastLoginTime.php
Normal file
28
app/Listeners/UpdateLastLoginTime.php
Normal 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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
22
app/Livewire/Actions/Logout.php
Normal file
22
app/Livewire/Actions/Logout.php
Normal 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('/');
|
||||
}
|
||||
}
|
||||
123
app/Livewire/AdminDashboard.php
Normal file
123
app/Livewire/AdminDashboard.php
Normal 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');
|
||||
}
|
||||
}
|
||||
165
app/Livewire/CommandCenter.php
Normal file
165
app/Livewire/CommandCenter.php
Normal 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
191
app/Livewire/Dashboard.php
Normal 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');
|
||||
}
|
||||
}
|
||||
418
app/Livewire/DeviceManagement.php
Normal file
418
app/Livewire/DeviceManagement.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
289
app/Livewire/DriverManagement.php
Normal file
289
app/Livewire/DriverManagement.php
Normal 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();
|
||||
}
|
||||
}
|
||||
416
app/Livewire/EventsAndAlerts.php
Normal file
416
app/Livewire/EventsAndAlerts.php
Normal 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
|
||||
]);
|
||||
}
|
||||
}
|
||||
255
app/Livewire/GeofenceManagement.php
Normal file
255
app/Livewire/GeofenceManagement.php
Normal 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
|
||||
]);
|
||||
}
|
||||
}
|
||||
405
app/Livewire/LiveTracking.php
Normal file
405
app/Livewire/LiveTracking.php
Normal 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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
393
app/Livewire/LiveTracking.php.backup
Normal file
393
app/Livewire/LiveTracking.php.backup
Normal 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
|
||||
]);
|
||||
}
|
||||
}
|
||||
54
app/Livewire/MapSettings.php
Normal file
54
app/Livewire/MapSettings.php
Normal 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');
|
||||
}
|
||||
}
|
||||
234
app/Livewire/NotificationCenter.php
Normal file
234
app/Livewire/NotificationCenter.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
406
app/Livewire/ReportsAndHistory.php
Normal file
406
app/Livewire/ReportsAndHistory.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
256
app/Livewire/SubscriptionManagement.php
Normal file
256
app/Livewire/SubscriptionManagement.php
Normal 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();
|
||||
}
|
||||
}
|
||||
208
app/Livewire/UserManagement.php
Normal file
208
app/Livewire/UserManagement.php
Normal 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
83
app/Models/Command.php
Normal 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
172
app/Models/Device.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
58
app/Models/DeviceGroup.php
Normal file
58
app/Models/DeviceGroup.php
Normal 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
61
app/Models/Driver.php
Normal 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
134
app/Models/Event.php
Normal 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
83
app/Models/Geofence.php
Normal 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'
|
||||
};
|
||||
}
|
||||
}
|
||||
43
app/Models/NotificationPreference.php
Normal file
43
app/Models/NotificationPreference.php
Normal 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);
|
||||
}
|
||||
}
|
||||
102
app/Models/NotificationSetting.php
Normal file
102
app/Models/NotificationSetting.php
Normal 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
10
app/Models/Permission.php
Normal 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
128
app/Models/Position.php
Normal 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
10
app/Models/Role.php
Normal 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
129
app/Models/Subscription.php
Normal 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
186
app/Models/User.php
Normal 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('');
|
||||
}
|
||||
}
|
||||
28
app/Providers/AppServiceProvider.php
Normal file
28
app/Providers/AppServiceProvider.php
Normal 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);
|
||||
}
|
||||
}
|
||||
28
app/Providers/VoltServiceProvider.php
Normal file
28
app/Providers/VoltServiceProvider.php
Normal 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
134
app/Services/MapService.php
Normal 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;
|
||||
}
|
||||
}
|
||||
385
app/Services/TraccarService.php
Normal file
385
app/Services/TraccarService.php
Normal 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
18
artisan
Executable 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
28
bootstrap/app.php
Normal 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
2
bootstrap/cache/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
6
bootstrap/providers.php
Normal file
6
bootstrap/providers.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\VoltServiceProvider::class,
|
||||
];
|
||||
85
composer.json
Normal file
85
composer.json
Normal 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
9938
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
126
config/app.php
Normal file
126
config/app.php
Normal 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
115
config/auth.php
Normal 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
32
config/boost.php
Normal 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
108
config/cache.php
Normal 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
182
config/database.php
Normal 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
338
config/debugbar.php
Normal 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
80
config/filesystems.php
Normal 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
160
config/livewire.php
Normal 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
132
config/logging.php
Normal 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
116
config/mail.php
Normal 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
202
config/permission.php
Normal 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
112
config/queue.php
Normal 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
91
config/services.php
Normal 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' => '© <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' => '© <a href="https://www.mapbox.com/">Mapbox</a> © <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' => '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <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 © Esri — 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
217
config/session.php
Normal 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
50
config/tinker.php
Normal 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
1
database/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.sqlite*
|
||||
44
database/factories/UserFactory.php
Normal file
44
database/factories/UserFactory.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
49
database/migrations/0001_01_01_000000_create_users_table.php
Normal file
49
database/migrations/0001_01_01_000000_create_users_table.php
Normal 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');
|
||||
}
|
||||
};
|
||||
35
database/migrations/0001_01_01_000001_create_cache_table.php
Normal file
35
database/migrations/0001_01_01_000001_create_cache_table.php
Normal 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');
|
||||
}
|
||||
};
|
||||
57
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal file
57
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal 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');
|
||||
}
|
||||
};
|
||||
@ -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']);
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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'
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
31
database/migrations/2025_09_10_150429_create_roles_table.php
Normal file
31
database/migrations/2025_09_10_150429_create_roles_table.php
Normal 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');
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
47
database/seeders/DatabaseSeeder.php
Normal file
47
database/seeders/DatabaseSeeder.php
Normal 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');
|
||||
}
|
||||
}
|
||||
141
database/seeders/RolePermissionSeeder.php
Normal file
141
database/seeders/RolePermissionSeeder.php
Normal 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
70
docker/8.0/Dockerfile
Normal 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
Reference in New Issue
Block a user