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