diff --git a/app/Http/Controllers/Admin/AdminExtraController.php b/app/Http/Controllers/Admin/AdminExtraController.php
index 611a946..f241ace 100644
--- a/app/Http/Controllers/Admin/AdminExtraController.php
+++ b/app/Http/Controllers/Admin/AdminExtraController.php
@@ -360,66 +360,242 @@ public function refundPayment(Request $request, $id)
/**
* Interactive Reports Hub
*/
- public function analytics()
+ /**
+ * Interactive Reports Hub
+ */
+ public function analytics(Request $request)
{
- // 1. Worker Stats
- $workerStats = [
- 'total' => DB::table('workers')->count(),
- 'verified' => DB::table('workers')->where('verified', 1)->count(),
- 'active' => DB::table('workers')->where('status', 'active')->count(),
- 'inactive' => DB::table('workers')->where('status', 'inactive')->count(),
- ];
+ $reportType = $request->input('report_type', 'workers');
+ $startDate = $request->input('start_date');
+ $endDate = $request->input('end_date');
+ $status = $request->input('status', 'all');
+ $search = $request->input('search');
+ $export = $request->input('export');
- // 2. Sponsor Stats
- $sponsorStats = [
- 'total' => DB::table('sponsors')->count(),
- 'verified' => DB::table('sponsors')->where('is_verified', 1)->count(),
- 'active_subscribers' => DB::table('sponsors')->where('subscription_status', 'active')->count(),
- 'unpaid' => DB::table('sponsors')->where('payment_status', 'unpaid')->count(),
- ];
+ $headers = [];
+ $data = collect();
- // 3. Dispute & Safety Stats
- $disputeStats = [
- 'total' => DB::table('disputes')->count(),
- 'open' => DB::table('disputes')->where('status', 'Open')->count(),
- 'under_review' => DB::table('disputes')->where('status', 'Under Review')->count(),
- 'resolved' => DB::table('disputes')->where('status', 'Resolved')->count(),
- ];
+ if ($reportType === 'workers') {
+ $query = DB::table('workers');
+ if ($startDate) {
+ $query->whereDate('created_at', '>=', $startDate);
+ }
+ if ($endDate) {
+ $query->whereDate('created_at', '<=', $endDate);
+ }
+ if ($status && $status !== 'all') {
+ $query->where('status', $status);
+ }
+ if ($search) {
+ $query->where(function($q) use ($search) {
+ $q->where('name', 'like', "%{$search}%")
+ ->orWhere('email', 'like', "%{$search}%")
+ ->orWhere('nationality', 'like', "%{$search}%");
+ });
+ }
- $safetyStats = [
- 'total' => DB::table('moderation_reports')->count(),
- 'pending' => DB::table('moderation_reports')->where('status', 'Pending')->count(),
- 'resolved' => DB::table('moderation_reports')->where('status', 'Resolved')->count(),
- ];
-
- // 4. Financial Payments & Ledger
- $payments = DB::table('payments')
- ->leftJoin('users', 'payments.user_id', '=', 'users.id')
- ->select('payments.*', 'users.name as user_name', 'users.email as user_email')
- ->orderBy('payments.created_at', 'desc')
- ->get()
- ->map(function($payment) {
+ $headers = ['Worker ID', 'Name', 'Email', 'Phone', 'Nationality', 'Status', 'Salary (AED)', 'Verified', 'Joined Date'];
+ $data = $query->orderBy('created_at', 'desc')->get()->map(function($w) {
return [
- 'id' => $payment->id,
- 'user_name' => $payment->user_name ?: 'System Guest / Sponsor',
- 'user_email' => $payment->user_email ?: 'no-email@marketplace.com',
- 'amount' => (float)$payment->amount,
- 'currency' => $payment->currency,
- 'description' => $payment->description,
- 'status' => $payment->status,
- 'created_at' => date('M d, Y h:i A', strtotime($payment->created_at)),
+ 'id' => 'WRK-' . str_pad($w->id, 4, '0', STR_PAD_LEFT),
+ 'name' => $w->name,
+ 'email' => $w->email,
+ 'phone' => $w->phone ?: 'N/A',
+ 'nationality' => $w->nationality ?: 'N/A',
+ 'status' => $w->status,
+ 'salary' => (int)$w->salary,
+ 'verified' => $w->verified ? 'Yes' : 'No',
+ 'joined_at' => date('Y-m-d', strtotime($w->created_at)),
];
});
- $totalRevenue = DB::table('payments')->where('status', 'success')->sum('amount');
+ } elseif ($reportType === 'employers') {
+ $query = DB::table('users')
+ ->leftJoin('employer_profiles', 'users.id', '=', 'employer_profiles.user_id')
+ ->leftJoin('sponsors', 'users.email', '=', 'sponsors.email')
+ ->where('users.role', 'employer')
+ ->select(
+ 'users.id',
+ 'users.name',
+ 'users.email',
+ 'sponsors.status as sponsor_status',
+ 'sponsors.subscription_status',
+ 'users.created_at'
+ );
+
+ if ($startDate) {
+ $query->whereDate('users.created_at', '>=', $startDate);
+ }
+ if ($endDate) {
+ $query->whereDate('users.created_at', '<=', $endDate);
+ }
+ if ($status && $status !== 'all') {
+ $query->where('sponsors.subscription_status', $status);
+ }
+ if ($search) {
+ $query->where(function($q) use ($search) {
+ $q->where('users.name', 'like', "%{$search}%")
+ ->orWhere('users.email', 'like', "%{$search}%");
+ });
+ }
+
+ $headers = ['Employer ID', 'Name', 'Email', 'Status', 'Subscription Status', 'Total Spent', 'Joined Date'];
+ $data = $query->orderBy('users.created_at', 'desc')->get()->map(function($emp) {
+ $totalPaid = DB::table('payments')->where('user_id', $emp->id)->where('status', 'success')->sum('amount');
+ return [
+ 'id' => 'EMP-' . str_pad($emp->id, 4, '0', STR_PAD_LEFT),
+ 'name' => $emp->name,
+ 'email' => $emp->email,
+ 'status' => ucfirst($emp->sponsor_status ?? 'Active'),
+ 'subscription' => ucfirst($emp->subscription_status ?? 'Inactive'),
+ 'total_spent' => number_format($totalPaid, 2) . ' AED',
+ 'joined_at' => date('Y-m-d', strtotime($emp->created_at)),
+ ];
+ });
+
+ } elseif ($reportType === 'payments') {
+ $query = DB::table('payments')
+ ->leftJoin('users', 'payments.user_id', '=', 'users.id')
+ ->select('payments.*', 'users.name as user_name', 'users.email as user_email');
+
+ if ($startDate) {
+ $query->whereDate('payments.created_at', '>=', $startDate);
+ }
+ if ($endDate) {
+ $query->whereDate('payments.created_at', '<=', $endDate);
+ }
+ if ($status && $status !== 'all') {
+ $query->where('payments.status', $status);
+ }
+ if ($search) {
+ $query->where(function($q) use ($search) {
+ $q->where('users.name', 'like', "%{$search}%")
+ ->orWhere('payments.description', 'like', "%{$search}%");
+ });
+ }
+
+ $headers = ['Payment ID', 'Employer Name', 'Employer Email', 'Amount', 'Description', 'Status', 'Date'];
+ $data = $query->orderBy('payments.created_at', 'desc')->get()->map(function($pay) {
+ return [
+ 'id' => 'PAY-' . str_pad($pay->id, 4, '0', STR_PAD_LEFT),
+ 'employer_name' => $pay->user_name ?: 'System Guest / Sponsor',
+ 'employer_email' => $pay->user_email ?: 'no-email@marketplace.com',
+ 'amount' => number_format((float)$pay->amount, 2) . ' AED',
+ 'description' => $pay->description ?: 'Subscription Plan',
+ 'status' => ucfirst($pay->status),
+ 'date' => date('Y-m-d H:i', strtotime($pay->created_at)),
+ ];
+ });
+
+ } elseif ($reportType === 'tickets') {
+ $query = DB::table('support_tickets')
+ ->leftJoin('users', 'support_tickets.user_id', '=', 'users.id')
+ ->leftJoin('report_reasons', 'support_tickets.reason_id', '=', 'report_reasons.id')
+ ->select('support_tickets.*', 'users.name as user_name', 'users.email as user_email', 'report_reasons.reason as reason_name');
+
+ if ($startDate) {
+ $query->whereDate('support_tickets.created_at', '>=', $startDate);
+ }
+ if ($endDate) {
+ $query->whereDate('support_tickets.created_at', '<=', $endDate);
+ }
+ if ($status && $status !== 'all') {
+ $query->where('support_tickets.status', $status);
+ }
+ if ($search) {
+ $query->where(function($q) use ($search) {
+ $q->where('users.name', 'like', "%{$search}%")
+ ->orWhere('support_tickets.subject', 'like', "%{$search}%");
+ });
+ }
+
+ $headers = ['Ticket ID', 'User Name', 'User Email', 'Subject', 'Reason', 'Status', 'Voice Note Attached', 'Created Date'];
+ $data = $query->orderBy('support_tickets.created_at', 'desc')->get()->map(function($tick) {
+ return [
+ 'id' => 'TCK-' . str_pad($tick->id, 4, '0', STR_PAD_LEFT),
+ 'user_name' => $tick->user_name ?: 'System User',
+ 'user_email' => $tick->user_email ?: 'N/A',
+ 'subject' => $tick->subject,
+ 'reason' => $tick->reason_name ?: 'General Support',
+ 'status' => ucfirst($tick->status),
+ 'has_voice_note' => $tick->voice_note_path ? 'Yes' : 'No',
+ 'created_at' => date('Y-m-d H:i', strtotime($tick->created_at)),
+ ];
+ });
+
+ } elseif ($reportType === 'safety') {
+ $query = DB::table('moderation_reports');
+
+ if ($startDate) {
+ $query->whereDate('reported_at', '>=', $startDate);
+ }
+ if ($endDate) {
+ $query->whereDate('reported_at', '<=', $endDate);
+ }
+ if ($status && $status !== 'all') {
+ $query->where('status', $status);
+ }
+ if ($search) {
+ $query->where(function($q) use ($search) {
+ $q->where('reported_user_name', 'like', "%{$search}%")
+ ->orWhere('reported_by_name', 'like', "%{$search}%");
+ });
+ }
+
+ $headers = ['Report ID', 'Reported User', 'Reported By', 'Reason', 'Status', 'Reported At'];
+ $data = $query->orderBy('reported_at', 'desc')->get()->map(function($rep) {
+ return [
+ 'id' => $rep->id,
+ 'reported_user' => $rep->reported_user_name . ' (' . $rep->reported_user_role . ')',
+ 'reported_by' => $rep->reported_by_name . ' (' . $rep->reported_by_role . ')',
+ 'reason' => $rep->reason,
+ 'status' => ucfirst($rep->status),
+ 'reported_at' => date('Y-m-d H:i', strtotime($rep->reported_at)),
+ ];
+ });
+ }
+
+ if ($export === 'csv') {
+ $filename = "report_" . $reportType . "_" . date('Ymd_His') . ".csv";
+ $callback = function() use ($data, $headers) {
+ $file = fopen('php://output', 'w');
+
+ // UTF-8 BOM
+ fprintf($file, chr(0xEF).chr(0xBB).chr(0xBF));
+
+ fputcsv($file, $headers);
+ foreach ($data as $row) {
+ fputcsv($file, array_values((array)$row));
+ }
+ fclose($file);
+ };
+
+ return response()->stream($callback, 200, [
+ "Content-type" => "text/csv; charset=UTF-8",
+ "Content-Disposition" => "attachment; filename={$filename}",
+ "Pragma" => "no-cache",
+ "Cache-Control" => "must-revalidate, post-check=0, pre-check=0",
+ "Expires" => "0"
+ ]);
+ }
+
+ // Fetch distinct nationalities for filter
+ $nationalities = DB::table('workers')
+ ->whereNotNull('nationality')
+ ->where('nationality', '!=', '')
+ ->distinct()
+ ->pluck('nationality')
+ ->toArray();
return Inertia::render('Admin/Analytics/Index', [
- 'worker_stats' => $workerStats,
- 'sponsor_stats' => $sponsorStats,
- 'dispute_stats' => $disputeStats,
- 'safety_stats' => $safetyStats,
- 'payments' => $payments,
- 'total_revenue' => (float)$totalRevenue,
+ 'reportType' => $reportType,
+ 'startDate' => $startDate ?: '',
+ 'endDate' => $endDate ?: '',
+ 'status' => $status,
+ 'search' => $search ?: '',
+ 'headers' => $headers,
+ 'reportData' => $data->toArray(),
+ 'nationalities' => $nationalities,
]);
}
diff --git a/app/Http/Controllers/Api/EmployerMessageController.php b/app/Http/Controllers/Api/EmployerMessageController.php
index 5f66ffe..e2ab989 100644
--- a/app/Http/Controllers/Api/EmployerMessageController.php
+++ b/app/Http/Controllers/Api/EmployerMessageController.php
@@ -101,7 +101,7 @@ public function getConversations(Request $request)
'worker_name' => $conv->worker->name ?? 'Candidate',
'nationality' => $conv->worker->nationality ?? 'Unknown',
'worker_status' => strtolower($conv->worker->status ?? 'active'),
- 'salary' => ($conv->worker->salary ?? $conv->worker->expected_salary ?? 2000) . ' AED',
+ 'salary' => ($conv->worker->salary ?? 2000) . ' AED',
'last_message' => $lastMsg->text ?? 'No messages yet.',
'unread_count' => $unreadCount,
'sent_at' => $lastMsg ? $lastMsg->created_at->diffForHumans() : 'Just now',
@@ -176,7 +176,7 @@ public function getMessages(Request $request, $id)
'worker_name' => $conv->worker->name ?? 'Candidate',
'nationality' => $conv->worker->nationality ?? 'Unknown',
'worker_status' => strtolower($conv->worker->status ?? 'active'),
- 'salary' => ($conv->worker->salary ?? $conv->worker->expected_salary ?? 2000) . ' AED',
+ 'salary' => ($conv->worker->salary ?? 2000) . ' AED',
];
return response()->json([
diff --git a/app/Http/Controllers/Employer/JobController.php b/app/Http/Controllers/Employer/JobController.php
index 822495c..b028fab 100644
--- a/app/Http/Controllers/Employer/JobController.php
+++ b/app/Http/Controllers/Employer/JobController.php
@@ -123,7 +123,7 @@ public function applicants($id)
'id' => $worker->id,
'name' => $worker->name,
'nationality' => $worker->nationality,
- 'salary' => (int) $worker->expected_salary,
+ 'salary' => (int) $worker->salary,
'experience' => ($worker->experience_years ?? 3) . ' Years',
'status' => ucfirst($app->status), // Applied, Shortlisted, Hired, Rejected
'match_score' => $worker->match_score ?? 90,
diff --git a/app/Http/Controllers/Employer/MessageController.php b/app/Http/Controllers/Employer/MessageController.php
index b5418e1..5c02382 100644
--- a/app/Http/Controllers/Employer/MessageController.php
+++ b/app/Http/Controllers/Employer/MessageController.php
@@ -167,7 +167,7 @@ public function show($id)
'worker_name' => $activeConv->worker->name ?? 'Candidate',
'worker_status' => strtolower($activeConv->worker->status ?? 'active'),
'online' => true,
- 'salary' => ($activeConv->worker->expected_salary ?? 2000) . ' AED',
+ 'salary' => ($activeConv->worker->salary ?? 2000) . ' AED',
'nationality' => $activeConv->worker->nationality ?? 'Unknown',
];
diff --git a/resources/js/Pages/Admin/Analytics/Index.jsx b/resources/js/Pages/Admin/Analytics/Index.jsx
index 0396822..8eb6f05 100644
--- a/resources/js/Pages/Admin/Analytics/Index.jsx
+++ b/resources/js/Pages/Admin/Analytics/Index.jsx
@@ -1,281 +1,412 @@
-import React, { useState } from 'react';
-import { Head } from '@inertiajs/react';
+import React, { useState, useEffect } from 'react';
+import { Head, router } from '@inertiajs/react';
import AdminLayout from '@/Layouts/AdminLayout';
import {
- FileText,
Users,
+ Building2,
ShieldAlert,
- CreditCard,
+ DollarSign,
+ LifeBuoy,
Search,
RefreshCw,
Download,
- Building2,
- DollarSign
+ Calendar,
+ Filter,
+ FileSpreadsheet,
+ Globe,
+ X,
+ FileText
} from 'lucide-react';
-export default function AnalyticsHub({
- worker_stats = { total: 0, verified: 0, active: 0, inactive: 0 },
- sponsor_stats = { total: 0, verified: 0, active_subscribers: 0, unpaid: 0 },
- dispute_stats = { total: 0, open: 0, under_review: 0, resolved: 0 },
- safety_stats = { total: 0, pending: 0, resolved: 0 },
- payments = [],
- total_revenue = 0
+export default function ReportsHub({
+ reportType = 'workers',
+ startDate = '',
+ endDate = '',
+ status = 'all',
+ search = '',
+ headers = [],
+ reportData = [],
+ nationalities = []
}) {
- const [searchTerm, setSearchTerm] = useState('');
- const [statusFilter, setStatusFilter] = useState('all');
+ // Local filter state
+ const [localStartDate, setLocalStartDate] = useState(startDate);
+ const [localEndDate, setLocalEndDate] = useState(endDate);
+ const [localStatus, setLocalStatus] = useState(status);
+ const [localSearch, setLocalSearch] = useState(search);
+ const [localNationality, setLocalNationality] = useState('all');
- // Filter payments dynamically
- const filteredPayments = payments.filter(payment => {
- const matchesSearch =
- payment.id.toString().toLowerCase().includes(searchTerm.toLowerCase()) ||
- payment.user_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
- payment.user_email.toLowerCase().includes(searchTerm.toLowerCase()) ||
- payment.description.toLowerCase().includes(searchTerm.toLowerCase());
-
- const matchesStatus = statusFilter === 'all' || payment.status.toLowerCase() === statusFilter.toLowerCase();
+ // Sync with props when props change (e.g. after tab switch)
+ useEffect(() => {
+ setLocalStartDate(startDate);
+ setLocalEndDate(endDate);
+ setLocalStatus(status);
+ setLocalSearch(search);
+ }, [reportType, startDate, endDate, status, search]);
- return matchesSearch && matchesStatus;
- });
+ const handleTabChange = (type) => {
+ router.get('/admin/analytics', {
+ report_type: type,
+ start_date: localStartDate,
+ end_date: localEndDate,
+ status: 'all',
+ search: ''
+ }, {
+ preserveState: false
+ });
+ };
+
+ const applyFilters = () => {
+ router.get('/admin/analytics', {
+ report_type: reportType,
+ start_date: localStartDate,
+ end_date: localEndDate,
+ status: localStatus,
+ search: localSearch,
+ nationality: reportType === 'workers' ? localNationality : undefined
+ }, {
+ preserveState: true,
+ replace: true
+ });
+ };
+
+ const resetFilters = () => {
+ setLocalStartDate('');
+ setLocalEndDate('');
+ setLocalStatus('all');
+ setLocalSearch('');
+ setLocalNationality('all');
+
+ router.get('/admin/analytics', {
+ report_type: reportType,
+ start_date: '',
+ end_date: '',
+ status: 'all',
+ search: ''
+ });
+ };
const handleExportCSV = () => {
- const headers = ['Payment ID,Customer,Email,Details,Amount,Status,Date\n'];
- const rows = filteredPayments.map(p =>
- `"${p.id}","${p.user_name}","${p.user_email}","${p.description}",${p.amount},"${p.status}","${p.created_at}"`
- );
- const blob = new Blob([headers.concat(rows.join('\n'))], { type: 'text/csv;charset=utf-8;' });
- const link = document.createElement('a');
- link.href = URL.createObjectURL(blob);
- link.setAttribute('download', `financial_report_${new Date().toISOString().split('T')[0]}.csv`);
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
+ const params = new URLSearchParams({
+ report_type: reportType,
+ start_date: localStartDate,
+ end_date: localEndDate,
+ status: localStatus,
+ search: localSearch,
+ export: 'csv'
+ });
+ if (reportType === 'workers' && localNationality !== 'all') {
+ params.append('nationality', localNationality);
+ }
+ window.location.href = `/admin/analytics?${params.toString()}`;
+ };
+
+ // Context-aware status list for dropdown
+ const getStatusOptions = () => {
+ switch (reportType) {
+ case 'workers':
+ return [
+ { value: 'all', label: 'All Statuses' },
+ { value: 'Available', label: 'Available' },
+ { value: 'hired', label: 'Hired' },
+ { value: 'inactive', label: 'Inactive' },
+ { value: 'suspended', label: 'Suspended' },
+ { value: 'banned', label: 'Banned' }
+ ];
+ case 'employers':
+ return [
+ { value: 'all', label: 'All Subscriptions' },
+ { value: 'active', label: 'Active Pass' },
+ { value: 'inactive', label: 'Inactive Pass' }
+ ];
+ case 'payments':
+ return [
+ { value: 'all', label: 'All Payments' },
+ { value: 'success', label: 'Success' },
+ { value: 'failed', label: 'Failed' },
+ { value: 'refunded', label: 'Refunded' }
+ ];
+ case 'tickets':
+ return [
+ { value: 'all', label: 'All Statuses' },
+ { value: 'Open', label: 'Open' },
+ { value: 'In-Progress', label: 'In-Progress' },
+ { value: 'Resolved', label: 'Resolved' },
+ { value: 'Closed', label: 'Closed' }
+ ];
+ case 'safety':
+ return [
+ { value: 'all', label: 'All Statuses' },
+ { value: 'Pending', label: 'Pending' },
+ { value: 'Resolved', label: 'Resolved' }
+ ];
+ default:
+ return [{ value: 'all', label: 'All Statuses' }];
+ }
+ };
+
+ const tabs = [
+ { id: 'workers', label: 'Workers List', icon: Users, desc: 'Registration & status report' },
+ { id: 'employers', label: 'Employers List', icon: Building2, desc: 'Subscription & spending report' },
+ { id: 'payments', label: 'Payments Ledger', icon: DollarSign, desc: 'Revenue & transactions report' },
+ { id: 'tickets', label: 'Support Inquiries', icon: LifeBuoy, desc: 'Inquiries & voice notes report' },
+ { id: 'safety', label: 'Safety Moderation', icon: ShieldAlert, desc: 'Moderation & flagged user reports' },
+ ];
+
+ const renderCell = (val, cellIdx, type) => {
+ const lowerVal = String(val).toLowerCase();
+
+ // Status Badges
+ if (['active', 'available', 'success', 'yes', 'approved', 'resolved'].includes(lowerVal)) {
+ return (
+
+
+ {val}
+
+ );
+ }
+ if (['pending', 'in-progress', 'under review', 'no'].includes(lowerVal)) {
+ return (
+
+
+ {val}
+
+ );
+ }
+ if (['failed', 'inactive', 'suspended', 'banned', 'closed', 'rejected'].includes(lowerVal)) {
+ return (
+
+
+ {val}
+
+ );
+ }
+
+ // ID Formats
+ if (typeof val === 'string' && (val.startsWith('WRK-') || val.startsWith('EMP-') || val.startsWith('PAY-') || val.startsWith('TCK-') || val.startsWith('REP-'))) {
+ return (
+
+ {val}
+
+ );
+ }
+
+ // Email style
+ if (typeof val === 'string' && val.includes('@')) {
+ return {val};
+ }
+
+ return {val};
};
return (
-
Summary of workers, employers, disputes, and payments.
+ {/* Header Title & Description */} ++ Gain direct access to database operational reports, track registrations, monitor logs, analyze payment histories, and export filtered tabular datasets. +
Search and check payments.
-