initial
This commit is contained in:
commit
452c1f909c
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
|
||||
|
||||
[{compose,docker-compose}.{yml,yaml}]
|
||||
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}"
|
||||
11
.gitattributes
vendored
Normal file
11
.gitattributes
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpactor.json
|
||||
.phpunit.result.cache
|
||||
/.codex
|
||||
/.cursor/
|
||||
/.idea
|
||||
/.nova
|
||||
/.phpunit.cache
|
||||
/.vscode
|
||||
/.zed
|
||||
/auth.json
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/fonts-manifest.dev.json
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
/vendor
|
||||
_ide_helper.php
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
Thumbs.db
|
||||
58
README.md
Normal file
58
README.md
Normal file
@ -0,0 +1,58 @@
|
||||
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
||||
</p>
|
||||
|
||||
## About Laravel
|
||||
|
||||
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
||||
|
||||
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
||||
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
||||
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
||||
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
||||
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
||||
- [Robust background job processing](https://laravel.com/docs/queues).
|
||||
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
||||
|
||||
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
||||
|
||||
## Learning Laravel
|
||||
|
||||
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
|
||||
|
||||
In addition, [Laracasts](https://laracasts.com) contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
||||
|
||||
You can also watch bite-sized lessons with real-world projects on [Laravel Learn](https://laravel.com/learn), where you will be guided through building a Laravel application from scratch while learning PHP fundamentals.
|
||||
|
||||
## Agentic Development
|
||||
|
||||
Laravel's predictable structure and conventions make it ideal for AI coding agents like Claude Code, Cursor, and GitHub Copilot. Install [Laravel Boost](https://laravel.com/docs/ai) to supercharge your AI workflow:
|
||||
|
||||
```bash
|
||||
composer require laravel/boost --dev
|
||||
|
||||
php artisan boost:install
|
||||
```
|
||||
|
||||
Boost provides your agent 15+ tools and skills that help agents build Laravel applications while following best practices.
|
||||
|
||||
## Contributing
|
||||
|
||||
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
||||
|
||||
## Security Vulnerabilities
|
||||
|
||||
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
||||
|
||||
## License
|
||||
|
||||
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||
BIN
Screenshot 2026-05-15 143146.pdf
Normal file
BIN
Screenshot 2026-05-15 143146.pdf
Normal file
Binary file not shown.
60
app/Http/Controllers/Admin/AdminAuthController.php
Normal file
60
app/Http/Controllers/Admin/AdminAuthController.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class AdminAuthController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the admin login page.
|
||||
*/
|
||||
public function showLogin()
|
||||
{
|
||||
return Inertia::render('Admin/Auth/Login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an admin login attempt.
|
||||
*/
|
||||
public function login(Request $request)
|
||||
{
|
||||
$credentials = $request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
'password' => ['required'],
|
||||
]);
|
||||
|
||||
// Mock authentication check for scaffolding without database driver dependency
|
||||
if ($credentials['email'] === 'admin@example.com' && $credentials['password'] === 'password') {
|
||||
session(['user' => (object)[
|
||||
'id' => 1,
|
||||
'name' => 'Portal Admin',
|
||||
'email' => 'admin@example.com',
|
||||
'role' => 'admin',
|
||||
]]);
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect()->intended('/admin/dashboard');
|
||||
}
|
||||
|
||||
return back()->withErrors([
|
||||
'email' => 'The provided credentials do not match our records. (Use admin@example.com / password)',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the admin out of the application.
|
||||
*/
|
||||
public function logout(Request $request)
|
||||
{
|
||||
session()->forget('user');
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect()->route('admin.login');
|
||||
}
|
||||
}
|
||||
112
app/Http/Controllers/Admin/DashboardController.php
Normal file
112
app/Http/Controllers/Admin/DashboardController.php
Normal file
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the admin dashboard overview.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$stats = [
|
||||
'total_workers' => 1420,
|
||||
'pending_verifications' => 0, // No longer pending as they are auto-approved
|
||||
'active_subscriptions' => 312,
|
||||
'revenue_this_month_aed' => 48500,
|
||||
'new_employers_this_week' => 28,
|
||||
];
|
||||
|
||||
// This section now tracks recently auto-processed verifications instead of pending ones
|
||||
$recentVerifications = [
|
||||
[
|
||||
'id' => 101,
|
||||
'name' => 'Fatima Zahra',
|
||||
'type' => 'Worker',
|
||||
'processed_at' => '2 hours ago',
|
||||
'document_type' => 'Passport & Visa',
|
||||
'status' => 'Auto-Approved',
|
||||
],
|
||||
[
|
||||
'id' => 102,
|
||||
'name' => 'Ahmed Mansoor',
|
||||
'type' => 'Employer',
|
||||
'processed_at' => '4 hours ago',
|
||||
'document_type' => 'Emirates ID',
|
||||
'status' => 'Auto-Approved',
|
||||
],
|
||||
];
|
||||
|
||||
$recentSubscriptions = [
|
||||
[
|
||||
'id' => 501,
|
||||
'employer_name' => 'Hassan Al Hosani',
|
||||
'plan_name' => 'Premium Employer Plan',
|
||||
'amount_aed' => 499,
|
||||
'subscribed_at' => 'Today, 10:30 AM',
|
||||
],
|
||||
[
|
||||
'id' => 502,
|
||||
'employer_name' => 'Sarah Sterling',
|
||||
'plan_name' => 'Standard Search Plan',
|
||||
'amount_aed' => 199,
|
||||
'subscribed_at' => 'Yesterday',
|
||||
],
|
||||
[
|
||||
'id' => 503,
|
||||
'employer_name' => 'Rashid Al Nuaimi',
|
||||
'plan_name' => 'Enterprise VIP',
|
||||
'amount_aed' => 1299,
|
||||
'subscribed_at' => 'May 12, 2026',
|
||||
],
|
||||
[
|
||||
'id' => 504,
|
||||
'employer_name' => 'Elena Rostova',
|
||||
'plan_name' => 'Standard Search Plan',
|
||||
'amount_aed' => 199,
|
||||
'subscribed_at' => 'May 11, 2026',
|
||||
],
|
||||
[
|
||||
'id' => 505,
|
||||
'employer_name' => 'Omar Farooq',
|
||||
'plan_name' => 'Premium Employer Plan',
|
||||
'amount_aed' => 499,
|
||||
'subscribed_at' => 'May 10, 2026',
|
||||
],
|
||||
];
|
||||
|
||||
return Inertia::render('Admin/Dashboard', [
|
||||
'stats' => $stats,
|
||||
'recent_verifications' => $recentVerifications,
|
||||
'recent_subscriptions' => $recentSubscriptions,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve a pending verification (Legacy/Manual Override).
|
||||
*/
|
||||
public function approveVerification(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'id' => 'required|integer',
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Verification document #' . $validated['id'] . ' has been approved manually.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a pending verification (Legacy/Manual Override).
|
||||
*/
|
||||
public function rejectVerification(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'id' => 'required|integer',
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Verification document #' . $validated['id'] . ' has been rejected.');
|
||||
}
|
||||
}
|
||||
89
app/Http/Controllers/Admin/WorkerController.php
Normal file
89
app/Http/Controllers/Admin/WorkerController.php
Normal file
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class WorkerController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of all workers.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
// Scaffolding mock dataset for worker management
|
||||
$workers = [
|
||||
[
|
||||
'id' => 101,
|
||||
'name' => 'Maria Santos',
|
||||
'email' => 'maria.santos@example.com',
|
||||
'nationality' => 'Philippines',
|
||||
'category' => 'Childcare',
|
||||
'experience' => '5+ Years',
|
||||
'status' => 'active',
|
||||
'joined_at' => '2026-01-12',
|
||||
],
|
||||
[
|
||||
'id' => 102,
|
||||
'name' => 'Lakshmi Sharma',
|
||||
'email' => 'l.sharma@example.com',
|
||||
'nationality' => 'India',
|
||||
'category' => 'Elderly Care',
|
||||
'experience' => '3-5 Years',
|
||||
'status' => 'active',
|
||||
'joined_at' => '2026-02-05',
|
||||
],
|
||||
[
|
||||
'id' => 103,
|
||||
'name' => 'Siti Aminah',
|
||||
'email' => 'siti.a@example.com',
|
||||
'nationality' => 'Indonesia',
|
||||
'category' => 'Housekeeping',
|
||||
'experience' => '2 Years',
|
||||
'status' => 'inactive',
|
||||
'joined_at' => '2026-03-18',
|
||||
],
|
||||
[
|
||||
'id' => 104,
|
||||
'name' => 'Fatima Zahra',
|
||||
'email' => 'fatima.z@example.com',
|
||||
'nationality' => 'Morocco',
|
||||
'category' => 'Cooking',
|
||||
'experience' => '8 Years',
|
||||
'status' => 'active',
|
||||
'joined_at' => '2026-04-02',
|
||||
],
|
||||
[
|
||||
'id' => 105,
|
||||
'name' => 'Grace Omondi',
|
||||
'email' => 'grace.o@example.com',
|
||||
'nationality' => 'Kenya',
|
||||
'category' => 'General Helper',
|
||||
'experience' => '4 Years',
|
||||
'status' => 'active',
|
||||
'joined_at' => '2026-04-25',
|
||||
],
|
||||
];
|
||||
|
||||
return Inertia::render('Admin/Workers/Index', [
|
||||
'workers' => $workers,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle worker active/inactive status.
|
||||
*/
|
||||
public function toggleStatus(Request $request, $id)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'status' => 'required|in:active,inactive',
|
||||
]);
|
||||
|
||||
// In a real app, you would update the database here:
|
||||
// User::where('id', $id)->update(['status' => $validated['status']]);
|
||||
|
||||
return back()->with('success', "Worker status has been updated to {$validated['status']}.");
|
||||
}
|
||||
}
|
||||
179
app/Http/Controllers/Admin/WorkerVerificationController.php
Normal file
179
app/Http/Controllers/Admin/WorkerVerificationController.php
Normal file
@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class WorkerVerificationController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of worker identity verifications.
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$status = $request->input('status', 'all');
|
||||
|
||||
// Scaffolding mock dataset supporting pagination and status filtering
|
||||
$allVerifications = [
|
||||
[
|
||||
'id' => 101,
|
||||
'worker_name' => 'Fatima Zahra',
|
||||
'nationality' => 'Morocco',
|
||||
'passport_number' => 'MA9823471',
|
||||
'processed_at' => '2026-05-14 10:15',
|
||||
'status' => 'approved',
|
||||
'verification_method' => 'Auto-OCR',
|
||||
'document_images' => [
|
||||
'https://images.unsplash.com/photo-1544717305-2782549b5136?q=80&w=600&auto=format&fit=crop',
|
||||
'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?q=80&w=600&auto=format&fit=crop'
|
||||
],
|
||||
'ocr_data' => [
|
||||
'Name' => 'Fatima Zahra',
|
||||
'DOB' => '1992-08-14',
|
||||
'Nationality' => 'Morocco',
|
||||
'Passport No.' => 'MA9823471',
|
||||
'Expiry' => '2030-11-20',
|
||||
]
|
||||
],
|
||||
[
|
||||
'id' => 102,
|
||||
'worker_name' => 'Maria Santos',
|
||||
'nationality' => 'Philippines',
|
||||
'passport_number' => 'P1234567A',
|
||||
'processed_at' => '2026-05-14 09:30',
|
||||
'status' => 'approved',
|
||||
'verification_method' => 'Auto-OCR',
|
||||
'document_images' => [
|
||||
'https://images.unsplash.com/photo-1580489944761-15a19d654956?q=80&w=600&auto=format&fit=crop'
|
||||
],
|
||||
'ocr_data' => [
|
||||
'Name' => 'Maria Santos',
|
||||
'DOB' => '1990-05-10',
|
||||
'Nationality' => 'Philippines',
|
||||
'Passport No.' => 'P1234567A',
|
||||
'Expiry' => '2029-04-15',
|
||||
]
|
||||
],
|
||||
[
|
||||
'id' => 103,
|
||||
'worker_name' => 'Amina Diop',
|
||||
'nationality' => 'Senegal',
|
||||
'passport_number' => 'SN8765432',
|
||||
'processed_at' => '2026-05-13 14:20',
|
||||
'status' => 'approved',
|
||||
'verification_method' => 'Auto-OCR',
|
||||
'document_images' => [
|
||||
'https://images.unsplash.com/photo-1531746020798-e6953c6e8e04?q=80&w=600&auto=format&fit=crop'
|
||||
],
|
||||
'ocr_data' => [
|
||||
'Name' => 'Amina Diop',
|
||||
'DOB' => '1994-12-01',
|
||||
'Nationality' => 'Senegal',
|
||||
'Passport No.' => 'SN8765432',
|
||||
'Expiry' => '2031-08-10',
|
||||
]
|
||||
],
|
||||
[
|
||||
'id' => 104,
|
||||
'worker_name' => 'Siti Aminah',
|
||||
'nationality' => 'Indonesia',
|
||||
'passport_number' => 'B76543210',
|
||||
'processed_at' => '2026-05-13 11:05',
|
||||
'status' => 'approved',
|
||||
'verification_method' => 'Auto-OCR',
|
||||
'document_images' => [
|
||||
'https://images.unsplash.com/photo-1534528741775-53994a69daeb?q=80&w=600&auto=format&fit=crop'
|
||||
],
|
||||
'ocr_data' => [
|
||||
'Name' => 'Siti Aminah',
|
||||
'DOB' => '1988-03-25',
|
||||
'Nationality' => 'Indonesia',
|
||||
'Passport No.' => 'B76543210',
|
||||
'Expiry' => '2028-09-30',
|
||||
]
|
||||
],
|
||||
[
|
||||
'id' => 105,
|
||||
'worker_name' => 'Grace Omondi',
|
||||
'nationality' => 'Kenya',
|
||||
'passport_number' => 'KE5432198',
|
||||
'processed_at' => '2026-05-12 16:45',
|
||||
'status' => 'approved',
|
||||
'verification_method' => 'Auto-OCR',
|
||||
'document_images' => [
|
||||
'https://images.unsplash.com/photo-1567532939604-b6b5b0db2604?q=80&w=600&auto=format&fit=crop'
|
||||
],
|
||||
'ocr_data' => [
|
||||
'Name' => 'Grace Omondi',
|
||||
'DOB' => '1995-07-19',
|
||||
'Nationality' => 'Kenya',
|
||||
'Passport No.' => 'KE5432198',
|
||||
'Expiry' => '2025-02-14',
|
||||
]
|
||||
],
|
||||
[
|
||||
'id' => 106,
|
||||
'worker_name' => 'Leila Bekri',
|
||||
'nationality' => 'Tunisia',
|
||||
'passport_number' => 'TN4321987',
|
||||
'processed_at' => '2026-05-12 10:10',
|
||||
'status' => 'approved',
|
||||
'verification_method' => 'Auto-OCR',
|
||||
'document_images' => [
|
||||
'https://images.unsplash.com/photo-1544005313-94ddf0286df2?q=80&w=600&auto=format&fit=crop'
|
||||
],
|
||||
'ocr_data' => [
|
||||
'Name' => 'Leila Bekri',
|
||||
'DOB' => '1991-09-08',
|
||||
'Nationality' => 'Tunisia',
|
||||
'Passport No.' => 'TN4321987',
|
||||
'Expiry' => '2032-01-25',
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
// Filter by status
|
||||
if ($status !== 'all') {
|
||||
$filtered = array_filter($allVerifications, function ($item) use ($status) {
|
||||
return $item['status'] === $status;
|
||||
});
|
||||
} else {
|
||||
$filtered = $allVerifications;
|
||||
}
|
||||
|
||||
$page = $request->input('page', 1);
|
||||
$perPage = 20;
|
||||
$total = count($filtered);
|
||||
$slice = array_slice($filtered, ($page - 1) * $perPage, $perPage);
|
||||
|
||||
// Generate paginator
|
||||
$paginator = new LengthAwarePaginator($slice, $total, $perPage, $page, [
|
||||
'path' => $request->url(),
|
||||
'query' => $request->query(),
|
||||
]);
|
||||
|
||||
return Inertia::render('Admin/Workers/Verifications', [
|
||||
'verifications' => $paginator,
|
||||
'status' => $status,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process verification approval or rejection.
|
||||
*/
|
||||
public function verify(Request $request, $worker)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'action' => 'required|in:approve,reject',
|
||||
'rejection_reason' => 'required_if:action,reject|nullable|string',
|
||||
'ocr_overrides' => 'nullable|array',
|
||||
]);
|
||||
|
||||
$statusMsg = $validated['action'] === 'approve' ? 'approved' : 'rejected';
|
||||
|
||||
return back()->with('success', "Worker verification #{$worker} has been {$statusMsg} successfully.");
|
||||
}
|
||||
}
|
||||
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
|
||||
{
|
||||
//
|
||||
}
|
||||
110
app/Http/Controllers/Employer/DashboardController.php
Normal file
110
app/Http/Controllers/Employer/DashboardController.php
Normal file
@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Employer;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$sess = session('user');
|
||||
$user = is_array($sess) ? (object)$sess : ($sess ?? (object)[
|
||||
'name' => 'John Doe',
|
||||
'subscription_status' => 'active',
|
||||
'subscription_expires_at' => now()->addDays(24)->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
return Inertia::render('Employer/Dashboard', [
|
||||
'employer' => [
|
||||
'name' => $user->name ?? 'John Doe',
|
||||
'subscription_status' => $user->subscription_status ?? 'active',
|
||||
'subscription_expires_at' => $user->subscription_expires_at ?? '2026-12-31',
|
||||
'plan_name' => 'Premium Employer Pass',
|
||||
],
|
||||
'stats' => [
|
||||
'shortlisted_count' => 4,
|
||||
'messages_sent' => 18,
|
||||
'profile_views_given' => 45,
|
||||
'days_remaining' => 24,
|
||||
],
|
||||
'shortlisted_workers' => [
|
||||
[
|
||||
'id' => 101,
|
||||
'name' => 'Maria Santos',
|
||||
'nationality' => 'Philippines',
|
||||
'skills' => ['Childcare', 'Cooking', 'Housekeeping'],
|
||||
'availability' => 'Immediate',
|
||||
'photo_url' => null,
|
||||
'verified' => true,
|
||||
],
|
||||
[
|
||||
'id' => 102,
|
||||
'name' => 'Lakshmi Sharma',
|
||||
'nationality' => 'India',
|
||||
'skills' => ['Elderly Care', 'Cooking'],
|
||||
'availability' => '2 Weeks',
|
||||
'photo_url' => null,
|
||||
'verified' => true,
|
||||
],
|
||||
[
|
||||
'id' => 103,
|
||||
'name' => 'Siti Aminah',
|
||||
'nationality' => 'Indonesia',
|
||||
'skills' => ['Housekeeping', 'Ironing'],
|
||||
'availability' => 'Immediate',
|
||||
'photo_url' => null,
|
||||
'verified' => true,
|
||||
],
|
||||
[
|
||||
'id' => 104,
|
||||
'name' => 'Grace Osei',
|
||||
'nationality' => 'Ghana',
|
||||
'skills' => ['Childcare', 'English Tutoring'],
|
||||
'availability' => '1 Month',
|
||||
'photo_url' => null,
|
||||
'verified' => false,
|
||||
],
|
||||
],
|
||||
'recent_messages' => [
|
||||
[
|
||||
'id' => 201,
|
||||
'worker_name' => 'Maria Santos',
|
||||
'last_message' => 'Yes ma\'am, I am available for a video interview tomorrow at 10 AM.',
|
||||
'unread' => true,
|
||||
'sent_at' => '10 mins ago',
|
||||
],
|
||||
[
|
||||
'id' => 202,
|
||||
'worker_name' => 'Lakshmi Sharma',
|
||||
'last_message' => 'I have 5 years of experience in Dubai taking care of elderly patients.',
|
||||
'unread' => true,
|
||||
'sent_at' => '2 hours ago',
|
||||
],
|
||||
[
|
||||
'id' => 203,
|
||||
'worker_name' => 'Siti Aminah',
|
||||
'last_message' => 'Thank ma\'am for shortlisting my profile. Please let me know your offer.',
|
||||
'unread' => false,
|
||||
'sent_at' => '1 day ago',
|
||||
],
|
||||
],
|
||||
'announcements' => [
|
||||
[
|
||||
'id' => 301,
|
||||
'title' => 'New Visa Regulations Update',
|
||||
'body' => 'The Ministry of Human Resources and Emiratisation has announced updated guidelines for domestic worker sponsorship starting this month.',
|
||||
'created_at' => 'May 10, 2026',
|
||||
],
|
||||
[
|
||||
'id' => 302,
|
||||
'title' => 'Enhanced OCR Verification Active',
|
||||
'body' => 'We have deployed upgraded OCR passport verification to ensure 100% legal compliance for all worker profiles on the platform.',
|
||||
'created_at' => 'May 1, 2026',
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
120
app/Http/Controllers/Employer/EmployerAuthController.php
Normal file
120
app/Http/Controllers/Employer/EmployerAuthController.php
Normal file
@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Employer;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class EmployerAuthController extends Controller
|
||||
{
|
||||
public function showLogin()
|
||||
{
|
||||
return Inertia::render('Employer/Auth/Login');
|
||||
}
|
||||
|
||||
public function login(Request $request)
|
||||
{
|
||||
$credentials = $request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
'password' => ['required'],
|
||||
]);
|
||||
|
||||
if ($credentials['email'] === 'pending@example.com') {
|
||||
return back()->with('status', 'pending_verification');
|
||||
}
|
||||
|
||||
if ($credentials['email'] === 'rejected@example.com') {
|
||||
return back()->with([
|
||||
'status' => 'rejected',
|
||||
'reason' => 'Emirates ID scan was blurry or illegible. Please provide a high-resolution color scan.',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($credentials['email'] === 'expired@example.com') {
|
||||
return back()->with('status', 'subscription_expired');
|
||||
}
|
||||
|
||||
if ($credentials['email'] === 'employer@example.com' && $credentials['password'] === 'password') {
|
||||
session(['user' => (object)[
|
||||
'id' => 2,
|
||||
'name' => 'John Doe (Employer)',
|
||||
'email' => 'employer@example.com',
|
||||
'role' => 'employer',
|
||||
'subscription_status' => 'active',
|
||||
'verification_status' => 'approved',
|
||||
]]);
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
if ($request->source === 'mobile') {
|
||||
return redirect('/mobile/employer/home');
|
||||
}
|
||||
|
||||
return redirect()->intended('/employer/dashboard');
|
||||
}
|
||||
|
||||
return back()->withErrors([
|
||||
'email' => 'Invalid credentials. Use employer@example.com (or test emails: pending@, rejected@, expired@ / password)',
|
||||
]);
|
||||
}
|
||||
|
||||
public function showRegister()
|
||||
{
|
||||
return Inertia::render('Employer/Auth/Register');
|
||||
}
|
||||
|
||||
public function register(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|string|email|max:255',
|
||||
'phone' => 'required|string|max:25',
|
||||
'password' => 'required|string|min:8',
|
||||
'emirates_id_front' => 'required|file|mimes:jpg,png,pdf|max:5120',
|
||||
'emirates_id_back' => 'required|file|mimes:jpg,png,pdf|max:5120',
|
||||
]);
|
||||
|
||||
$frontPath = null;
|
||||
$backPath = null;
|
||||
|
||||
if ($request->hasFile('emirates_id_front')) {
|
||||
$frontPath = $request->file('emirates_id_front')->store('private/emirates-id', 'local');
|
||||
}
|
||||
|
||||
if ($request->hasFile('emirates_id_back')) {
|
||||
$backPath = $request->file('emirates_id_back')->store('private/emirates-id', 'local');
|
||||
}
|
||||
|
||||
// Auto-approve registration since proof is provided
|
||||
session(['user' => (object)[
|
||||
'id' => 3,
|
||||
'name' => $validated['name'],
|
||||
'email' => $validated['email'],
|
||||
'phone' => $validated['phone'],
|
||||
'role' => 'employer',
|
||||
'subscription_status' => 'active',
|
||||
'verification_status' => 'approved',
|
||||
]]);
|
||||
|
||||
if ($request->source === 'mobile') {
|
||||
return redirect('/mobile/employer/home');
|
||||
}
|
||||
|
||||
return redirect()->route('employer.dashboard')->with('success', 'Registration successful! Your account is verified and ready.');
|
||||
}
|
||||
|
||||
public function verifyEmail($token)
|
||||
{
|
||||
return redirect()->route('employer.dashboard')->with('success', 'Email verified successfully.');
|
||||
}
|
||||
|
||||
public function logout(Request $request)
|
||||
{
|
||||
session()->forget('user');
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect()->route('employer.login');
|
||||
}
|
||||
}
|
||||
168
app/Http/Controllers/Employer/WorkerController.php
Normal file
168
app/Http/Controllers/Employer/WorkerController.php
Normal file
@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Employer;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class WorkerController extends Controller
|
||||
{
|
||||
private function getWorkersList()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'id' => 101,
|
||||
'name' => 'Maria Santos',
|
||||
'nationality' => 'Philippines',
|
||||
'category' => 'Childcare',
|
||||
'skills' => ['Childcare', 'Cooking', 'Housekeeping', 'Ironing'],
|
||||
'availability' => 'Immediate',
|
||||
'experience' => '5+ Years',
|
||||
'experience_years' => 6,
|
||||
'salary' => 1800,
|
||||
'religion' => 'Christian',
|
||||
'languages' => ['English', 'Tagalog'],
|
||||
'age' => 32,
|
||||
'verified' => true,
|
||||
'bio' => 'Experienced nanny with 6 years working with expatriate families in Dubai. Certified in pediatric first aid.',
|
||||
],
|
||||
[
|
||||
'id' => 102,
|
||||
'name' => 'Lakshmi Sharma',
|
||||
'nationality' => 'India',
|
||||
'category' => 'Elderly Care',
|
||||
'skills' => ['Elderly Care', 'Cooking', 'Medication Management'],
|
||||
'availability' => '2 Weeks',
|
||||
'experience' => '3-5 Years',
|
||||
'experience_years' => 4,
|
||||
'salary' => 1600,
|
||||
'religion' => 'Hindu',
|
||||
'languages' => ['English', 'Hindi'],
|
||||
'age' => 38,
|
||||
'verified' => true,
|
||||
'bio' => 'Patient caregiver specializing in elderly assistance, mobility support, and vegetarian cooking.',
|
||||
],
|
||||
[
|
||||
'id' => 103,
|
||||
'name' => 'Siti Aminah',
|
||||
'nationality' => 'Indonesia',
|
||||
'category' => 'Housekeeping',
|
||||
'skills' => ['Housekeeping', 'Ironing', 'Deep Cleaning'],
|
||||
'availability' => 'Immediate',
|
||||
'experience' => '3-5 Years',
|
||||
'experience_years' => 3,
|
||||
'salary' => 1400,
|
||||
'religion' => 'Muslim',
|
||||
'languages' => ['English', 'Arabic'],
|
||||
'age' => 29,
|
||||
'verified' => true,
|
||||
'bio' => 'Hardworking and meticulous housekeeper with excellent references from Abu Dhabi households.',
|
||||
],
|
||||
[
|
||||
'id' => 104,
|
||||
'name' => 'Grace Osei',
|
||||
'nationality' => 'Ghana',
|
||||
'category' => 'Childcare',
|
||||
'skills' => ['Childcare', 'English Tutoring', 'Cooking'],
|
||||
'availability' => '1 Month',
|
||||
'experience' => '1-2 Years',
|
||||
'experience_years' => 2,
|
||||
'salary' => 1500,
|
||||
'religion' => 'Christian',
|
||||
'languages' => ['English'],
|
||||
'age' => 26,
|
||||
'verified' => false,
|
||||
'bio' => 'Energetic and educated nanny. Fluent in English, great with toddlers and assisting with homework.',
|
||||
],
|
||||
[
|
||||
'id' => 105,
|
||||
'name' => 'Anoma Perera',
|
||||
'nationality' => 'Sri Lanka',
|
||||
'category' => 'Cooking',
|
||||
'skills' => ['Cooking', 'Baking', 'Housekeeping'],
|
||||
'availability' => 'Immediate',
|
||||
'experience' => '5+ Years',
|
||||
'experience_years' => 8,
|
||||
'salary' => 2200,
|
||||
'religion' => 'Buddhist',
|
||||
'languages' => ['English', 'Arabic'],
|
||||
'age' => 41,
|
||||
'verified' => true,
|
||||
'bio' => 'Professional domestic cook skilled in Arabic, Continental, and Asian cuisine. Highly organized.',
|
||||
],
|
||||
[
|
||||
'id' => 106,
|
||||
'name' => 'Mary Wanjiku',
|
||||
'nationality' => 'Kenya',
|
||||
'category' => 'Housekeeping',
|
||||
'skills' => ['Housekeeping', 'Laundry', 'Pet Care'],
|
||||
'availability' => '1 Week',
|
||||
'experience' => '1-2 Years',
|
||||
'experience_years' => 2,
|
||||
'salary' => 1300,
|
||||
'religion' => 'Christian',
|
||||
'languages' => ['English'],
|
||||
'age' => 25,
|
||||
'verified' => true,
|
||||
'bio' => 'Enthusiastic and animal-loving housekeeper. Extremely reliable and quick learner.',
|
||||
],
|
||||
[
|
||||
'id' => 107,
|
||||
'name' => 'Fatima Zahra',
|
||||
'nationality' => 'Ethiopia',
|
||||
'category' => 'Housekeeping',
|
||||
'skills' => ['Housekeeping', 'Arabic Cooking', 'Baby Care'],
|
||||
'availability' => 'Immediate',
|
||||
'experience' => '3-5 Years',
|
||||
'experience_years' => 5,
|
||||
'salary' => 1500,
|
||||
'religion' => 'Muslim',
|
||||
'languages' => ['Arabic', 'English'],
|
||||
'age' => 31,
|
||||
'verified' => true,
|
||||
'bio' => 'Fluent Arabic speaker with 5 years experience in Al Ain. Excellent at traditional Arabic dishes.',
|
||||
],
|
||||
[
|
||||
'id' => 108,
|
||||
'name' => 'Ramesh Thapa',
|
||||
'nationality' => 'Nepal',
|
||||
'category' => 'Driver',
|
||||
'skills' => ['Family Driver', 'Garden Maintenance', 'Heavy Lifting'],
|
||||
'availability' => '2 Weeks',
|
||||
'experience' => '5+ Years',
|
||||
'experience_years' => 7,
|
||||
'salary' => 2500,
|
||||
'religion' => 'Hindu',
|
||||
'languages' => ['English', 'Hindi'],
|
||||
'age' => 36,
|
||||
'verified' => true,
|
||||
'bio' => 'Valid UAE driving license with clean record. Familiar with all Dubai and Sharjah school routes.',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
return Inertia::render('Employer/Workers/Index', [
|
||||
'initialWorkers' => $this->getWorkersList(),
|
||||
'filtersMetadata' => [
|
||||
'categories' => ['All Categories', 'Childcare', 'Housekeeping', 'Cooking', 'Elderly Care', 'Driver'],
|
||||
'nationalities' => ['All Nationalities', 'Philippines', 'India', 'Indonesia', 'Sri Lanka', 'Nepal', 'Ethiopia', 'Kenya', 'Ghana'],
|
||||
'availabilities' => ['All Availabilities', 'Immediate', '1 Week', '2 Weeks', '1 Month'],
|
||||
'experienceLevels' => ['All Experience', '1-2 Years', '3-5 Years', '5+ Years'],
|
||||
'religions' => ['All Religions', 'Christian', 'Muslim', 'Hindu', 'Buddhist'],
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function show($id)
|
||||
{
|
||||
$workers = $this->getWorkersList();
|
||||
$worker = collect($workers)->firstWhere('id', (int)$id) ?? $workers[0];
|
||||
|
||||
return Inertia::render('Employer/Workers/Show', [
|
||||
'worker' => $worker,
|
||||
]);
|
||||
}
|
||||
}
|
||||
25
app/Http/Middleware/AdminMiddleware.php
Normal file
25
app/Http/Middleware/AdminMiddleware.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AdminMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$user = session('user');
|
||||
$role = is_array($user) ? ($user['role'] ?? null) : ($user->role ?? null);
|
||||
|
||||
if (!$user || $role !== 'admin') {
|
||||
return redirect()->route('admin.login');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
30
app/Http/Middleware/EmployerMiddleware.php
Normal file
30
app/Http/Middleware/EmployerMiddleware.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EmployerMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$user = auth()->check() ? auth()->user() : session('user');
|
||||
$role = is_array($user) ? ($user['role'] ?? null) : ($user->role ?? null);
|
||||
$subStatus = is_array($user) ? ($user['subscription_status'] ?? 'none') : ($user->subscription_status ?? 'none');
|
||||
|
||||
if (!$user || $role !== 'employer') {
|
||||
return redirect()->route('employer.login');
|
||||
}
|
||||
|
||||
if ($subStatus === 'expired' && !$request->routeIs('employer.subscription')) {
|
||||
return redirect()->route('employer.subscription')->with('error', 'Your subscription has expired.');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
52
app/Http/Middleware/HandleInertiaRequests.php
Normal file
52
app/Http/Middleware/HandleInertiaRequests.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Middleware;
|
||||
|
||||
class HandleInertiaRequests extends Middleware
|
||||
{
|
||||
/**
|
||||
* The root template that's loaded on the first page visit.
|
||||
*
|
||||
* @see https://inertiajs.com/server-side-setup#root-template
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rootView = 'app';
|
||||
|
||||
/**
|
||||
* Determines the current asset version.
|
||||
*
|
||||
* @see https://inertiajs.com/asset-versioning
|
||||
*/
|
||||
public function version(Request $request): ?string
|
||||
{
|
||||
return parent::version($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the props that are shared by default.
|
||||
*
|
||||
* @see https://inertiajs.com/shared-data
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function share(Request $request): array
|
||||
{
|
||||
return [
|
||||
...parent::share($request),
|
||||
'auth' => [
|
||||
'user' => session('user'),
|
||||
],
|
||||
'unread_messages_count' => 3,
|
||||
'flash' => [
|
||||
'status' => session('status'),
|
||||
'reason' => session('reason'),
|
||||
'success' => session('success'),
|
||||
'error' => session('error'),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
18
app/Models/EmployerProfile.php
Normal file
18
app/Models/EmployerProfile.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class EmployerProfile extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'company_name',
|
||||
'phone',
|
||||
'emirates_id_front',
|
||||
'emirates_id_back',
|
||||
'verification_status',
|
||||
'rejection_reason'
|
||||
];
|
||||
}
|
||||
26
app/Models/Subscription.php
Normal file
26
app/Models/Subscription.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Subscription extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'plan_id',
|
||||
'amount_aed',
|
||||
'starts_at',
|
||||
'expires_at',
|
||||
'paytabs_transaction_id',
|
||||
'status'
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'starts_at' => 'datetime',
|
||||
'expires_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
58
app/Models/User.php
Normal file
58
app/Models/User.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
#[Fillable(['name', 'email', 'password', 'role', 'status', 'subscription_status', 'subscription_expires_at'])]
|
||||
#[Hidden(['password', 'remember_token'])]
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'subscription_expires_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function isAdmin(): bool
|
||||
{
|
||||
return $this->role === 'admin';
|
||||
}
|
||||
|
||||
public function isEmployer(): bool
|
||||
{
|
||||
return $this->role === 'employer';
|
||||
}
|
||||
|
||||
public function hasActiveSubscription(): bool
|
||||
{
|
||||
return $this->subscription_status === 'active' && ($this->subscription_expires_at === null || $this->subscription_expires_at > now());
|
||||
}
|
||||
|
||||
public function subscription()
|
||||
{
|
||||
return $this->hasOne(Subscription::class);
|
||||
}
|
||||
|
||||
public function emiratesIdVerification()
|
||||
{
|
||||
return $this->hasOne(EmployerProfile::class, 'user_id');
|
||||
}
|
||||
}
|
||||
24
app/Providers/AppServiceProvider.php
Normal file
24
app/Providers/AppServiceProvider.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
18
artisan
Normal file
18
artisan
Normal 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);
|
||||
24
bootstrap/app.php
Normal file
24
bootstrap/app.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?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): void {
|
||||
$middleware->web(append: [
|
||||
\App\Http\Middleware\HandleInertiaRequests::class,
|
||||
]);
|
||||
$middleware->alias([
|
||||
'admin' => \App\Http\Middleware\AdminMiddleware::class,
|
||||
'employer' => \App\Http\Middleware\EmployerMiddleware::class,
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
//
|
||||
})->create();
|
||||
2
bootstrap/cache/.gitignore
vendored
Normal file
2
bootstrap/cache/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
7
bootstrap/providers.php
Normal file
7
bootstrap/providers.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Providers\AppServiceProvider;
|
||||
|
||||
return [
|
||||
AppServiceProvider::class,
|
||||
];
|
||||
25
components.json
Normal file
25
components.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "radix-nova",
|
||||
"rsc": false,
|
||||
"tsx": false,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "resources/css/app.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"rtl": false,
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"menuColor": "default",
|
||||
"menuAccent": "subtle",
|
||||
"registries": {}
|
||||
}
|
||||
88
composer.json
Normal file
88
composer.json
Normal file
@ -0,0 +1,88 @@
|
||||
{
|
||||
"$schema": "https://getcomposer.org/schema.json",
|
||||
"name": "laravel/laravel",
|
||||
"type": "project",
|
||||
"description": "The skeleton application for the Laravel framework.",
|
||||
"keywords": ["laravel", "framework"],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.3",
|
||||
"inertiajs/inertia-laravel": "^3.1",
|
||||
"laravel/framework": "^13.8",
|
||||
"laravel/tinker": "^3.0",
|
||||
"tightenco/ziggy": "^2.6"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/pail": "^1.2.5",
|
||||
"laravel/pao": "^1.0.6",
|
||||
"laravel/pint": "^1.27",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.6",
|
||||
"phpunit/phpunit": "^12.5.12"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"setup": [
|
||||
"composer install",
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||
"@php artisan key:generate",
|
||||
"@php artisan migrate --force",
|
||||
"npm install --ignore-scripts",
|
||||
"npm run build"
|
||||
],
|
||||
"dev": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
||||
],
|
||||
"test": [
|
||||
"@php artisan config:clear --ansi @no_additional_args",
|
||||
"@php artisan test"
|
||||
],
|
||||
"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"
|
||||
],
|
||||
"pre-package-uninstall": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
|
||||
]
|
||||
},
|
||||
"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
|
||||
}
|
||||
8531
composer.lock
generated
Normal file
8531
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(',', (string) 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'),
|
||||
],
|
||||
|
||||
];
|
||||
117
config/auth.php
Normal file
117
config/auth.php
Normal file
@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
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', 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 number 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),
|
||||
|
||||
];
|
||||
130
config/cache.php
Normal file
130
config/cache.php
Normal file
@ -0,0 +1,130 @@
|
||||
<?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",
|
||||
| "failover", "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',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'stores' => [
|
||||
'database',
|
||||
'array',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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((string) env('APP_NAME', 'laravel')).'-cache-'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Serializable Classes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the classes that can be unserialized from cache
|
||||
| storage. By default, no PHP classes will be unserialized from your
|
||||
| cache to prevent gadget chain attacks if your APP_KEY is leaked.
|
||||
|
|
||||
*/
|
||||
|
||||
'serializable_classes' => false,
|
||||
|
||||
];
|
||||
184
config/database.php
Normal file
184
config/database.php
Normal file
@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Pdo\Mysql;
|
||||
|
||||
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,
|
||||
'transaction_mode' => 'DEFERRED',
|
||||
],
|
||||
|
||||
'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([
|
||||
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([
|
||||
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' => env('DB_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((string) 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),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
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' => rtrim(env('APP_URL', 'http://localhost'), '/').'/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'),
|
||||
],
|
||||
|
||||
];
|
||||
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(',', (string) 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', env('APP_NAME', 'Laravel')),
|
||||
'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,
|
||||
'handler_with' => [
|
||||
'stream' => 'php://stderr',
|
||||
],
|
||||
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||
'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'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
118
config/mail.php
Normal file
118
config/mail.php
Normal file
@ -0,0 +1,118 @@
|
||||
<?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((string) 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',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
'roundrobin' => [
|
||||
'transport' => 'roundrobin',
|
||||
'mailers' => [
|
||||
'ses',
|
||||
'postmark',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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', env('APP_NAME', 'Laravel')),
|
||||
],
|
||||
|
||||
];
|
||||
129
config/queue.php
Normal file
129
config/queue.php
Normal file
@ -0,0 +1,129 @@
|
||||
<?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",
|
||||
| "deferred", "background", "failover", "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,
|
||||
],
|
||||
|
||||
'deferred' => [
|
||||
'driver' => 'deferred',
|
||||
],
|
||||
|
||||
'background' => [
|
||||
'driver' => 'background',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'connections' => [
|
||||
'database',
|
||||
'deferred',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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',
|
||||
],
|
||||
|
||||
];
|
||||
38
config/services.php
Normal file
38
config/services.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?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' => [
|
||||
'key' => env('POSTMARK_API_KEY'),
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'key' => env('RESEND_API_KEY'),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'notifications' => [
|
||||
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
||||
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
233
config/session.php
Normal file
233
config/session.php
Normal file
@ -0,0 +1,233 @@
|
||||
<?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", "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: "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((string) 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 without 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),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Serialization
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value controls the serialization strategy for session data, which
|
||||
| is JSON by default. Setting this to "php" allows the storage of PHP
|
||||
| objects in the session but can make an application vulnerable to
|
||||
| "gadget chain" serialization attacks if the APP_KEY is leaked.
|
||||
|
|
||||
| Supported: "json", "php"
|
||||
|
|
||||
*/
|
||||
|
||||
'serialization' => 'json',
|
||||
|
||||
];
|
||||
1
database/.gitignore
vendored
Normal file
1
database/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.sqlite*
|
||||
45
database/factories/UserFactory.php
Normal file
45
database/factories/UserFactory.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends Factory<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->bigInteger('expiration')->index();
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->bigInteger('expiration')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
||||
59
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal file
59
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?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->unsignedSmallInteger('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->string('connection');
|
||||
$table->string('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
|
||||
$table->index(['connection', 'queue', 'failed_at']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
Schema::dropIfExists('job_batches');
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('role')->default('worker');
|
||||
$table->string('subscription_status')->default('none');
|
||||
$table->timestamp('subscription_expires_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('employer_profiles', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('company_name');
|
||||
$table->string('phone')->nullable();
|
||||
$table->string('emirates_id_front')->nullable();
|
||||
$table->string('emirates_id_back')->nullable();
|
||||
$table->string('verification_status')->default('pending');
|
||||
$table->text('rejection_reason')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('subscriptions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('plan_id');
|
||||
$table->decimal('amount_aed', 10, 2);
|
||||
$table->timestamp('starts_at')->nullable();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->string('paytabs_transaction_id')->nullable();
|
||||
$table->string('status')->default('active');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('subscriptions');
|
||||
Schema::dropIfExists('employer_profiles');
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn(['role', 'subscription_status', 'subscription_expires_at']);
|
||||
});
|
||||
}
|
||||
};
|
||||
25
database/seeders/DatabaseSeeder.php
Normal file
25
database/seeders/DatabaseSeeder.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
use WithoutModelEvents;
|
||||
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// User::factory(10)->create();
|
||||
|
||||
User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
}
|
||||
}
|
||||
8
jsconfig.json
Normal file
8
jsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["resources/js/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
7707
package-lock.json
generated
Normal file
7707
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
package.json
Normal file
35
package.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"$schema": "https://www.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"autoprefixer": "^10.5.0",
|
||||
"concurrently": "^9.0.1",
|
||||
"laravel-vite-plugin": "^3.1",
|
||||
"postcss": "^8.5.14",
|
||||
"tailwindcss": "^4.3.0",
|
||||
"vite": "^8.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/geist": "^5.2.8",
|
||||
"@inertiajs/react": "^3.1.1",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"axios": "^1.16.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.16.0",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
"shadcn": "^4.7.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"ziggy-js": "github:tightenco/ziggy"
|
||||
}
|
||||
}
|
||||
36
phpunit.xml
Normal file
36
phpunit.xml
Normal file
@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory>tests/Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Feature">
|
||||
<directory>tests/Feature</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
<include>
|
||||
<directory>app</directory>
|
||||
</include>
|
||||
</source>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="BROADCAST_CONNECTION" value="null"/>
|
||||
<env name="CACHE_STORE" value="array"/>
|
||||
<env name="DB_CONNECTION" value="sqlite"/>
|
||||
<env name="DB_DATABASE" value=":memory:"/>
|
||||
<env name="DB_URL" value=""/>
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="PULSE_ENABLED" value="false"/>
|
||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||
<env name="NIGHTWATCH_ENABLED" value="false"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
25
public/.htaccess
Normal file
25
public/.htaccess
Normal file
@ -0,0 +1,25 @@
|
||||
<IfModule mod_rewrite.c>
|
||||
<IfModule mod_negotiation.c>
|
||||
Options -MultiViews -Indexes
|
||||
</IfModule>
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# Handle Authorization Header
|
||||
RewriteCond %{HTTP:Authorization} .
|
||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||
|
||||
# Handle X-XSRF-Token Header
|
||||
RewriteCond %{HTTP:x-xsrf-token} .
|
||||
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
|
||||
|
||||
# Redirect Trailing Slashes If Not A Folder...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_URI} (.+)/$
|
||||
RewriteRule ^ %1 [L,R=301]
|
||||
|
||||
# Send Requests To Front Controller...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [L]
|
||||
</IfModule>
|
||||
0
public/favicon.ico
Normal file
0
public/favicon.ico
Normal file
20
public/index.php
Normal file
20
public/index.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Determine if the application is in maintenance mode...
|
||||
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
||||
require $maintenance;
|
||||
}
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the request...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/../bootstrap/app.php';
|
||||
|
||||
$app->handleRequest(Request::capture());
|
||||
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
140
resources/css/app.css
Normal file
140
resources/css/app.css
Normal file
@ -0,0 +1,140 @@
|
||||
@import 'tailwindcss';
|
||||
@import "tw-animate-css";
|
||||
@import "shadcn/tailwind.css";
|
||||
@import "@fontsource-variable/geist";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
@source '../../storage/framework/views/*.php';
|
||||
@source '../**/*.blade.php';
|
||||
@source '../**/*.js';
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--font-heading: var(--font-sans);
|
||||
--font-sans: 'Geist Variable', sans-serif;
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-background: var(--background);
|
||||
--radius-sm: calc(var(--radius) * 0.6);
|
||||
--radius-md: calc(var(--radius) * 0.8);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) * 1.4);
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
--radius-3xl: calc(var(--radius) * 2.2);
|
||||
--radius-4xl: calc(var(--radius) * 2.6);
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
html {
|
||||
@apply font-sans;
|
||||
}
|
||||
}
|
||||
155
resources/js/Layouts/AdminLayout.jsx
Normal file
155
resources/js/Layouts/AdminLayout.jsx
Normal file
@ -0,0 +1,155 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, usePage } from '@inertiajs/react';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Users,
|
||||
Briefcase,
|
||||
CreditCard,
|
||||
Megaphone,
|
||||
Settings,
|
||||
LogOut,
|
||||
Menu,
|
||||
Bell,
|
||||
Shield,
|
||||
BadgeDollarSign
|
||||
} from 'lucide-react';
|
||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||
|
||||
export default function AdminLayout({ children, title = 'Dashboard' }) {
|
||||
const { url, props } = usePage();
|
||||
const { auth } = props;
|
||||
const user = auth?.user || { name: 'Admin Portal', email: 'admin@marketplace.com' };
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const navItems = [
|
||||
{ name: 'Dashboard', href: '/admin/dashboard', icon: LayoutDashboard },
|
||||
{ name: 'Workers', href: '/admin/workers', icon: Users },
|
||||
{ name: 'Employers', href: '/admin/employers', icon: Briefcase },
|
||||
{ name: 'Subscriptions', href: '/admin/subscriptions', icon: CreditCard },
|
||||
{ name: 'Payments', href: '/admin/payments', icon: BadgeDollarSign },
|
||||
{ name: 'Announcements', href: '/admin/announcements', icon: Megaphone },
|
||||
{ name: 'Worker Categories', href: '/admin/master-data/categories', icon: Settings },
|
||||
];
|
||||
|
||||
const getInitials = (name) => {
|
||||
return name
|
||||
?.split(' ')
|
||||
.map((n) => n[0])
|
||||
.join('')
|
||||
.toUpperCase() || 'A';
|
||||
};
|
||||
|
||||
const SidebarContent = ({ isMobile }) => (
|
||||
<div className="flex flex-col h-full bg-white font-sans select-none">
|
||||
{/* Top: App Logo + Admin Label */}
|
||||
<div className="flex items-center space-x-3 px-6 h-16 border-b border-gray-100 flex-shrink-0">
|
||||
<div className="w-9 h-9 bg-[#0F6E56]/10 rounded-xl flex items-center justify-center shadow-inner">
|
||||
<Shield className="w-5 h-5 text-[#0F6E56]" />
|
||||
</div>
|
||||
<span className="font-semibold text-gray-800 text-lg tracking-tight">Admin Portal</span>
|
||||
</div>
|
||||
|
||||
{/* Nav Items */}
|
||||
<div className="flex-1 overflow-y-auto px-3 py-4 space-y-1.5">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const active = url.startsWith(item.href);
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
onClick={() => isMobile && setOpen(false)}
|
||||
className={`flex items-center space-x-3 px-4 py-3 rounded-xl text-sm transition-all duration-200 group ${
|
||||
active
|
||||
? 'bg-[#0F6E56] text-white font-bold shadow-lg shadow-teal-900/20 scale-[1.02]'
|
||||
: 'text-gray-500 hover:bg-gray-50 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
<Icon className={`w-5 h-5 flex-shrink-0 transition-colors ${active ? 'text-white' : 'text-gray-400 group-hover:text-gray-900'}`} />
|
||||
<span className="tracking-tight">{item.name}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Bottom: Admin Avatar, Name, Email, Logout */}
|
||||
<div className="p-4 border-t border-gray-100 flex-shrink-0">
|
||||
<div className="flex items-center space-x-3 mb-3 px-2">
|
||||
<div className="w-9 h-9 rounded-full bg-[#0F6E56] text-white flex items-center justify-center font-medium text-sm flex-shrink-0 shadow-sm">
|
||||
{getInitials(user.name)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 truncate">{user.name}</p>
|
||||
<p className="text-xs text-gray-500 truncate">{user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
href="/admin/logout"
|
||||
method="post"
|
||||
as="button"
|
||||
className="w-full flex items-center space-x-3 px-3 py-2 text-sm text-red-600 hover:bg-red-50 rounded-lg transition-colors font-medium justify-start focus:outline-none"
|
||||
>
|
||||
<LogOut className="w-5 h-5 flex-shrink-0" />
|
||||
<span>Log Out</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex font-sans">
|
||||
{/* Fixed Left Sidebar (Desktop lg+) */}
|
||||
<aside className="hidden lg:flex lg:flex-col lg:w-[240px] lg:fixed lg:inset-y-0 lg:border-r lg:border-gray-100 lg:bg-white lg:z-30">
|
||||
<SidebarContent isMobile={false} />
|
||||
</aside>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div className="flex-1 flex flex-col min-w-0 lg:ml-[240px]">
|
||||
{/* Top Bar */}
|
||||
<header className="h-16 bg-white border-b border-gray-100 flex items-center justify-between px-6 sticky top-0 z-20 shadow-sm">
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* Mobile menu trigger */}
|
||||
<div className="lg:hidden">
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 text-gray-600 hover:text-gray-900 focus:outline-none rounded-lg hover:bg-gray-50 transition-colors flex items-center justify-center"
|
||||
>
|
||||
<Menu className="w-6 h-6" />
|
||||
</button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="p-0 w-[240px] bg-white border-r border-gray-100">
|
||||
<SidebarContent isMobile={true} />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
<h1 className="text-xl font-semibold text-gray-800 tracking-tight">{title}</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 text-gray-500 hover:text-gray-800 rounded-full hover:bg-gray-50 relative focus:outline-none transition-colors"
|
||||
>
|
||||
<Bell className="w-5 h-5" />
|
||||
<span className="absolute top-2 right-2 w-2 h-2 bg-[#0F6E56] rounded-full ring-2 ring-white" />
|
||||
</button>
|
||||
|
||||
<div className="flex items-center space-x-3 border-l border-gray-100 pl-4">
|
||||
<div className="w-8 h-8 rounded-full bg-[#0F6E56] text-white flex items-center justify-center font-medium text-xs shadow-sm">
|
||||
{getInitials(user.name)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Content Slot */}
|
||||
<main className="flex-1 p-6 max-w-7xl w-full mx-auto">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
282
resources/js/Layouts/EmployerLayout.jsx
Normal file
282
resources/js/Layouts/EmployerLayout.jsx
Normal file
@ -0,0 +1,282 @@
|
||||
import React from 'react';
|
||||
import { Link, usePage } from '@inertiajs/react';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Search,
|
||||
Bookmark,
|
||||
MessageSquare,
|
||||
CreditCard,
|
||||
User,
|
||||
LogOut,
|
||||
Bell,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
ArrowRight,
|
||||
UserCheck,
|
||||
Megaphone,
|
||||
Briefcase
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
|
||||
export default function EmployerLayout({ children, title, fullPage = false }) {
|
||||
const { url, props } = usePage();
|
||||
const { auth, unread_messages_count } = props;
|
||||
|
||||
const user = auth?.user || {
|
||||
name: 'John Doe',
|
||||
company_name: 'Al Mansoor Household',
|
||||
email: 'employer@example.com',
|
||||
subscription_status: 'active',
|
||||
subscription_expires_at: '2026-12-31',
|
||||
};
|
||||
|
||||
const isSubActive = user.subscription_status === 'active';
|
||||
|
||||
const navItems = [
|
||||
{ name: 'Dashboard', href: '/employer/dashboard', icon: LayoutDashboard },
|
||||
{ name: 'Find Workers', href: '/employer/workers', icon: Search },
|
||||
{ name: 'My Jobs', href: '/employer/jobs', icon: Briefcase },
|
||||
{ name: 'Shortlist', href: '/employer/shortlist', icon: Bookmark },
|
||||
{ name: 'Candidates', href: '/employer/candidates', icon: UserCheck },
|
||||
{ name: 'Messages', href: '/employer/messages', icon: MessageSquare, badge: unread_messages_count },
|
||||
{ name: 'Announcements', href: '/employer/announcements', icon: Megaphone },
|
||||
{ name: 'Subscription', href: '/employer/subscription', icon: CreditCard },
|
||||
{ name: 'My Profile', href: '/employer/profile', icon: User },
|
||||
];
|
||||
|
||||
const mobileTabs = [
|
||||
{ name: 'Search', href: '/employer/workers', icon: Search },
|
||||
{ name: 'Shortlist', href: '/employer/shortlist', icon: Bookmark },
|
||||
{ name: 'Candidates', href: '/employer/candidates', icon: UserCheck },
|
||||
{ name: 'Messages', href: '/employer/messages', icon: MessageSquare, badge: unread_messages_count },
|
||||
{ name: 'Profile', href: '/employer/profile', icon: User },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-slate-50 font-sans select-none">
|
||||
{/* Top Subscription Gate Banner */}
|
||||
{!isSubActive && !fullPage && (
|
||||
<div className="bg-[#185FA5] text-white px-4 py-2.5 text-xs sm:text-sm font-medium flex items-center justify-between shadow-sm z-50">
|
||||
<div className="flex items-center space-x-2 truncate">
|
||||
<AlertCircle className="w-4 h-4 text-amber-300 flex-shrink-0 animate-bounce" />
|
||||
<span className="truncate">You need an active subscription to contact workers.</span>
|
||||
</div>
|
||||
<Link
|
||||
href="/employer/subscription"
|
||||
className="bg-white text-[#185FA5] hover:bg-slate-100 px-3 py-1 rounded-lg text-xs font-bold transition-colors inline-flex items-center space-x-1 flex-shrink-0 shadow-sm"
|
||||
>
|
||||
<span>Subscribe now</span>
|
||||
<ArrowRight className="w-3 h-3" />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile Header Navbar */}
|
||||
{!fullPage && (
|
||||
<header className="lg:hidden sticky top-0 z-40 bg-white border-b border-slate-200 px-4 h-14 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2.5">
|
||||
<div className="w-8 h-8 bg-[#185FA5] text-white rounded-lg flex items-center justify-center font-bold text-base shadow-sm">
|
||||
M
|
||||
</div>
|
||||
<span className="font-bold text-lg text-slate-800 tracking-tight truncate">Employer Portal</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<Link href="/employer/messages" className="relative p-2 text-slate-600 hover:bg-slate-100 rounded-lg">
|
||||
<Bell className="w-5 h-5" />
|
||||
{unread_messages_count > 0 && (
|
||||
<span className="absolute top-1.5 right-1.5 w-2 h-2 bg-red-500 rounded-full animate-ping" />
|
||||
)}
|
||||
</Link>
|
||||
|
||||
<div className="w-8 h-8 rounded-full bg-slate-100 border border-slate-200 flex items-center justify-center font-bold text-xs text-[#185FA5]">
|
||||
{user.name.charAt(0)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)}
|
||||
|
||||
<div className="flex flex-1 flex-col lg:flex-row h-screen overflow-hidden">
|
||||
{/* Desktop Sidebar (lg+) */}
|
||||
<aside className="hidden lg:flex w-64 bg-white border-r border-slate-200 flex-col justify-between flex-shrink-0 sticky top-0 h-screen">
|
||||
<div>
|
||||
{/* Top Section */}
|
||||
<div className="p-6 border-b border-slate-100 space-y-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 bg-[#185FA5] text-white rounded-xl flex items-center justify-center font-bold text-xl shadow-md">
|
||||
M
|
||||
</div>
|
||||
<span className="font-bold text-xl text-slate-800 tracking-tight">Employer Portal</span>
|
||||
</div>
|
||||
|
||||
{/* Subscription Status Pill */}
|
||||
<Link href="/employer/subscription" className="block">
|
||||
{isSubActive ? (
|
||||
<div className="flex items-center justify-between px-3 py-1.5 bg-emerald-50 border border-emerald-200 rounded-lg text-xs font-semibold text-emerald-700 hover:bg-emerald-100 transition-colors">
|
||||
<div className="flex items-center space-x-1.5 truncate">
|
||||
<CheckCircle className="w-3.5 h-3.5 text-emerald-600 flex-shrink-0" />
|
||||
<span className="truncate">Active until {user.subscription_expires_at || 'Dec 31'}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between px-3 py-1.5 bg-red-50 border border-red-200 rounded-lg text-xs font-semibold text-red-700 hover:bg-red-100 transition-colors">
|
||||
<div className="flex items-center space-x-1.5 truncate">
|
||||
<AlertCircle className="w-3.5 h-3.5 text-red-600 flex-shrink-0" />
|
||||
<span className="truncate">Expired — Renew now</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Navigation Items */}
|
||||
<nav className="p-4 space-y-1">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = url.startsWith(item.href);
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={`flex items-center justify-between px-4 py-3 rounded-xl text-sm transition-all duration-200 group ${
|
||||
isActive
|
||||
? 'bg-[#185FA5] text-white font-bold shadow-lg shadow-blue-900/20 scale-[1.02]'
|
||||
: 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Icon className={`w-5 h-5 transition-colors ${isActive ? 'text-white' : 'text-slate-400 group-hover:text-slate-900'}`} />
|
||||
<span className="tracking-tight">{item.name}</span>
|
||||
</div>
|
||||
|
||||
{item.badge > 0 && (
|
||||
<span className={`text-[10px] font-black px-2 py-0.5 rounded-full ${isActive ? 'bg-white text-[#185FA5]' : 'bg-[#185FA5] text-white'}`}>
|
||||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<main className="flex-1 flex flex-col min-w-0 h-screen relative overflow-hidden bg-[#FAFBFF]">
|
||||
{/* Top Desktop Header */}
|
||||
{!fullPage && (
|
||||
<header className="hidden lg:flex h-16 bg-white border-b border-slate-200 items-center justify-between px-8 shrink-0 sticky top-0 z-30 shadow-sm">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="text-xs font-bold text-slate-400 uppercase tracking-widest">
|
||||
{title || 'Overview'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-6">
|
||||
{/* Language Switcher */}
|
||||
<div className="flex items-center bg-slate-50 border border-slate-200 rounded-xl p-1">
|
||||
<button className="px-3 py-1 text-[10px] font-black bg-white text-[#185FA5] rounded-lg shadow-sm border border-slate-100">EN</button>
|
||||
<button className="px-3 py-1 text-[10px] font-black text-slate-400 hover:text-slate-600">AR</button>
|
||||
</div>
|
||||
|
||||
{/* Chat/Notifications */}
|
||||
<Link href="/employer/messages" className="relative p-2.5 text-slate-500 hover:text-[#185FA5] hover:bg-blue-50 rounded-xl transition-all group">
|
||||
<MessageSquare className="w-5 h-5" />
|
||||
<span className="absolute top-2 right-2 w-2 h-2 bg-red-500 rounded-full border-2 border-white group-hover:scale-110 transition-transform" />
|
||||
</Link>
|
||||
|
||||
{/* Profile Section with Dropdown */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className="flex items-center space-x-3 pl-6 border-l border-slate-200 group outline-none">
|
||||
<div className="text-right hidden sm:block">
|
||||
<div className="text-xs font-black text-slate-900 group-hover:text-[#185FA5] transition-colors">{user.name}</div>
|
||||
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-tighter">Employer Account</div>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-xl bg-blue-100 text-[#185FA5] flex items-center justify-center font-black text-xs border border-blue-200 shadow-sm transition-transform group-hover:scale-105">
|
||||
{user.name.charAt(0)}
|
||||
</div>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56 mt-2">
|
||||
<DropdownMenuLabel className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-2 py-1.5">My Account</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/employer/profile" className="flex items-center space-x-2 text-xs font-bold text-slate-600 cursor-pointer">
|
||||
<User className="w-4 h-4" />
|
||||
<span>Profile Settings</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/employer/subscription" className="flex items-center space-x-2 text-xs font-bold text-slate-600 cursor-pointer">
|
||||
<CreditCard className="w-4 h-4" />
|
||||
<span>Subscription</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href={route('employer.logout')}
|
||||
method="post"
|
||||
as="button"
|
||||
className="w-full flex items-center space-x-2 text-xs font-bold text-rose-600 cursor-pointer"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
<span>Sign Out</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</header>
|
||||
)}
|
||||
|
||||
<div className={`flex-1 overflow-y-auto ${fullPage ? 'p-0' : 'p-4 sm:p-8 pb-24 lg:pb-8'}`}>
|
||||
{title && !fullPage && (
|
||||
<h1 className="text-2xl font-black text-slate-900 mb-6 tracking-tight lg:hidden">{title}</h1>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Mobile Bottom Tab Bar */}
|
||||
{!fullPage && (
|
||||
<nav className="lg:hidden fixed bottom-0 left-0 right-0 bg-white border-t border-slate-200 h-16 px-4 flex items-center justify-around z-40">
|
||||
{mobileTabs.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
const isActive = url.startsWith(tab.href);
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={tab.name}
|
||||
href={tab.href}
|
||||
className={`flex flex-col items-center justify-center space-y-1 w-16 h-full ${
|
||||
isActive ? 'text-[#185FA5]' : 'text-slate-400 hover:text-slate-600'
|
||||
}`}
|
||||
>
|
||||
<div className="relative">
|
||||
<Icon className="w-5 h-5" />
|
||||
{tab.badge > 0 && (
|
||||
<span className="absolute -top-1 -right-2 w-3.5 h-3.5 bg-red-500 text-white rounded-full flex items-center justify-center text-[8px] font-bold">
|
||||
{tab.badge}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[10px] font-bold">{tab.name}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
180
resources/js/Pages/Admin/Announcements/Index.jsx
Normal file
180
resources/js/Pages/Admin/Announcements/Index.jsx
Normal file
@ -0,0 +1,180 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Head } from '@inertiajs/react';
|
||||
import AdminLayout from '../../../Layouts/AdminLayout';
|
||||
import { Megaphone, Plus, Trash2, Send, CheckCircle2 } from 'lucide-react';
|
||||
|
||||
export default function Announcements() {
|
||||
const [announcements, setAnnouncements] = useState([
|
||||
{
|
||||
id: 1,
|
||||
title: 'New Visa Regulations Update',
|
||||
content: 'The Ministry of Human Resources and Emiratisation has announced updated guidelines for domestic worker sponsorship starting this month.',
|
||||
audience: 'Both',
|
||||
created_at: 'May 10, 2026',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Enhanced OCR Verification Active',
|
||||
content: 'We have deployed upgraded OCR passport verification to ensure 100% legal compliance for all worker profiles on the platform.',
|
||||
audience: 'Employers',
|
||||
created_at: 'May 1, 2026',
|
||||
},
|
||||
]);
|
||||
|
||||
const [isFormOpen, setIsFormOpen] = useState(false);
|
||||
const [newAnnouncement, setNewAnnouncement] = useState({ title: '', content: '', audience: 'Both' });
|
||||
const [toastMessage, setToastMessage] = useState(null);
|
||||
|
||||
const showToast = (message) => {
|
||||
setToastMessage(message);
|
||||
setTimeout(() => setToastMessage(null), 3000);
|
||||
};
|
||||
|
||||
const handleCreate = (e) => {
|
||||
e.preventDefault();
|
||||
const newEntry = {
|
||||
id: announcements.length + 1,
|
||||
title: newAnnouncement.title,
|
||||
content: newAnnouncement.content,
|
||||
audience: newAnnouncement.audience,
|
||||
created_at: 'Just now',
|
||||
};
|
||||
setAnnouncements([newEntry, ...announcements]);
|
||||
setNewAnnouncement({ title: '', content: '', audience: 'Both' });
|
||||
setIsFormOpen(false);
|
||||
showToast('Announcement sent successfully');
|
||||
};
|
||||
|
||||
const handleDelete = (id) => {
|
||||
setAnnouncements(announcements.filter(a => a.id !== id));
|
||||
showToast('Announcement deleted');
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminLayout title="Announcements">
|
||||
<Head title="Announcements - Admin Portal" />
|
||||
|
||||
{/* Toast Notification */}
|
||||
{toastMessage && (
|
||||
<div className="fixed bottom-4 right-4 bg-slate-900 text-white px-6 py-3 rounded-xl shadow-2xl flex items-center space-x-3 z-50 animate-in slide-in-from-bottom-5">
|
||||
<CheckCircle2 className="w-5 h-5 text-emerald-400" />
|
||||
<span className="font-medium text-sm">{toastMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-6 select-none max-w-6xl mx-auto">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900">Platform Announcements</h1>
|
||||
<p className="text-sm text-slate-500">Broadcast messages to employers and workers.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsFormOpen(true)}
|
||||
className="bg-[#185FA5] hover:bg-[#144f8a] text-white px-5 py-2.5 rounded-xl font-bold text-sm flex items-center space-x-2 transition-colors shadow-sm"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>New Announcement</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isFormOpen && (
|
||||
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm animate-in fade-in slide-in-from-top-4">
|
||||
<div className="flex items-center space-x-2 mb-4 text-[#185FA5] font-bold">
|
||||
<Megaphone className="w-5 h-5" />
|
||||
<h2>Compose Message</h2>
|
||||
</div>
|
||||
<form onSubmit={handleCreate} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-600 mb-1">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={newAnnouncement.title}
|
||||
onChange={(e) => setNewAnnouncement({ ...newAnnouncement, title: e.target.value })}
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-slate-300 text-sm focus:ring-2 focus:ring-[#185FA5]/20 focus:border-[#185FA5] outline-none transition-all"
|
||||
placeholder="Enter announcement title..."
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-600 mb-1">Content</label>
|
||||
<textarea
|
||||
required
|
||||
rows="4"
|
||||
value={newAnnouncement.content}
|
||||
onChange={(e) => setNewAnnouncement({ ...newAnnouncement, content: e.target.value })}
|
||||
className="w-full px-4 py-3 rounded-xl border border-slate-300 text-sm focus:ring-2 focus:ring-[#185FA5]/20 focus:border-[#185FA5] outline-none transition-all resize-none"
|
||||
placeholder="Type your message here..."
|
||||
></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-600 mb-1">Target Audience</label>
|
||||
<select
|
||||
value={newAnnouncement.audience}
|
||||
onChange={(e) => setNewAnnouncement({ ...newAnnouncement, audience: e.target.value })}
|
||||
className="w-full sm:w-64 px-4 py-2.5 rounded-xl border border-slate-300 text-sm focus:ring-2 focus:ring-[#185FA5]/20 focus:border-[#185FA5] outline-none transition-all bg-white"
|
||||
>
|
||||
<option value="Both">Both Employers & Workers</option>
|
||||
<option value="Employers">Employers Only</option>
|
||||
<option value="Workers">Workers Only</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center justify-end space-x-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsFormOpen(false)}
|
||||
className="px-5 py-2.5 rounded-xl text-sm font-semibold text-slate-600 hover:bg-slate-100 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-emerald-600 hover:bg-emerald-700 text-white px-6 py-2.5 rounded-xl text-sm font-bold flex items-center space-x-2 transition-colors shadow-sm"
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
<span>Broadcast Message</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
{announcements.length === 0 ? (
|
||||
<div className="text-center py-16 bg-white rounded-2xl border border-slate-200 shadow-sm text-slate-500">
|
||||
<Megaphone className="w-12 h-12 text-slate-300 mx-auto mb-3" />
|
||||
<p className="font-medium text-sm">No announcements published yet.</p>
|
||||
</div>
|
||||
) : (
|
||||
announcements.map((ann) => (
|
||||
<div key={ann.id} className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm flex flex-col sm:flex-row sm:items-start justify-between gap-4 group">
|
||||
<div className="space-y-2 flex-1">
|
||||
<div className="flex items-center space-x-3">
|
||||
<h3 className="font-bold text-lg text-slate-900 tracking-tight">{ann.title}</h3>
|
||||
<span className={`px-2.5 py-1 rounded-md text-[10px] font-bold uppercase tracking-wider ${
|
||||
ann.audience === 'Both' ? 'bg-purple-100 text-purple-700' :
|
||||
ann.audience === 'Employers' ? 'bg-blue-100 text-blue-700' :
|
||||
'bg-emerald-100 text-emerald-700'
|
||||
}`}>
|
||||
To: {ann.audience}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 leading-relaxed">{ann.content}</p>
|
||||
<div className="text-[11px] font-medium text-slate-400">
|
||||
Published: {ann.created_at}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDelete(ann.id)}
|
||||
className="p-2 text-slate-400 hover:text-red-600 hover:bg-red-50 rounded-xl transition-colors self-start sm:opacity-0 group-hover:opacity-100 focus:opacity-100"
|
||||
title="Delete Announcement"
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
173
resources/js/Pages/Admin/Auth/Login.jsx
Normal file
173
resources/js/Pages/Admin/Auth/Login.jsx
Normal file
@ -0,0 +1,173 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Shield, Eye, EyeOff, Loader2, UserCircle, Globe, Smartphone } from 'lucide-react';
|
||||
import { useForm, Head, Link } from '@inertiajs/react';
|
||||
|
||||
export default function Login() {
|
||||
const { data, setData, post, processing, errors } = useForm({
|
||||
email: '',
|
||||
password: '',
|
||||
remember: false,
|
||||
});
|
||||
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
post('/admin/login');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col justify-center items-center px-4 relative overflow-hidden bg-slate-50 font-sans">
|
||||
<Head title="Admin Portal - Login" />
|
||||
|
||||
{/* Subtle animated diagonal grid background */}
|
||||
<div
|
||||
className="absolute inset-[-50%] z-0 opacity-40 pointer-events-none"
|
||||
style={{
|
||||
backgroundImage: `
|
||||
linear-gradient(to right, rgba(15, 110, 86, 0.08) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(15, 110, 86, 0.08) 1px, transparent 1px)
|
||||
`,
|
||||
backgroundSize: '48px 48px',
|
||||
transform: 'rotate(-20deg)',
|
||||
animation: 'diagonalGrid 30s linear infinite',
|
||||
}}
|
||||
/>
|
||||
<style>{`
|
||||
@keyframes diagonalGrid {
|
||||
0% { background-position: 0 0; }
|
||||
100% { background-position: 48px 48px; }
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{/* Login Card */}
|
||||
<div className="w-full max-w-md bg-white rounded-xl shadow-sm border border-slate-200 p-8 z-10 relative">
|
||||
{/* Header / Logo */}
|
||||
<div className="flex flex-col items-center mb-6 text-center">
|
||||
<div className="w-16 h-16 bg-[#0F6E56]/10 rounded-2xl flex items-center justify-center mb-4 shadow-inner">
|
||||
<Shield className="w-10 h-10 text-[#0F6E56]" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-semibold text-gray-800 tracking-tight">Admin Portal</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">Domestic Worker Marketplace</p>
|
||||
</div>
|
||||
|
||||
<hr className="border-slate-100 mb-6" />
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
autoFocus
|
||||
value={data.email}
|
||||
onChange={(e) => setData('email', e.target.value)}
|
||||
placeholder="admin@example.com"
|
||||
className="w-full px-3.5 py-2.5 rounded-lg border border-slate-300 focus:outline-none focus:ring-2 focus:ring-[#0F6E56]/20 focus:border-[#0F6E56] text-gray-800 placeholder-slate-400 text-sm transition-colors"
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="mt-1.5 text-sm text-red-500 font-medium">{errors.email}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={data.password}
|
||||
onChange={(e) => setData('password', e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className="w-full pl-3.5 pr-10 py-2.5 rounded-lg border border-slate-300 focus:outline-none focus:ring-2 focus:ring-[#0F6E56]/20 focus:border-[#0F6E56] text-gray-800 placeholder-slate-400 text-sm transition-colors"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center text-slate-400 hover:text-slate-600 transition-colors focus:outline-none"
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff className="w-5 h-5" />
|
||||
) : (
|
||||
<Eye className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{errors.password && (
|
||||
<p className="mt-1.5 text-sm text-red-500 font-medium">{errors.password}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="remember"
|
||||
type="checkbox"
|
||||
checked={data.remember}
|
||||
onChange={(e) => setData('remember', e.target.checked)}
|
||||
className="w-4 h-4 rounded border-slate-300 text-[#0F6E56] focus:ring-[#0F6E56]"
|
||||
/>
|
||||
<label htmlFor="remember" className="ml-2.5 text-sm text-gray-600 select-none">
|
||||
Remember me
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={processing}
|
||||
className="w-full bg-[#0F6E56] hover:bg-[#085041] active:bg-[#063c31] text-white rounded-lg h-11 flex items-center justify-center font-medium text-sm transition-colors shadow-sm disabled:opacity-70 disabled:cursor-not-allowed"
|
||||
>
|
||||
{processing ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin mr-2" />
|
||||
Signing in...
|
||||
</>
|
||||
) : (
|
||||
'Sign in to Admin'
|
||||
)}
|
||||
</button>
|
||||
<Link
|
||||
href="/employer/login"
|
||||
className="w-full h-11 flex items-center justify-center text-sm font-medium text-slate-500 hover:text-[#0F6E56] transition-colors"
|
||||
>
|
||||
Sign in as Employer
|
||||
</Link>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Footer Portals */}
|
||||
<div className="mt-10 z-10 w-full max-w-md space-y-6 px-2">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="h-[1px] flex-1 bg-slate-200" />
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]">Quick Access</span>
|
||||
<div className="h-[1px] flex-1 bg-slate-200" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Link
|
||||
href="/mobile/employer/login"
|
||||
className="flex flex-col items-center justify-center p-6 bg-white border border-slate-100 rounded-[32px] shadow-sm hover:shadow-md transition-all group space-y-3"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-2xl bg-[#185FA5]/5 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<Smartphone className="w-6 h-6 text-[#185FA5]" />
|
||||
</div>
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-slate-900 text-center">Employer Mobile</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/mobile/worker/register"
|
||||
className="flex flex-col items-center justify-center p-6 bg-white border border-slate-100 rounded-[32px] shadow-sm hover:shadow-md transition-all group space-y-3"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-2xl bg-[#185FA5]/5 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<Smartphone className="w-6 h-6 text-[#185FA5]" />
|
||||
</div>
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-slate-900 text-center">Employee Mobile</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
213
resources/js/Pages/Admin/Dashboard.jsx
Normal file
213
resources/js/Pages/Admin/Dashboard.jsx
Normal file
@ -0,0 +1,213 @@
|
||||
import React from 'react';
|
||||
import { Head, Link, router } from '@inertiajs/react';
|
||||
import AdminLayout from '@/Layouts/AdminLayout';
|
||||
import {
|
||||
Users,
|
||||
ShieldAlert,
|
||||
CreditCard,
|
||||
BadgeDollarSign,
|
||||
CheckCircle,
|
||||
ArrowRight
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
|
||||
export default function Dashboard({ stats, recent_verifications, recent_subscriptions }) {
|
||||
const handleApprove = (id) => {
|
||||
router.post('/admin/verifications/approve', { id });
|
||||
};
|
||||
|
||||
const handleReject = (id) => {
|
||||
router.post('/admin/verifications/reject', { id });
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminLayout title="Dashboard Overview">
|
||||
<Head title="Admin Dashboard" />
|
||||
|
||||
{/* Stat Cards Row */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5 mb-8 font-sans">
|
||||
{/* Total Workers */}
|
||||
<div className="bg-white rounded-xl p-5 border border-gray-200 shadow-sm flex flex-col justify-between hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-gray-500">Total Workers</span>
|
||||
<div className="w-10 h-10 bg-[#0F6E56]/10 rounded-lg flex items-center justify-center shadow-inner">
|
||||
<Users className="w-5 h-5 text-[#0F6E56]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<h3 className="text-2xl font-bold text-gray-900 tracking-tight">
|
||||
{stats?.total_workers?.toLocaleString() || 0}
|
||||
</h3>
|
||||
<p className="text-xs text-emerald-600 font-medium mt-1">+12% from last month</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Verified Today */}
|
||||
<div className="bg-white rounded-xl p-5 border border-gray-200 shadow-sm flex flex-col justify-between hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-gray-500">Auto-Verified Today</span>
|
||||
<div className="w-10 h-10 bg-emerald-50 rounded-lg flex items-center justify-center relative shadow-inner">
|
||||
<CheckCircle className="w-5 h-5 text-emerald-600" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-baseline justify-between">
|
||||
<h3 className="text-2xl font-bold text-gray-900 tracking-tight">
|
||||
12
|
||||
</h3>
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold bg-emerald-100 text-emerald-800">
|
||||
Automatic
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active Subscriptions */}
|
||||
<div className="bg-white rounded-xl p-5 border border-gray-200 shadow-sm flex flex-col justify-between hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-gray-500">Active Subscriptions</span>
|
||||
<div className="w-10 h-10 bg-blue-50 rounded-lg flex items-center justify-center shadow-inner">
|
||||
<CreditCard className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<h3 className="text-2xl font-bold text-gray-900 tracking-tight">
|
||||
{stats?.active_subscriptions?.toLocaleString() || 0}
|
||||
</h3>
|
||||
<p className="text-xs text-blue-600 font-medium mt-1">
|
||||
+{stats?.new_employers_this_week || 0} new this week
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Monthly Revenue */}
|
||||
<div className="bg-white rounded-xl p-5 border border-gray-200 shadow-sm flex flex-col justify-between hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-gray-500">Revenue This Month</span>
|
||||
<div className="w-10 h-10 bg-emerald-50 rounded-lg flex items-center justify-center shadow-inner">
|
||||
<BadgeDollarSign className="w-5 h-5 text-emerald-600" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<h3 className="text-2xl font-bold text-gray-900 tracking-tight">
|
||||
AED {stats?.revenue_this_month_aed?.toLocaleString() || 0}
|
||||
</h3>
|
||||
<p className="text-xs text-emerald-600 font-medium mt-1">Updated real-time</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Verifications Section */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 shadow-sm mb-8 overflow-hidden font-sans">
|
||||
<div className="p-5 border-b border-gray-100 flex items-center justify-between bg-white">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-gray-800 tracking-tight">Recent Verifications</h2>
|
||||
<p className="text-xs text-gray-500 mt-0.5">Profiles automatically verified via document proof</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/admin/workers/verifications"
|
||||
className="text-xs font-semibold text-[#0F6E56] hover:text-[#085041] flex items-center transition-colors"
|
||||
>
|
||||
View audit logs <ArrowRight className="w-3.5 h-3.5 ml-1" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{recent_verifications && recent_verifications.length > 0 ? (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-slate-50/50 hover:bg-slate-50/50">
|
||||
<TableHead className="font-semibold text-slate-600 text-xs h-10">Name</TableHead>
|
||||
<TableHead className="font-semibold text-slate-600 text-xs h-10">Type</TableHead>
|
||||
<TableHead className="font-semibold text-slate-600 text-xs h-10">Document</TableHead>
|
||||
<TableHead className="font-semibold text-slate-600 text-xs h-10">Processed</TableHead>
|
||||
<TableHead className="font-semibold text-slate-600 text-xs h-10 text-right pr-6">Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{recent_verifications.map((item) => (
|
||||
<TableRow key={item.id} className="hover:bg-slate-50/80 transition-colors">
|
||||
<TableCell className="font-medium text-gray-900 text-sm">{item.name}</TableCell>
|
||||
<TableCell>
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
item.type === 'Worker' ? 'bg-teal-50 text-[#0F6E56]' : 'bg-indigo-50 text-indigo-700'
|
||||
}`}>
|
||||
{item.type}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-gray-600">{item.document_type}</TableCell>
|
||||
<TableCell className="text-xs text-gray-500">{item.processed_at}</TableCell>
|
||||
<TableCell className="text-right pr-6">
|
||||
<span className="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-semibold bg-emerald-100 text-emerald-800">
|
||||
<CheckCircle className="w-3 h-3 mr-1" />
|
||||
{item.status || 'Verified'}
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<div className="p-12 text-center flex flex-col items-center justify-center">
|
||||
<CheckCircle className="w-12 h-12 text-emerald-500 mb-3" />
|
||||
<h3 className="text-base font-semibold text-gray-800 tracking-tight">No recent activity</h3>
|
||||
<p className="text-xs text-gray-500 mt-1">Automatic verification system is monitoring new registrations.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Recent Subscriptions Section */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden font-sans">
|
||||
<div className="p-5 border-b border-gray-100 flex items-center justify-between bg-white">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-gray-800 tracking-tight">Recent Subscriptions</h2>
|
||||
<p className="text-xs text-gray-500 mt-0.5">Latest employer subscription activations and renewals</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/admin/subscriptions"
|
||||
className="text-xs font-semibold text-[#0F6E56] hover:text-[#085041] flex items-center transition-colors"
|
||||
>
|
||||
View all <ArrowRight className="w-3.5 h-3.5 ml-1" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{recent_subscriptions && recent_subscriptions.length > 0 ? (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-slate-50/50 hover:bg-slate-50/50">
|
||||
<TableHead className="font-semibold text-slate-600 text-xs h-10">Employer</TableHead>
|
||||
<TableHead className="font-semibold text-slate-600 text-xs h-10">Plan</TableHead>
|
||||
<TableHead className="font-semibold text-slate-600 text-xs h-10">Amount</TableHead>
|
||||
<TableHead className="font-semibold text-slate-600 text-xs h-10 text-right pr-6">Date</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{recent_subscriptions.map((item) => (
|
||||
<TableRow key={item.id} className="hover:bg-slate-50/80 transition-colors">
|
||||
<TableCell className="font-medium text-gray-900 text-sm">{item.employer_name}</TableCell>
|
||||
<TableCell>
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-700">
|
||||
{item.plan_name}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm font-medium text-gray-900">
|
||||
AED {item.amount_aed}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-gray-500 text-right pr-6">{item.subscribed_at}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<div className="p-12 text-center text-gray-500 text-xs font-medium">
|
||||
No recent subscriptions found.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
310
resources/js/Pages/Admin/Employers/Index.jsx
Normal file
310
resources/js/Pages/Admin/Employers/Index.jsx
Normal file
@ -0,0 +1,310 @@
|
||||
import React from 'react';
|
||||
import { Head } from '@inertiajs/react';
|
||||
import AdminLayout from '@/Layouts/AdminLayout';
|
||||
import {
|
||||
Search,
|
||||
Filter,
|
||||
Download,
|
||||
Briefcase,
|
||||
Mail,
|
||||
MapPin,
|
||||
Calendar,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
XCircle,
|
||||
ExternalLink,
|
||||
MoreVertical,
|
||||
BadgeDollarSign
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
export default function EmployersIndex({ employers: initialEmployers }) {
|
||||
const [searchTerm, setSearchTerm] = React.useState('');
|
||||
const [statusFilter, setStatusFilter] = React.useState('All');
|
||||
const [locationFilter, setLocationFilter] = React.useState('All');
|
||||
const [selectedEmployer, setSelectedEmployer] = React.useState(null);
|
||||
const [isDialogOpen, setIsDialogOpen] = React.useState(false);
|
||||
|
||||
const [employers, setEmployers] = React.useState(initialEmployers || [
|
||||
{ id: 1, name: 'Al Barari Real Estate', email: 'hr@albarari.ae', plan: 'Premium Pass', status: 'Active', location: 'Dubai', join_date: '2026-04-10', phone: '+971 50 123 4567', contact_person: 'Ahmed Malik', website: 'www.albarari.ae', company_id: 'TRD-991283' },
|
||||
{ id: 2, name: 'Marina Cleaners', email: 'contact@marinaclean.com', plan: 'Basic Search', status: 'Active', location: 'Abu Dhabi', join_date: '2026-04-12', phone: '+971 50 234 5678', contact_person: 'Sarah Johnson', website: 'marinaclean.com', company_id: 'TRD-102938' },
|
||||
{ id: 3, name: 'Golden Hospitality', email: 'hiring@golden.hotel', plan: 'None', status: 'Pending', location: 'Sharjah', join_date: '2026-05-01', phone: '+971 50 345 6789', contact_person: 'John Doe', website: 'golden.hotel', company_id: 'TRD-882731' },
|
||||
{ id: 4, name: 'Emirates Logistics', email: 'jobs@emirateslogistics.com', plan: 'Premium Pass', status: 'Active', location: 'Dubai', join_date: '2026-03-20', phone: '+971 50 456 7890', contact_person: 'Mike Ross', website: 'emirateslogistics.com', company_id: 'TRD-771625' },
|
||||
{ id: 5, name: 'Dubai Mall Services', email: 'support@dubaimall.ae', plan: 'VIP Concierge', status: 'Suspended', location: 'Dubai', join_date: '2026-02-15', phone: '+971 50 567 8901', contact_person: 'Jane Foster', website: 'dubaimall.ae', company_id: 'TRD-660514' },
|
||||
]);
|
||||
|
||||
const filteredEmployers = React.useMemo(() => {
|
||||
return employers.filter(emp => {
|
||||
const matchesSearch = emp.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
emp.email.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesStatus = statusFilter === 'All' || emp.status === statusFilter;
|
||||
const matchesLocation = locationFilter === 'All' || emp.location === locationFilter;
|
||||
return matchesSearch && matchesStatus && matchesLocation;
|
||||
});
|
||||
}, [employers, searchTerm, statusFilter, locationFilter]);
|
||||
|
||||
const handleApprove = (id) => {
|
||||
setEmployers(employers.map(emp =>
|
||||
emp.id === id ? { ...emp, status: 'Active' } : emp
|
||||
));
|
||||
};
|
||||
|
||||
const handleAction = (emp) => {
|
||||
setSelectedEmployer(emp);
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminLayout title="Employer Management">
|
||||
<Head title="Employers" />
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Search and Filters */}
|
||||
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
|
||||
<div className="flex flex-1 items-center space-x-3">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name or email..."
|
||||
className="w-full pl-10 pr-4 py-2 bg-white border border-slate-200 rounded-xl text-sm font-medium focus:ring-4 focus:ring-teal-500/10 outline-none transition-all"
|
||||
value={searchTerm}
|
||||
onChange={e => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<select
|
||||
className="px-4 py-2 bg-white border border-slate-200 rounded-xl text-xs font-bold text-slate-600 outline-none focus:ring-2 focus:ring-teal-500/10 transition-all"
|
||||
value={statusFilter}
|
||||
onChange={e => setStatusFilter(e.target.value)}
|
||||
>
|
||||
<option value="All">All Status</option>
|
||||
<option value="Active">Active</option>
|
||||
<option value="Pending">Pending</option>
|
||||
<option value="Suspended">Suspended</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
className="px-4 py-2 bg-white border border-slate-200 rounded-xl text-xs font-bold text-slate-600 outline-none focus:ring-2 focus:ring-teal-500/10 transition-all"
|
||||
value={locationFilter}
|
||||
onChange={e => setLocationFilter(e.target.value)}
|
||||
>
|
||||
<option value="All">All Locations</option>
|
||||
<option value="Dubai">Dubai</option>
|
||||
<option value="Abu Dhabi">Abu Dhabi</option>
|
||||
<option value="Sharjah">Sharjah</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<button className="flex items-center space-x-2 px-6 py-2 bg-[#0F6E56] text-white rounded-xl text-sm font-bold shadow-lg shadow-teal-500/10 hover:bg-[#085041] transition-all">
|
||||
<Download className="w-4 h-4" />
|
||||
<span>Export CSV</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Employers Table */}
|
||||
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-slate-50/50 hover:bg-slate-50/50">
|
||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 pl-8">Employer Info</TableHead>
|
||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Location</TableHead>
|
||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Plan</TableHead>
|
||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Reg. Date</TableHead>
|
||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Status</TableHead>
|
||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 text-right pr-8">Action</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredEmployers.map((emp) => (
|
||||
<TableRow key={emp.id} className="hover:bg-slate-50/50 transition-colors">
|
||||
<TableCell className="py-5 pl-8">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-slate-50 flex items-center justify-center border border-slate-100">
|
||||
<Briefcase className="w-5 h-5 text-slate-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-slate-900 leading-none mb-1">{emp.name}</h4>
|
||||
<p className="text-xs text-slate-500 font-medium">{emp.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center space-x-2 text-slate-600">
|
||||
<MapPin className="w-3.5 h-3.5 text-slate-400" />
|
||||
<span className="text-xs font-bold">{emp.location}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className={`px-2.5 py-1 rounded-lg text-[10px] font-black uppercase tracking-tight ${
|
||||
emp.plan === 'None' ? 'bg-slate-100 text-slate-500' : 'bg-blue-50 text-blue-600'
|
||||
}`}>
|
||||
{emp.plan}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs font-bold text-slate-400">
|
||||
{emp.join_date}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center space-x-2">
|
||||
{emp.status === 'Active' && <CheckCircle className="w-4 h-4 text-emerald-500" />}
|
||||
{emp.status === 'Pending' && <Clock className="w-4 h-4 text-amber-500" />}
|
||||
{emp.status === 'Suspended' && <XCircle className="w-4 h-4 text-rose-500" />}
|
||||
<span className={`text-[10px] font-black uppercase tracking-widest ${
|
||||
emp.status === 'Active' ? 'text-emerald-600' :
|
||||
emp.status === 'Pending' ? 'text-amber-600' : 'text-rose-600'
|
||||
}`}>
|
||||
{emp.status}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right pr-8">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className="p-2 text-slate-400 hover:text-slate-900 hover:bg-slate-50 rounded-lg transition-all outline-none">
|
||||
<MoreVertical className="w-5 h-5" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48 p-2 rounded-xl">
|
||||
<DropdownMenuLabel className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-2 py-2">Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="p-3 rounded-lg focus:bg-slate-50 cursor-pointer" onClick={() => handleAction(emp)}>
|
||||
<div className="flex items-center space-x-3">
|
||||
<ExternalLink className="w-4 h-4 text-slate-500" />
|
||||
<span className="text-xs font-bold">View Details</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
{emp.status === 'Pending' && (
|
||||
<DropdownMenuItem className="p-3 rounded-lg focus:bg-emerald-50 cursor-pointer text-emerald-600" onClick={() => handleApprove(emp.id)}>
|
||||
<div className="flex items-center space-x-3">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
<span className="text-xs font-bold">Approve Employer</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="p-3 rounded-lg focus:bg-rose-50 cursor-pointer text-rose-600">
|
||||
<div className="flex items-center space-x-3">
|
||||
<XCircle className="w-4 h-4" />
|
||||
<span className="text-xs font-bold">Suspend Account</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogContent className="max-w-2xl bg-white p-0 overflow-hidden rounded-[32px] border-none shadow-2xl">
|
||||
<div className="bg-[#0F6E56] p-8 text-white relative">
|
||||
<div className="absolute top-0 right-0 p-8 opacity-10">
|
||||
<Briefcase className="w-32 h-32" />
|
||||
</div>
|
||||
<div className="relative z-10 flex items-center space-x-4">
|
||||
<div className="w-16 h-16 rounded-2xl bg-white/20 backdrop-blur-md flex items-center justify-center border border-white/20">
|
||||
<Briefcase className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-black tracking-tight">{selectedEmployer?.name}</h2>
|
||||
<p className="text-teal-100 text-sm font-medium">{selectedEmployer?.location} • Registered {selectedEmployer?.join_date}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-8 space-y-8">
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Primary Email</label>
|
||||
<div className="flex items-center space-x-2 text-sm font-bold text-slate-900">
|
||||
<Mail className="w-4 h-4 text-teal-600" />
|
||||
<span>{selectedEmployer?.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Phone Number</label>
|
||||
<div className="flex items-center space-x-2 text-sm font-bold text-slate-900">
|
||||
<Clock className="w-4 h-4 text-teal-600" />
|
||||
<span>{selectedEmployer?.phone}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Contact Person</label>
|
||||
<div className="text-sm font-bold text-slate-900">{selectedEmployer?.contact_person}</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Company ID</label>
|
||||
<div className="text-sm font-bold text-slate-900">{selectedEmployer?.company_id}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-50 rounded-2xl p-6 border border-slate-100 flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Subscription Plan</label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<BadgeDollarSign className="w-4 h-4 text-blue-600" />
|
||||
<span className="text-sm font-black text-blue-600">{selectedEmployer?.plan}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Current Status</label>
|
||||
<div className={`text-xs font-black uppercase tracking-widest ${
|
||||
selectedEmployer?.status === 'Active' ? 'text-emerald-600' : 'text-amber-600'
|
||||
}`}>
|
||||
{selectedEmployer?.status}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end space-x-3 pt-4 border-t border-slate-100">
|
||||
{selectedEmployer?.status === 'Pending' && (
|
||||
<button
|
||||
onClick={() => { handleApprove(selectedEmployer.id); setIsDialogOpen(false); }}
|
||||
className="px-6 py-2.5 bg-[#0F6E56] text-white rounded-xl text-[10px] font-black uppercase tracking-widest shadow-lg shadow-teal-500/10 hover:bg-[#085041] transition-all"
|
||||
>
|
||||
Approve Employer
|
||||
</button>
|
||||
)}
|
||||
<button className="px-6 py-2.5 bg-slate-900 text-white rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-slate-800 transition-all">
|
||||
Send Message
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
193
resources/js/Pages/Admin/MasterData/WorkerCategories.jsx
Normal file
193
resources/js/Pages/Admin/MasterData/WorkerCategories.jsx
Normal file
@ -0,0 +1,193 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Head } from '@inertiajs/react';
|
||||
import AdminLayout from '@/Layouts/AdminLayout';
|
||||
import {
|
||||
Plus,
|
||||
Search,
|
||||
GripVertical,
|
||||
Edit2,
|
||||
Trash2,
|
||||
Check,
|
||||
X,
|
||||
LayoutGrid,
|
||||
Info,
|
||||
ChevronRight,
|
||||
ArrowUp,
|
||||
ArrowDown
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
export default function WorkerCategories({ categories: initialCategories }) {
|
||||
const [categories, setCategories] = useState(initialCategories || [
|
||||
{ id: 1, name: 'Electrician', worker_count: 142, status: 'Active', order: 1 },
|
||||
{ id: 2, name: 'Mason', worker_count: 89, status: 'Active', order: 2 },
|
||||
{ id: 3, name: 'Plumber', worker_count: 67, status: 'Active', order: 3 },
|
||||
{ id: 4, name: 'Site Supervisor', worker_count: 34, status: 'Active', order: 4 },
|
||||
{ id: 5, name: 'Helper', worker_count: 256, status: 'Active', order: 5 },
|
||||
]);
|
||||
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [editingCategory, setEditingCategory] = useState(null);
|
||||
|
||||
const handleDelete = (id) => {
|
||||
if (confirm('Delete this category? This might affect job postings.')) {
|
||||
setCategories(categories.filter(c => c.id !== id));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminLayout title="Worker Categories">
|
||||
<Head title="Master Data - Categories" />
|
||||
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
{/* Header Actions */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="relative flex-1 max-w-sm">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Find category..."
|
||||
className="w-full pl-10 pr-4 py-2.5 bg-white border border-slate-200 rounded-xl text-sm font-medium focus:ring-4 focus:ring-teal-500/10 outline-none transition-all"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => { setEditingCategory(null); setIsDialogOpen(true); }}
|
||||
className="bg-[#0F6E56] text-white px-6 py-2.5 rounded-xl text-sm font-bold flex items-center justify-center space-x-2 hover:bg-[#085041] transition-all shadow-lg shadow-teal-500/10"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>Add Category</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Categories Card */}
|
||||
<div className="bg-white rounded-[24px] border border-slate-200 shadow-sm overflow-hidden">
|
||||
<div className="p-6 border-b border-slate-50 bg-slate-50/30">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 bg-teal-50 rounded-lg">
|
||||
<LayoutGrid className="w-5 h-5 text-[#0F6E56]" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-sm font-black text-slate-900 uppercase tracking-tight">Worker Classifications</h2>
|
||||
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest mt-0.5">Manage job categories for the entire platform</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-white hover:bg-white">
|
||||
<TableHead className="w-12"></TableHead>
|
||||
<TableHead className="font-bold text-slate-400 text-[10px] uppercase tracking-widest h-12">Category Name</TableHead>
|
||||
<TableHead className="font-bold text-slate-400 text-[10px] uppercase tracking-widest h-12">Worker Count</TableHead>
|
||||
<TableHead className="font-bold text-slate-400 text-[10px] uppercase tracking-widest h-12">Status</TableHead>
|
||||
<TableHead className="font-bold text-slate-400 text-[10px] uppercase tracking-widest h-12 text-right pr-6">Action</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{categories.map((cat) => (
|
||||
<TableRow key={cat.id} className="hover:bg-slate-50/50 transition-colors group">
|
||||
<TableCell className="pl-4">
|
||||
<GripVertical className="w-4 h-4 text-slate-300 cursor-grab active:cursor-grabbing" />
|
||||
</TableCell>
|
||||
<TableCell className="py-4 font-bold text-slate-900 text-sm">
|
||||
{cat.name}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm font-black text-slate-600">{cat.worker_count}</span>
|
||||
<span className="text-[10px] font-bold text-slate-400 uppercase">Workers</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="inline-flex items-center px-2 py-0.5 bg-emerald-50 text-emerald-600 rounded text-[10px] font-black uppercase tracking-tight">
|
||||
{cat.status}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right pr-6">
|
||||
<div className="flex items-center justify-end space-x-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={() => { setEditingCategory(cat); setIsDialogOpen(true); }}
|
||||
className="p-2 text-slate-400 hover:text-[#0F6E56] hover:bg-teal-50 rounded-lg transition-all"
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(cat.id)}
|
||||
className="p-2 text-slate-400 hover:text-rose-500 hover:bg-rose-50 rounded-lg transition-all"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<div className="p-6 bg-slate-50/50 border-t border-slate-100">
|
||||
<div className="flex items-start space-x-3 text-slate-400">
|
||||
<Info className="w-4 h-4 shrink-0 mt-0.5" />
|
||||
<p className="text-[10px] font-bold uppercase tracking-wide leading-relaxed">
|
||||
Tip: You can reorder categories by dragging the handle. The order will reflect in job posting screens and filter menus for both employers and workers.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category Modal */}
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[400px] rounded-[24px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-black text-slate-900 tracking-tight">
|
||||
{editingCategory ? 'Edit Category' : 'New Category'}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-4 space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Category Name</label>
|
||||
<input className="w-full px-4 py-3 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none" placeholder="e.g. Electrician" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Icon Style (Optional)</label>
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
{['⚡', '🏗️', '🔧', '🧹', '🛡️'].map(emoji => (
|
||||
<button key={emoji} className="h-10 bg-slate-50 rounded-lg flex items-center justify-center border border-slate-100 hover:bg-white transition-colors">{emoji}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="sm:justify-end gap-3 pt-4">
|
||||
<button
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
className="px-6 py-2.5 rounded-xl text-sm font-bold text-slate-400 uppercase tracking-widest hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="bg-[#0F6E56] text-white px-8 py-2.5 rounded-xl text-sm font-bold uppercase tracking-widest shadow-lg shadow-teal-500/20 active:scale-95 transition-all"
|
||||
>
|
||||
{editingCategory ? 'Update' : 'Add Category'}
|
||||
</button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
265
resources/js/Pages/Admin/Payments/Index.jsx
Normal file
265
resources/js/Pages/Admin/Payments/Index.jsx
Normal file
@ -0,0 +1,265 @@
|
||||
import React from 'react';
|
||||
import { Head } from '@inertiajs/react';
|
||||
import AdminLayout from '@/Layouts/AdminLayout';
|
||||
import {
|
||||
Download,
|
||||
ArrowUpRight,
|
||||
ArrowDownRight,
|
||||
Calendar,
|
||||
Filter,
|
||||
Search,
|
||||
CreditCard,
|
||||
TrendingUp,
|
||||
Users,
|
||||
BadgeDollarSign,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
FileText,
|
||||
MoreHorizontal,
|
||||
XCircle,
|
||||
Building2,
|
||||
Zap,
|
||||
Globe
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
export default function PaymentsIndex({ stats, payments: initialPayments }) {
|
||||
const [selectedPayment, setSelectedPayment] = React.useState(null);
|
||||
const [isDialogOpen, setIsDialogOpen] = React.useState(false);
|
||||
|
||||
const payments = initialPayments || [
|
||||
{ id: 'PAY-001', employer: 'Al Barari Real Estate', plan: 'Premium Pass', amount: 199, date: '2026-05-15', status: 'Completed', method: 'Visa •••• 4242', period: 'May 2026 - June 2026' },
|
||||
{ id: 'PAY-002', employer: 'Marina Cleaners', plan: 'Basic Search', amount: 99, date: '2026-05-14', status: 'Completed', method: 'Mastercard •••• 8812', period: 'May 2026 - June 2026' },
|
||||
{ id: 'PAY-003', employer: 'Golden Hospitality', plan: 'VIP Concierge', amount: 499, date: '2026-05-14', status: 'Pending', method: 'Bank Transfer', period: 'May 2026 - June 2026' },
|
||||
{ id: 'PAY-004', employer: 'Emirates Logistics', plan: 'Premium Pass', amount: 199, date: '2026-05-13', status: 'Completed', method: 'Visa •••• 1002', period: 'May 2026 - June 2026' },
|
||||
{ id: 'PAY-005', employer: 'Dubai Mall Services', plan: 'Premium Pass', amount: 199, date: '2026-05-12', status: 'Failed', method: 'Visa •••• 4242', period: 'May 2026 - June 2026' },
|
||||
];
|
||||
|
||||
const analytics = [
|
||||
{ title: 'Total Revenue', value: '42,850', icon: BadgeDollarSign, trend: '+15.2%', positive: true, color: 'emerald' },
|
||||
{ title: 'Active Subscriptions', value: '184', icon: Users, trend: '+4.3%', positive: true, color: 'blue' },
|
||||
{ title: 'Failed Payments', value: '3', icon: XCircle, trend: '-2.1%', positive: true, color: 'rose' },
|
||||
{ title: 'Average Ticket', value: '232', icon: TrendingUp, trend: '+0.8%', positive: true, color: 'amber' },
|
||||
];
|
||||
|
||||
const openDetails = (pay) => {
|
||||
setSelectedPayment(pay);
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminLayout title="Financial Analytics">
|
||||
<Head title="Payments & Revenue" />
|
||||
|
||||
<div className="space-y-8">
|
||||
{/* Stats Overview */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{analytics.map((item, i) => (
|
||||
<div key={i} className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className={`w-12 h-12 rounded-xl flex items-center justify-center bg-${item.color}-50 text-${item.color}-600 shadow-inner`}>
|
||||
<item.icon className="w-6 h-6" />
|
||||
</div>
|
||||
<div className={`flex items-center space-x-1 px-2 py-1 rounded-full text-[10px] font-black uppercase tracking-tight ${
|
||||
item.positive ? 'bg-emerald-50 text-emerald-600' : 'bg-rose-50 text-rose-600'
|
||||
}`}>
|
||||
{item.positive ? <ArrowUpRight className="w-3 h-3" /> : <ArrowDownRight className="w-3 h-3" />}
|
||||
<span>{item.trend}</span>
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-sm font-semibold text-slate-500 mb-1">{item.title}</h3>
|
||||
<div className="flex items-baseline space-x-2">
|
||||
<span className="text-2xl font-black text-slate-900 tracking-tight">{item.title.includes('Revenue') || item.title.includes('Ticket') ? 'AED ' : ''}{item.value}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
|
||||
<div className="p-6 border-b border-slate-100 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-black text-slate-900 tracking-tight">Recent Transactions</h2>
|
||||
<p className="text-xs text-slate-500 font-medium uppercase tracking-widest mt-1">Audit log of all employer subscription payments</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by Employer..."
|
||||
className="pl-10 pr-4 py-2 bg-slate-50 border border-slate-100 rounded-lg text-xs font-bold focus:ring-4 focus:ring-teal-500/10 outline-none w-48 lg:w-64"
|
||||
/>
|
||||
</div>
|
||||
<button className="p-2.5 bg-slate-50 text-slate-500 rounded-lg border border-slate-100 hover:bg-slate-100 transition-colors">
|
||||
<Filter className="w-4 h-4" />
|
||||
</button>
|
||||
<button className="flex items-center space-x-2 px-4 py-2 bg-[#0F6E56] text-white rounded-lg text-xs font-bold uppercase tracking-widest hover:bg-[#085041] transition-all shadow-lg shadow-teal-500/10">
|
||||
<Download className="w-4 h-4" />
|
||||
<span>Export</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-slate-50/50 hover:bg-slate-50/50">
|
||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 pl-8">Employer</TableHead>
|
||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Plan Name</TableHead>
|
||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Amount (AED)</TableHead>
|
||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Date</TableHead>
|
||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14">Status</TableHead>
|
||||
<TableHead className="font-bold text-slate-500 text-[10px] uppercase tracking-widest h-14 text-right pr-8">Ref</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{payments.map((pay) => (
|
||||
<TableRow
|
||||
key={pay.id}
|
||||
className="hover:bg-slate-50 transition-colors cursor-pointer group"
|
||||
onClick={() => openDetails(pay)}
|
||||
>
|
||||
<TableCell className="py-5 pl-8">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-slate-100 flex items-center justify-center text-slate-400 group-hover:bg-teal-50 group-hover:text-teal-600 transition-colors">
|
||||
<Building2 className="w-4 h-4" />
|
||||
</div>
|
||||
<span className="font-bold text-slate-900 text-sm">{pay.employer}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="px-2.5 py-1 bg-blue-50 text-blue-600 rounded-lg text-[10px] font-black uppercase tracking-tight">
|
||||
{pay.plan}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="font-black text-slate-900 tracking-tight">{pay.amount}</TableCell>
|
||||
<TableCell className="text-xs font-bold text-slate-400">{pay.date}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center space-x-2">
|
||||
{pay.status === 'Completed' && <CheckCircle2 className="w-4 h-4 text-emerald-500" />}
|
||||
{pay.status === 'Pending' && <Clock className="w-4 h-4 text-amber-500" />}
|
||||
{pay.status === 'Failed' && <XCircle className="w-4 h-4 text-rose-500" />}
|
||||
<span className={`text-[10px] font-black uppercase tracking-widest ${
|
||||
pay.status === 'Completed' ? 'text-emerald-600' :
|
||||
pay.status === 'Pending' ? 'text-amber-600' : 'text-rose-600'
|
||||
}`}>
|
||||
{pay.status}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right pr-8">
|
||||
<span className="text-[10px] font-mono font-bold text-slate-300 uppercase">{pay.id}</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<div className="p-6 border-t border-slate-100 flex items-center justify-between bg-slate-50/30">
|
||||
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Showing 5 of 1,284 transactions</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button className="px-4 py-2 bg-white border border-slate-200 rounded-lg text-[10px] font-black uppercase tracking-widest text-slate-500 hover:bg-slate-50 transition-colors">Previous</button>
|
||||
<button className="px-4 py-2 bg-white border border-slate-200 rounded-lg text-[10px] font-black uppercase tracking-widest text-slate-500 hover:bg-slate-50 transition-colors">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogContent className="max-w-2xl bg-white p-0 overflow-hidden rounded-[32px] border-none shadow-2xl">
|
||||
<div className="bg-slate-900 p-8 text-white relative">
|
||||
<div className="absolute top-0 right-0 p-8 opacity-10">
|
||||
<BadgeDollarSign className="w-32 h-32" />
|
||||
</div>
|
||||
<div className="relative z-10 space-y-2">
|
||||
<div className="flex items-center space-x-2 text-[10px] font-black uppercase tracking-[0.2em] text-teal-400">
|
||||
<Zap className="w-3 h-3" />
|
||||
<span>Transaction Details</span>
|
||||
</div>
|
||||
<h2 className="text-3xl font-black tracking-tight">{selectedPayment?.id}</h2>
|
||||
<p className="text-slate-400 text-sm font-medium">{selectedPayment?.date} • External Reference: TRX-99283-PL</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-8 space-y-8">
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Employer</label>
|
||||
<div className="flex items-center space-x-2 text-sm font-bold text-slate-900">
|
||||
<Building2 className="w-4 h-4 text-[#0F6E56]" />
|
||||
<span>{selectedPayment?.employer}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Subscription Plan</label>
|
||||
<div className="flex items-center space-x-2 text-sm font-bold text-slate-900">
|
||||
<Zap className="w-4 h-4 text-amber-500" />
|
||||
<span>{selectedPayment?.plan}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4 text-right">
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Amount Paid</label>
|
||||
<div className="text-2xl font-black text-slate-900">AED {selectedPayment?.amount}</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Payment Status</label>
|
||||
<div className={`text-xs font-black uppercase tracking-widest ${
|
||||
selectedPayment?.status === 'Completed' ? 'text-emerald-600' :
|
||||
selectedPayment?.status === 'Pending' ? 'text-amber-600' : 'text-rose-600'
|
||||
}`}>
|
||||
{selectedPayment?.status}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-50 rounded-2xl p-6 border border-slate-100 grid grid-cols-2 gap-6">
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Billing Period</label>
|
||||
<div className="text-xs font-bold text-slate-700 flex items-center space-x-2">
|
||||
<Calendar className="w-3.5 h-3.5" />
|
||||
<span>{selectedPayment?.period}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Payment Method</label>
|
||||
<div className="text-xs font-bold text-slate-700 flex items-center space-x-2">
|
||||
<CreditCard className="w-3.5 h-3.5" />
|
||||
<span>{selectedPayment?.method}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-4 border-t border-slate-100">
|
||||
<div className="flex items-center space-x-2 text-[10px] font-bold text-slate-400 uppercase tracking-widest">
|
||||
<Globe className="w-3.5 h-3.5" />
|
||||
<span>IP: 192.168.1.1 (Dubai, UAE)</span>
|
||||
</div>
|
||||
<button className="text-[10px] font-black text-[#0F6E56] uppercase tracking-widest hover:underline flex items-center space-x-1">
|
||||
<FileText className="w-3.5 h-3.5" />
|
||||
<span>Download PDF Invoice</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
208
resources/js/Pages/Admin/Subscriptions/Index.jsx
Normal file
208
resources/js/Pages/Admin/Subscriptions/Index.jsx
Normal file
@ -0,0 +1,208 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Head, router } from '@inertiajs/react';
|
||||
import AdminLayout from '@/Layouts/AdminLayout';
|
||||
import {
|
||||
Plus,
|
||||
Edit2,
|
||||
Trash2,
|
||||
Check,
|
||||
X,
|
||||
Info,
|
||||
MoreVertical,
|
||||
DollarSign,
|
||||
Clock,
|
||||
Zap,
|
||||
Shield,
|
||||
Trophy,
|
||||
Search
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
export default function SubscriptionsIndex({ plans: initialPlans }) {
|
||||
const [plans, setPlans] = useState(initialPlans || [
|
||||
{ id: 'basic', name: 'Basic Search', price: 99, duration: 'Monthly', features: ['Browse 500+ workers', '10 Shortlists'], status: 'Active' },
|
||||
{ id: 'premium', name: 'Premium Pass', price: 199, duration: 'Monthly', features: ['Unlimited shortlisting', 'Direct messaging'], status: 'Active' },
|
||||
{ id: 'vip', name: 'VIP Concierge', price: 499, duration: 'Monthly', features: ['Dedicated Manager', '30-day replacement'], status: 'Active' },
|
||||
]);
|
||||
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [editingPlan, setEditingPlan] = useState(null);
|
||||
|
||||
const handleEdit = (plan) => {
|
||||
setEditingPlan(plan);
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDelete = (id) => {
|
||||
if (confirm('Are you sure you want to delete this plan?')) {
|
||||
// router.delete(`/admin/subscriptions/plans/${id}`);
|
||||
setPlans(plans.filter(p => p.id !== id));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminLayout title="Subscription Management">
|
||||
<Head title="Plans Management" />
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Header Actions */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search plans..."
|
||||
className="w-full pl-10 pr-4 py-2 bg-white border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-teal-500/10 focus:border-teal-500 outline-none transition-all"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => { setEditingPlan(null); setIsDialogOpen(true); }}
|
||||
className="bg-[#0F6E56] text-white px-4 py-2 rounded-lg text-sm font-semibold flex items-center justify-center space-x-2 hover:bg-[#085041] transition-colors shadow-sm"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>Create New Plan</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Plans Table */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-slate-50/50 hover:bg-slate-50/50">
|
||||
<TableHead className="font-semibold text-slate-600 text-xs h-12">Plan Name</TableHead>
|
||||
<TableHead className="font-semibold text-slate-600 text-xs h-12">Pricing (AED)</TableHead>
|
||||
<TableHead className="font-semibold text-slate-600 text-xs h-12">Duration</TableHead>
|
||||
<TableHead className="font-semibold text-slate-600 text-xs h-12">Key Features</TableHead>
|
||||
<TableHead className="font-semibold text-slate-600 text-xs h-12">Status</TableHead>
|
||||
<TableHead className="font-semibold text-slate-600 text-xs h-12 text-right pr-6">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{plans.map((plan) => (
|
||||
<TableRow key={plan.id} className="hover:bg-slate-50/50 transition-colors">
|
||||
<TableCell className="py-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${
|
||||
plan.id === 'basic' ? 'bg-blue-50 text-blue-600' :
|
||||
plan.id === 'premium' ? 'bg-teal-50 text-[#0F6E56]' :
|
||||
'bg-amber-50 text-amber-600'
|
||||
}`}>
|
||||
{plan.id === 'basic' ? <Zap className="w-4 h-4" /> :
|
||||
plan.id === 'premium' ? <Shield className="w-4 h-4" /> :
|
||||
<Trophy className="w-4 h-4" />}
|
||||
</div>
|
||||
<span className="font-bold text-slate-900 text-sm">{plan.name}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="font-bold text-slate-900">{plan.price}</TableCell>
|
||||
<TableCell className="text-sm text-slate-500 font-medium">{plan.duration}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{plan.features.map((f, i) => (
|
||||
<span key={i} className="px-2 py-0.5 bg-slate-100 text-slate-600 rounded text-[10px] font-bold uppercase tracking-tight">
|
||||
{f}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-black uppercase tracking-widest ${
|
||||
plan.status === 'Active' ? 'bg-emerald-50 text-emerald-700' : 'bg-slate-100 text-slate-500'
|
||||
}`}>
|
||||
{plan.status}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right pr-6">
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<button
|
||||
onClick={() => handleEdit(plan)}
|
||||
className="p-2 text-slate-400 hover:text-[#0F6E56] hover:bg-teal-50 rounded-lg transition-all"
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(plan.id)}
|
||||
className="p-2 text-slate-400 hover:text-rose-500 hover:bg-rose-50 rounded-lg transition-all"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Plan Modal (Placeholder UI) */}
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[500px] rounded-[24px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-black text-slate-900 tracking-tight">
|
||||
{editingPlan ? 'Edit Subscription Plan' : 'Create New Plan'}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-xs font-medium text-slate-500 uppercase tracking-widest">
|
||||
Configure pricing and limits for employers
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-6 py-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Plan Name</label>
|
||||
<input className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none" placeholder="e.g. Pro Search" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Price (AED)</label>
|
||||
<input type="number" className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none" placeholder="299" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Duration</label>
|
||||
<select className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none">
|
||||
<option>Monthly</option>
|
||||
<option>Quarterly</option>
|
||||
<option>Yearly</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Features (one per line)</label>
|
||||
<textarea className="w-full px-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold focus:ring-4 focus:ring-teal-500/10 outline-none min-h-[100px]" placeholder="Unlimited search Verified badges" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="sm:justify-end gap-3">
|
||||
<button
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
className="px-6 py-2.5 rounded-xl text-sm font-bold text-slate-400 uppercase tracking-widest hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="bg-[#0F6E56] text-white px-8 py-2.5 rounded-xl text-sm font-bold uppercase tracking-widest shadow-lg shadow-teal-500/20 active:scale-95 transition-all"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
233
resources/js/Pages/Admin/Workers/Index.jsx
Normal file
233
resources/js/Pages/Admin/Workers/Index.jsx
Normal file
@ -0,0 +1,233 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Head, Link, router } from '@inertiajs/react';
|
||||
import AdminLayout from '@/Layouts/AdminLayout';
|
||||
import {
|
||||
Users,
|
||||
Search,
|
||||
Filter,
|
||||
MoreHorizontal,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
UserCheck,
|
||||
UserX,
|
||||
Mail,
|
||||
Globe,
|
||||
Briefcase,
|
||||
Download,
|
||||
Phone,
|
||||
FileText
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
|
||||
export default function WorkerManagement({ workers }) {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState('all');
|
||||
|
||||
const handleToggleStatus = (id, currentStatus) => {
|
||||
const newStatus = currentStatus === 'active' ? 'inactive' : 'active';
|
||||
router.post(`/admin/workers/${id}/toggle-status`, { status: newStatus });
|
||||
};
|
||||
|
||||
const filteredWorkers = workers.filter(worker => {
|
||||
const matchesSearch = worker.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
worker.email.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesStatus = statusFilter === 'all' || worker.status === statusFilter;
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
return (
|
||||
<AdminLayout title="Worker Management">
|
||||
<Head title="Worker Management - Admin Portal" />
|
||||
|
||||
<div className="font-sans max-w-7xl mx-auto space-y-6">
|
||||
{/* Header Section */}
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 tracking-tight">Worker Management</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">View and manage worker availability and account status.</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button className="inline-flex items-center px-4 py-2 bg-white border border-gray-200 text-[#0F6E56] rounded-xl text-sm font-bold hover:bg-gray-50 transition-colors shadow-sm space-x-2">
|
||||
<Download className="w-4 h-4" />
|
||||
<span>Export Data</span>
|
||||
</button>
|
||||
<Link
|
||||
href="/admin/workers/verifications"
|
||||
className="inline-flex items-center px-4 py-2 bg-[#0F6E56] text-white rounded-xl text-sm font-bold hover:bg-[#085041] transition-colors shadow-sm"
|
||||
>
|
||||
View Verifications
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters & Search */}
|
||||
<div className="bg-white p-4 rounded-2xl border border-gray-100 shadow-sm flex flex-col md:flex-row gap-4 items-center justify-between">
|
||||
<div className="flex flex-1 items-center space-x-4 w-full md:w-auto">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name or email..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2.5 bg-gray-50 border-transparent rounded-xl text-sm focus:bg-white focus:ring-2 focus:ring-[#0F6E56]/20 focus:border-[#0F6E56] transition-all"
|
||||
/>
|
||||
</div>
|
||||
<button className="p-2.5 bg-white border border-slate-200 rounded-xl text-slate-400 hover:bg-slate-50 transition-colors">
|
||||
<Filter className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 w-full md:w-auto">
|
||||
<span className="text-[10px] font-black text-gray-400 uppercase tracking-[0.2em] mr-2">Status:</span>
|
||||
<div className="flex bg-gray-100 p-1 rounded-xl">
|
||||
{['all', 'active', 'inactive'].map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
onClick={() => setStatusFilter(f)}
|
||||
className={`px-4 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-widest transition-all ${
|
||||
statusFilter === f
|
||||
? 'bg-white text-[#0F6E56] shadow-sm'
|
||||
: 'text-gray-500 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
{f}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Workers Table */}
|
||||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-gray-50/50 hover:bg-gray-50/50">
|
||||
<TableHead className="font-black text-gray-400 text-[10px] uppercase tracking-widest py-4 pl-8">Worker Information</TableHead>
|
||||
<TableHead className="font-black text-gray-400 text-[10px] uppercase tracking-widest py-4">Contact Info</TableHead>
|
||||
<TableHead className="font-black text-gray-400 text-[10px] uppercase tracking-widest py-4">Category & Exp</TableHead>
|
||||
<TableHead className="font-black text-gray-400 text-[10px] uppercase tracking-widest py-4">Documents</TableHead>
|
||||
<TableHead className="font-black text-gray-400 text-[10px] uppercase tracking-widest py-4">Status</TableHead>
|
||||
<TableHead className="font-black text-gray-400 text-[10px] uppercase tracking-widest py-4 text-right pr-8">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredWorkers.length > 0 ? (
|
||||
filteredWorkers.map((worker) => (
|
||||
<TableRow key={worker.id} className="group hover:bg-gray-50/50 transition-colors">
|
||||
<TableCell className="py-4">
|
||||
<div className="flex items-center gap-3 pl-4">
|
||||
<div className="w-10 h-10 rounded-full bg-[#0F6E56]/10 flex items-center justify-center text-[#0F6E56] font-bold text-sm">
|
||||
{worker.name.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-gray-900 text-sm">{worker.name}</div>
|
||||
<div className="text-xs text-gray-400 font-medium mt-0.5">{worker.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center space-x-2 text-slate-600">
|
||||
<Phone className="w-3.5 h-3.5 text-slate-400" />
|
||||
<span className="text-xs font-bold">{worker.phone || '+971 50 000 0000'}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm font-medium text-gray-700 flex items-center gap-1.5">
|
||||
<Briefcase className="w-3.5 h-3.5 text-gray-400" /> {worker.category}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 flex items-center gap-1.5">
|
||||
<Globe className="w-3.5 h-3.5 text-gray-400" /> {worker.nationality} • {worker.experience}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center space-x-1.5">
|
||||
<div className="p-1.5 bg-emerald-50 rounded-lg">
|
||||
<FileText className="w-4 h-4 text-emerald-500" />
|
||||
</div>
|
||||
<span className="text-[10px] font-black text-emerald-600 uppercase">Verified</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`px-3 py-1 rounded-full font-bold text-[10px] uppercase tracking-wider ${
|
||||
worker.status === 'active'
|
||||
? 'bg-emerald-50 text-emerald-700 border-emerald-200'
|
||||
: 'bg-red-50 text-red-700 border-red-200'
|
||||
}`}
|
||||
>
|
||||
{worker.status === 'active' ? (
|
||||
<CheckCircle2 className="w-3 h-3 mr-1" />
|
||||
) : (
|
||||
<XCircle className="w-3 h-3 mr-1" />
|
||||
)}
|
||||
{worker.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right pr-8">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="p-2 hover:bg-gray-100 rounded-lg transition-colors focus:outline-none">
|
||||
<MoreHorizontal className="w-5 h-5 text-gray-400" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48 p-2 rounded-xl shadow-xl border-gray-100">
|
||||
<DropdownMenuLabel className="text-[10px] font-bold text-gray-400 uppercase px-2 mb-1">Account Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleToggleStatus(worker.id, worker.status)}
|
||||
className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer font-semibold text-xs ${
|
||||
worker.status === 'active'
|
||||
? 'text-red-600 hover:bg-red-50'
|
||||
: 'text-emerald-600 hover:bg-emerald-50'
|
||||
}`}
|
||||
>
|
||||
{worker.status === 'active' ? (
|
||||
<><UserX className="w-4 h-4" /> Deactivate Account</>
|
||||
) : (
|
||||
<><UserCheck className="w-4 h-4" /> Activate Account</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="my-1" />
|
||||
<DropdownMenuItem className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-semibold text-xs text-gray-600 hover:bg-gray-50">
|
||||
<Mail className="w-4 h-4" /> Send Message
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="flex items-center gap-2 p-2 rounded-lg cursor-pointer font-semibold text-xs text-gray-600 hover:bg-gray-50">
|
||||
<Users className="w-4 h-4" /> View Full Profile
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="py-20 text-center">
|
||||
<Users className="w-12 h-12 text-gray-200 mx-auto mb-3" />
|
||||
<div className="font-bold text-gray-400">No workers found matching your criteria.</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
305
resources/js/Pages/Admin/Workers/Verifications.jsx
Normal file
305
resources/js/Pages/Admin/Workers/Verifications.jsx
Normal file
@ -0,0 +1,305 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Head, router } from '@inertiajs/react';
|
||||
import AdminLayout from '@/Layouts/AdminLayout';
|
||||
import {
|
||||
Search,
|
||||
FileText,
|
||||
Eye,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Clock,
|
||||
Edit2,
|
||||
ZoomIn,
|
||||
X,
|
||||
Loader2
|
||||
} from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
} from '@/components/ui/pagination';
|
||||
|
||||
export default function Verifications({ verifications, status }) {
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [lightboxImage, setLightboxImage] = useState(null);
|
||||
|
||||
// Dialog state
|
||||
const [ocrOverrides, setOcrOverrides] = useState({});
|
||||
const [editingField, setEditingField] = useState(null);
|
||||
const [rejectionReason, setRejectionReason] = useState('');
|
||||
const [showRejectionInput, setShowRejectionInput] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const tabs = [
|
||||
{ label: 'All', value: 'all' },
|
||||
{ label: 'Auto-Approved', value: 'approved' },
|
||||
{ label: 'Flagged/Manual', value: 'rejected' },
|
||||
];
|
||||
|
||||
const handleTabClick = (tabValue) => {
|
||||
router.get('/admin/workers/verifications', { status: tabValue }, { preserveState: true });
|
||||
};
|
||||
|
||||
const openReview = (item) => {
|
||||
setSelectedItem(item);
|
||||
setOcrOverrides(item.ocr_data || {});
|
||||
setEditingField(null);
|
||||
setShowRejectionInput(false);
|
||||
setRejectionReason('');
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminLayout title="Worker Identity Verifications">
|
||||
<Head title="Worker Verifications - Admin Portal" />
|
||||
|
||||
<div className="font-sans max-w-7xl mx-auto space-y-6">
|
||||
{/* Header / Sub-title */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-900 tracking-tight">Identity Audit Logs</h1>
|
||||
<p className="text-xs text-gray-500 mt-0.5">Automated document verification system logs and OCR extractions.</p>
|
||||
</div>
|
||||
|
||||
{/* Filter Tabs */}
|
||||
<div className="flex bg-slate-200/60 p-1 rounded-xl w-fit border border-slate-200">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.value}
|
||||
type="button"
|
||||
onClick={() => handleTabClick(tab.value)}
|
||||
className={`px-4 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
||||
status === tab.value
|
||||
? 'bg-white text-[#0F6E56] shadow-sm'
|
||||
: 'text-slate-600 hover:text-slate-900'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop Table / Mobile Card List */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
||||
<div className="hidden sm:block overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-slate-50/70 border-b border-gray-100 text-xs font-semibold text-slate-500">
|
||||
<th className="py-3.5 px-5">Worker Name</th>
|
||||
<th className="py-3.5 px-5">Nationality</th>
|
||||
<th className="py-3.5 px-5">Passport No.</th>
|
||||
<th className="py-3.5 px-5">Processed</th>
|
||||
<th className="py-3.5 px-5">Status</th>
|
||||
<th className="py-3.5 px-5 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100 text-sm">
|
||||
{verifications?.data?.map((item) => (
|
||||
<tr key={item.id} className="hover:bg-slate-50/50 transition-colors">
|
||||
<td className="py-4 px-5 font-medium text-gray-900">{item.worker_name}</td>
|
||||
<td className="py-4 px-5 text-gray-600">{item.nationality}</td>
|
||||
<td className="py-4 px-5 font-mono text-xs text-gray-600">{item.passport_number}</td>
|
||||
<td className="py-4 px-5 text-xs text-gray-500">{item.processed_at}</td>
|
||||
<td className="py-4 px-5">
|
||||
<Badge variant="outline" className="bg-emerald-50 text-emerald-700 border-emerald-200 px-2.5 py-0.5 rounded-full font-medium text-xs">
|
||||
<CheckCircle className="w-3 h-3 mr-1 inline" /> Auto-Verified
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="py-4 px-5 text-right">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openReview(item)}
|
||||
className="inline-flex items-center px-3 py-1.5 border border-slate-200 hover:border-[#0F6E56] text-xs font-semibold text-slate-700 hover:text-[#0F6E56] bg-white hover:bg-teal-50/30 rounded-lg transition-colors focus:outline-none"
|
||||
>
|
||||
<Eye className="w-3.5 h-3.5 mr-1.5" /> View Log
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Mobile Card List */}
|
||||
<div className="block sm:hidden divide-y divide-gray-100">
|
||||
{verifications?.data?.map((item) => (
|
||||
<div key={item.id} className="p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-semibold text-gray-900">{item.worker_name}</span>
|
||||
<Badge className="bg-emerald-50 text-emerald-700 border-emerald-200">Auto-Verified</Badge>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 text-xs text-gray-600 gap-1">
|
||||
<div><span className="text-gray-400">Nationality:</span> {item.nationality}</div>
|
||||
<div><span className="text-gray-400">Passport:</span> {item.passport_number}</div>
|
||||
<div className="col-span-2 text-gray-400 text-[10px] mt-1">Processed: {item.processed_at}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openReview(item)}
|
||||
className="w-full inline-flex items-center justify-center px-3 py-2 border border-slate-200 text-xs font-semibold text-slate-700 hover:text-[#0F6E56] bg-slate-50 hover:bg-teal-50 rounded-lg transition-colors"
|
||||
>
|
||||
<Eye className="w-3.5 h-3.5 mr-1.5" /> View Audit Details
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{verifications?.data?.length === 0 && (
|
||||
<div className="p-12 text-center text-gray-500 text-sm font-medium">
|
||||
No verification logs found for the selected filter.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Pagination Component */}
|
||||
{verifications?.links && verifications.links.length > 3 && (
|
||||
<div className="flex justify-center pt-4">
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
{verifications.links.map((link, idx) => {
|
||||
const isPrev = link.label.includes('Previous');
|
||||
const isNext = link.label.includes('Next');
|
||||
const label = isPrev ? '‹' : isNext ? '›' : link.label;
|
||||
|
||||
if (!link.url) {
|
||||
return (
|
||||
<PaginationItem key={idx}>
|
||||
<span className="px-3 py-2 text-xs text-slate-300 cursor-not-allowed select-none">
|
||||
{label}
|
||||
</span>
|
||||
</PaginationItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PaginationItem key={idx}>
|
||||
<PaginationLink
|
||||
href={link.url}
|
||||
isActive={link.active}
|
||||
className={`text-xs px-3 py-2 rounded-lg transition-colors ${
|
||||
link.active ? 'bg-[#0F6E56] text-white hover:bg-[#085041] hover:text-white font-bold' : 'text-slate-600 hover:bg-slate-100'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
);
|
||||
})}
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Review Dialog */}
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogContent className="max-w-4xl bg-white p-0 overflow-hidden font-sans rounded-2xl">
|
||||
<DialogHeader className="p-6 pb-4 border-b border-slate-100 bg-slate-50/50">
|
||||
<DialogTitle className="text-lg font-semibold text-gray-800 flex items-center justify-between tracking-tight">
|
||||
<span>Verification Log: <span className="text-[#0F6E56]">{selectedItem?.worker_name}</span></span>
|
||||
<Badge className="bg-emerald-100 text-emerald-800 border-none px-3 py-1">System Verified</Badge>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 p-6 max-h-[80vh] overflow-y-auto">
|
||||
{/* Left: Document Images */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider flex items-center">
|
||||
<FileText className="w-3.5 h-3.5 mr-1.5" /> Scanned Proof Documents ({selectedItem?.document_images?.length || 0})
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{selectedItem?.document_images?.map((imgUrl, i) => (
|
||||
<div
|
||||
key={i}
|
||||
onClick={() => setLightboxImage(imgUrl)}
|
||||
className="relative group rounded-xl overflow-hidden border border-slate-200 bg-slate-100 cursor-pointer shadow-sm hover:ring-2 hover:ring-[#0F6E56]/40 transition-all aspect-[4/3]"
|
||||
>
|
||||
<img
|
||||
src={imgUrl}
|
||||
alt="Document scan"
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/30 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center text-white text-xs font-semibold backdrop-blur-[1px]">
|
||||
<ZoomIn className="w-5 h-5 mr-1.5" /> Click to View
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: OCR Data & Status */}
|
||||
<div className="space-y-6 flex flex-col justify-between">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider flex items-center justify-between">
|
||||
<span>System Extracted Data</span>
|
||||
<span className="text-[10px] text-emerald-600 bg-emerald-50 px-2 py-0.5 rounded-full font-medium">Confidence: 98%</span>
|
||||
</h3>
|
||||
|
||||
<div className="space-y-2.5">
|
||||
{Object.entries(ocrOverrides).map(([key, value]) => (
|
||||
<div key={key} className="flex items-center justify-between p-3 bg-slate-50 rounded-xl text-sm border border-slate-100 hover:border-slate-200 transition-colors">
|
||||
<span className="font-medium text-slate-600 text-xs">{key}:</span>
|
||||
<span className="text-gray-900 font-semibold text-xs font-mono">{value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Info */}
|
||||
<div className="space-y-4 pt-4 border-t border-slate-100">
|
||||
<div className="bg-emerald-50 p-4 rounded-xl border border-emerald-100 flex items-start">
|
||||
<CheckCircle className="w-5 h-5 text-emerald-600 mr-3 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-xs font-bold text-emerald-800">Verified Automatically</p>
|
||||
<p className="text-[11px] text-emerald-700 mt-1">
|
||||
This profile was verified by the system on {selectedItem?.processed_at}. No further action is required from administrators.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
className="w-full bg-slate-900 hover:bg-slate-800 text-white py-3 px-4 rounded-xl font-semibold text-xs shadow-sm flex items-center justify-center transition-colors"
|
||||
>
|
||||
Close Audit Log
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Lightbox Zoom */}
|
||||
{lightboxImage && (
|
||||
<div
|
||||
onClick={() => setLightboxImage(null)}
|
||||
className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4 animate-in fade-in duration-200"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-6 right-6 text-white/80 hover:text-white p-2 bg-white/10 hover:bg-white/20 rounded-full transition-all focus:outline-none"
|
||||
onClick={() => setLightboxImage(null)}
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
<img
|
||||
src={lightboxImage}
|
||||
alt="Zoomed document"
|
||||
className="max-w-full max-h-[90vh] object-contain rounded-xl shadow-2xl border border-white/20 animate-in zoom-in-95 duration-200"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
232
resources/js/Pages/Employer/Announcements.jsx
Normal file
232
resources/js/Pages/Employer/Announcements.jsx
Normal file
@ -0,0 +1,232 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Head } from '@inertiajs/react';
|
||||
import EmployerLayout from '../../Layouts/EmployerLayout';
|
||||
import {
|
||||
Megaphone,
|
||||
Send,
|
||||
CheckCircle2,
|
||||
Users,
|
||||
Search,
|
||||
Calendar,
|
||||
Target,
|
||||
Filter,
|
||||
ChevronRight,
|
||||
MessageSquare,
|
||||
Info
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogFooter
|
||||
} from '@/components/ui/dialog';
|
||||
|
||||
export default function Announcements() {
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [newAnnouncement, setNewAnnouncement] = useState({ title: '', content: '', audience: 'Shortlisted' });
|
||||
const [toastMessage, setToastMessage] = useState(null);
|
||||
const [announcements, setAnnouncements] = useState([
|
||||
{
|
||||
id: 1,
|
||||
title: 'Interview Schedule Updates',
|
||||
content: 'Hello everyone, we will be conducting the next round of interviews next week. Please keep your phones available and ensure you have a stable internet connection for the video call.',
|
||||
audience: 'Selected Candidates',
|
||||
created_at: '2 days ago',
|
||||
reads: 12,
|
||||
replies: 4
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Welcome to the Recruitment Pool',
|
||||
content: 'We have successfully added your profile to our recruitment pool. We will contact you if your skills match our requirements.',
|
||||
audience: 'Shortlisted',
|
||||
created_at: '5 days ago',
|
||||
reads: 45,
|
||||
replies: 0
|
||||
}
|
||||
]);
|
||||
|
||||
const showToast = (message) => {
|
||||
setToastMessage(message);
|
||||
setTimeout(() => setToastMessage(null), 3000);
|
||||
};
|
||||
|
||||
const handleCreate = (e) => {
|
||||
e.preventDefault();
|
||||
const newEntry = {
|
||||
id: announcements.length + 1,
|
||||
title: newAnnouncement.title,
|
||||
content: newAnnouncement.content,
|
||||
audience: newAnnouncement.audience,
|
||||
created_at: 'Just now',
|
||||
reads: 0,
|
||||
replies: 0
|
||||
};
|
||||
setAnnouncements([newEntry, ...announcements]);
|
||||
setNewAnnouncement({ title: '', content: '', audience: 'Shortlisted' });
|
||||
setIsDialogOpen(false);
|
||||
showToast('Announcement sent successfully');
|
||||
};
|
||||
|
||||
const filteredAnnouncements = announcements.filter(ann =>
|
||||
ann.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
ann.content.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const stats = [
|
||||
{ label: 'Total Announcements', value: announcements.length, icon: Megaphone, color: 'text-blue-600', bg: 'bg-blue-50' },
|
||||
{ label: 'Audience Reach', value: '57 Candidates', icon: Target, color: 'text-emerald-600', bg: 'bg-emerald-50' },
|
||||
{ label: 'Avg. Engagement', value: '82%', icon: CheckCircle2, color: 'text-purple-600', bg: 'bg-purple-50' },
|
||||
];
|
||||
|
||||
return (
|
||||
<EmployerLayout title="Announcements">
|
||||
<Head title="Announcements - Employer Portal" />
|
||||
|
||||
{toastMessage && (
|
||||
<div className="fixed bottom-8 right-8 bg-slate-900 text-white px-6 py-4 rounded-2xl shadow-2xl flex items-center space-x-3 z-[100] animate-in slide-in-from-bottom-5">
|
||||
<div className="bg-emerald-500 p-1 rounded-full">
|
||||
<CheckCircle2 className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<span className="font-bold text-xs uppercase tracking-widest">{toastMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-6 select-none max-w-5xl mx-auto">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-white p-6 rounded-[32px] border border-slate-100 shadow-sm">
|
||||
<div className="relative flex-1 max-w-md group">
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 group-focus-within:text-[#185FA5] transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search announcements..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-12 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<button className="bg-[#185FA5] hover:bg-[#144f8a] text-white px-8 py-3 rounded-2xl font-black text-xs uppercase tracking-widest flex items-center justify-center space-x-2 transition-all shadow-lg shadow-blue-200/50 hover:-translate-y-1 active:translate-y-0">
|
||||
<Megaphone className="w-4 h-4" />
|
||||
<span>New Announcement</span>
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[500px] rounded-3xl p-0 overflow-hidden border-none shadow-2xl">
|
||||
<div className="bg-[#185FA5] p-8 text-white relative">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-white/10 rounded-full -mr-16 -mt-16 blur-2xl" />
|
||||
<DialogTitle className="text-2xl font-black relative z-10">Post Announcement</DialogTitle>
|
||||
<DialogDescription className="text-blue-100 font-medium mt-2 relative z-10">
|
||||
This will be visible to your selected candidates immediately.
|
||||
</DialogDescription>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleCreate} className="p-8 space-y-6 bg-white">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Target Audience</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{['Shortlisted', 'Selected Candidates'].map(aud => (
|
||||
<button
|
||||
key={aud}
|
||||
type="button"
|
||||
onClick={() => setNewAnnouncement({ ...newAnnouncement, audience: aud })}
|
||||
className={`px-4 py-3 rounded-xl text-[10px] font-black uppercase tracking-widest border transition-all ${
|
||||
newAnnouncement.audience === aud
|
||||
? 'bg-blue-50 border-[#185FA5] text-[#185FA5]'
|
||||
: 'bg-slate-50 border-slate-200 text-slate-500 hover:bg-white'
|
||||
}`}
|
||||
>
|
||||
{aud}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={newAnnouncement.title}
|
||||
onChange={(e) => setNewAnnouncement({ ...newAnnouncement, title: e.target.value })}
|
||||
className="w-full px-5 py-3.5 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all outline-none"
|
||||
placeholder="e.g. Schedule for Video Interview"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Message Content</label>
|
||||
<textarea
|
||||
required
|
||||
rows="4"
|
||||
value={newAnnouncement.content}
|
||||
onChange={(e) => setNewAnnouncement({ ...newAnnouncement, content: e.target.value })}
|
||||
className="w-full px-5 py-4 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all outline-none resize-none"
|
||||
placeholder="Enter your message..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-[#185FA5] hover:bg-[#144f8a] text-white py-4 rounded-2xl text-xs font-black uppercase tracking-widest flex items-center justify-center space-x-2 transition-all shadow-lg shadow-blue-200/50"
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
<span>Post Announcement</span>
|
||||
</button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{filteredAnnouncements.length === 0 ? (
|
||||
<div className="text-center py-24 bg-white rounded-[40px] border border-slate-200 shadow-sm text-slate-500 border-dashed">
|
||||
<div className="w-20 h-20 bg-slate-50 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<Megaphone className="w-10 h-10 text-slate-200" />
|
||||
</div>
|
||||
<h3 className="font-black text-slate-900 text-lg uppercase tracking-tight">No announcements</h3>
|
||||
<p className="font-bold text-xs text-slate-400 mt-1 uppercase tracking-widest">You haven't posted any announcements yet</p>
|
||||
</div>
|
||||
) : (
|
||||
filteredAnnouncements.map((ann) => (
|
||||
<div key={ann.id} className="group bg-white rounded-[32px] border border-slate-100 p-6 shadow-sm hover:shadow-md transition-all duration-300 relative overflow-hidden flex flex-col md:flex-row md:items-start gap-6">
|
||||
<div className="flex-1 space-y-3">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<span className={`px-3 py-1.5 rounded-xl text-[9px] font-black uppercase tracking-widest border ${
|
||||
ann.audience === 'Selected Candidates'
|
||||
? 'bg-emerald-50 border-emerald-100 text-emerald-700'
|
||||
: 'bg-blue-50 border-blue-100 text-[#185FA5]'
|
||||
}`}>
|
||||
To: {ann.audience}
|
||||
</span>
|
||||
<div className="flex items-center text-slate-400 text-[10px] font-black uppercase tracking-widest">
|
||||
<Calendar className="w-3.5 h-3.5 mr-1.5" />
|
||||
{ann.created_at}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<h4 className="text-lg font-black text-slate-900 tracking-tight">{ann.title}</h4>
|
||||
<p className="text-sm font-medium text-slate-600 leading-relaxed">{ann.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3 md:border-l md:border-slate-50 md:pl-6 shrink-0">
|
||||
<button className="p-3 rounded-2xl bg-slate-50 text-slate-400 hover:text-[#185FA5] transition-all">
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</EmployerLayout>
|
||||
);
|
||||
}
|
||||
49
resources/js/Pages/Employer/Auth/ForgotPassword.jsx
Normal file
49
resources/js/Pages/Employer/Auth/ForgotPassword.jsx
Normal file
@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import { KeyRound } from 'lucide-react';
|
||||
|
||||
export default function ForgotPassword() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col justify-center items-center px-4 bg-slate-50 font-sans">
|
||||
<Head title="Reset Password - Employer Portal" />
|
||||
|
||||
<div className="w-full max-w-md bg-white rounded-2xl shadow-sm border border-slate-200 p-8 text-center space-y-6">
|
||||
<div className="w-16 h-16 bg-blue-50 rounded-full flex items-center justify-center mx-auto border border-blue-200">
|
||||
<KeyRound className="w-8 h-8 text-[#185FA5]" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-bold text-gray-900 tracking-tight">Forgot Password</h1>
|
||||
<p className="text-sm text-gray-600">
|
||||
Enter your registration email address and we'll send you a password reset link.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 text-left">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="employer@example.com"
|
||||
className="w-full px-3.5 py-2.5 rounded-xl border border-slate-300 text-sm focus:outline-none focus:ring-2 focus:ring-[#185FA5]/20 focus:border-[#185FA5]"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="w-full bg-[#185FA5] hover:bg-[#0C447C] text-white rounded-xl h-11 font-semibold text-sm transition-colors shadow-sm"
|
||||
>
|
||||
Send Reset Link
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<Link href="/employer/login" className="text-xs text-[#185FA5] font-bold hover:underline">
|
||||
Return to Sign in
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
203
resources/js/Pages/Employer/Auth/Login.jsx
Normal file
203
resources/js/Pages/Employer/Auth/Login.jsx
Normal file
@ -0,0 +1,203 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useForm, Head, Link } from '@inertiajs/react';
|
||||
import {
|
||||
CheckCircle,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Loader2,
|
||||
AlertTriangle,
|
||||
Info,
|
||||
XCircle,
|
||||
Users,
|
||||
DollarSign,
|
||||
MessageSquare
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function Login({ flash }) {
|
||||
const { data, setData, post, processing, errors } = useForm({
|
||||
email: '',
|
||||
password: '',
|
||||
remember: false,
|
||||
});
|
||||
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
post('/employer/login');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen grid grid-cols-1 lg:grid-cols-12 bg-slate-50 font-sans">
|
||||
<Head title="Employer Login - Marketplace Portal" />
|
||||
|
||||
{/* Left Brand Panel (Desktop Only) */}
|
||||
<div className="hidden lg:flex lg:col-span-5 bg-[#185FA5] p-12 flex-col justify-between text-white relative overflow-hidden select-none">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-600/20 to-black/30 pointer-events-none" />
|
||||
|
||||
<div className="relative z-10 space-y-6">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 bg-white text-[#185FA5] rounded-xl flex items-center justify-center font-bold text-xl shadow-md">
|
||||
M
|
||||
</div>
|
||||
<span className="font-bold text-2xl tracking-tight">Marketplace Portal</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 pt-6">
|
||||
<span className="inline-block px-3 py-1 bg-white/10 border border-white/20 rounded-full text-xs font-semibold uppercase tracking-wider">
|
||||
Employer Login
|
||||
</span>
|
||||
<h1 className="text-4xl font-extrabold tracking-tight leading-tight">
|
||||
Find verified domestic workers — no agents, no fees.
|
||||
</h1>
|
||||
<p className="text-blue-100 text-sm font-medium">
|
||||
Sign in to review your shortlisted candidates, manage interview schedules, and communicate directly.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stat Row */}
|
||||
<div className="grid grid-cols-3 gap-4 pt-8 border-t border-white/10">
|
||||
<div className="bg-white/10 backdrop-blur-sm p-4 rounded-xl border border-white/10 text-center space-y-1">
|
||||
<Users className="w-6 h-6 text-emerald-300 mx-auto" />
|
||||
<div className="font-bold text-lg">500+</div>
|
||||
<div className="text-[10px] text-blue-100 uppercase tracking-wider font-semibold">Verified Workers</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white/10 backdrop-blur-sm p-4 rounded-xl border border-white/10 text-center space-y-1">
|
||||
<DollarSign className="w-6 h-6 text-emerald-300 mx-auto" />
|
||||
<div className="font-bold text-lg">0 AED</div>
|
||||
<div className="text-[10px] text-blue-100 uppercase tracking-wider font-semibold">Agent Fees</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white/10 backdrop-blur-sm p-4 rounded-xl border border-white/10 text-center space-y-1">
|
||||
<MessageSquare className="w-6 h-6 text-emerald-300 mx-auto" />
|
||||
<div className="font-bold text-lg">Direct</div>
|
||||
<div className="text-[10px] text-blue-100 uppercase tracking-wider font-semibold">Messaging</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 text-xs text-blue-200 font-medium">
|
||||
© 2026 Domestic Worker Marketplace. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Login Form */}
|
||||
<div className="col-span-1 lg:col-span-7 flex flex-col justify-center items-center p-6 sm:p-12 overflow-y-auto">
|
||||
<div className="w-full max-w-md bg-white rounded-2xl shadow-sm border border-slate-200 p-8 space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 tracking-tight">Welcome Back</h2>
|
||||
<p className="text-xs text-gray-500 mt-1">Sign in to your employer portal to continue searching</p>
|
||||
</div>
|
||||
|
||||
{/* Status Flash Alerts */}
|
||||
{flash?.status === 'pending_verification' && (
|
||||
<div className="flex items-start space-x-3 p-4 bg-amber-50 border border-amber-200 rounded-xl text-amber-800 text-xs font-medium">
|
||||
<AlertTriangle className="w-5 h-5 text-amber-600 flex-shrink-0 mt-0.5" />
|
||||
<span>Your account is pending Emirates ID verification. We'll notify you within 24 hours.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{flash?.status === 'rejected' && (
|
||||
<div className="flex items-start space-x-3 p-4 bg-red-50 border border-red-200 rounded-xl text-red-800 text-xs font-medium">
|
||||
<XCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<div className="font-bold">Verification Rejected</div>
|
||||
<div className="mt-0.5 text-red-700">Reason: {flash?.reason || 'Document unreadable.'} Please re-register.</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{flash?.status === 'subscription_expired' && (
|
||||
<div className="flex items-start space-x-3 p-4 bg-blue-50 border border-blue-200 rounded-xl text-blue-800 text-xs font-medium">
|
||||
<Info className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
|
||||
<span>Your subscription has expired. Renew to continue searching and shortlisting.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Demonstration Helper Note */}
|
||||
<div className="p-3 bg-slate-50 border border-slate-200 rounded-xl text-[11px] text-slate-600 space-y-1">
|
||||
<div className="font-semibold text-slate-700">💡 Scaffolding Demo Emails:</div>
|
||||
<div>• Active Employer: <code className="text-[#185FA5] font-bold">employer@example.com</code></div>
|
||||
<div>• Pending Alert: <code className="text-amber-600 font-bold">pending@example.com</code></div>
|
||||
<div>• Rejected Alert: <code className="text-red-600 font-bold">rejected@example.com</code></div>
|
||||
<div>• Expired Alert: <code className="text-blue-600 font-bold">expired@example.com</code></div>
|
||||
<div className="text-[10px] text-slate-500 pt-1">Password for all: <code className="font-bold">password</code></div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
value={data.email}
|
||||
onChange={(e) => setData('email', e.target.value)}
|
||||
placeholder="employer@example.com"
|
||||
className="w-full px-3.5 py-2.5 rounded-xl border border-slate-300 text-sm focus:outline-none focus:ring-2 focus:ring-[#185FA5]/20 focus:border-[#185FA5]"
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
{errors.email && <p className="mt-1 text-xs text-red-500">{errors.email}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<label className="block text-xs font-medium text-gray-700">Password</label>
|
||||
<Link href="/employer/forgot-password" className="text-xs text-[#185FA5] font-semibold hover:underline">
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={data.password}
|
||||
onChange={(e) => setData('password', e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className="w-full pl-3.5 pr-10 py-2.5 rounded-xl border border-slate-300 text-sm focus:outline-none focus:ring-2 focus:ring-[#185FA5]/20 focus:border-[#185FA5]"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center text-slate-400 hover:text-slate-600 focus:outline-none"
|
||||
>
|
||||
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="remember"
|
||||
type="checkbox"
|
||||
checked={data.remember}
|
||||
onChange={(e) => setData('remember', e.target.checked)}
|
||||
className="h-4 w-4 rounded border-slate-300 text-[#185FA5] focus:ring-[#185FA5]"
|
||||
/>
|
||||
<label htmlFor="remember" className="ml-2 block text-xs text-gray-700 select-none">
|
||||
Remember me for 30 days
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={processing}
|
||||
className="w-full bg-[#185FA5] hover:bg-[#0C447C] active:bg-[#08305c] text-white rounded-xl h-11 font-semibold text-sm transition-colors shadow-sm flex items-center justify-center disabled:opacity-70 disabled:cursor-not-allowed"
|
||||
>
|
||||
{processing ? <Loader2 className="w-5 h-5 animate-spin mr-2" /> : 'Sign in to Employer Portal'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="border-t border-slate-100 pt-6 text-center">
|
||||
<p className="text-xs text-gray-600 font-medium">
|
||||
New employer?{' '}
|
||||
<Link href="/employer/register" className="text-[#185FA5] font-bold hover:underline">
|
||||
Create account
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
46
resources/js/Pages/Employer/Auth/PendingVerification.jsx
Normal file
46
resources/js/Pages/Employer/Auth/PendingVerification.jsx
Normal file
@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import { Clock, CheckCircle } from 'lucide-react';
|
||||
|
||||
export default function PendingVerification() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col justify-center items-center px-4 bg-slate-50 font-sans">
|
||||
<Head title="Verification Pending - Employer Portal" />
|
||||
|
||||
<div className="w-full max-w-md bg-white rounded-2xl shadow-sm border border-slate-200 p-8 text-center space-y-6">
|
||||
<div className="w-16 h-16 bg-amber-50 rounded-full flex items-center justify-center mx-auto border border-amber-200">
|
||||
<Clock className="w-8 h-8 text-amber-600 animate-pulse" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-bold text-gray-900 tracking-tight">Account Under Review</h1>
|
||||
<p className="text-sm text-gray-600">
|
||||
Thank you for registering. We have received your Emirates ID documents and our team is currently verifying your details.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100 text-xs text-left space-y-2 text-slate-600">
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckCircle className="w-4 h-4 text-emerald-600" />
|
||||
<span>Emirates ID front scan received</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckCircle className="w-4 h-4 text-emerald-600" />
|
||||
<span>Emirates ID back scan received</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckCircle className="w-4 h-4 text-emerald-600" />
|
||||
<span>Review expected within 24 hours</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href="/employer/dashboard"
|
||||
className="w-full bg-[#185FA5] hover:bg-[#144f8a] text-white rounded-xl h-11 font-semibold text-sm transition-colors shadow-sm flex items-center justify-center"
|
||||
>
|
||||
Proceed to Dashboard Preview
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
510
resources/js/Pages/Employer/Auth/Register.jsx
Normal file
510
resources/js/Pages/Employer/Auth/Register.jsx
Normal file
@ -0,0 +1,510 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useForm, Head, Link } from '@inertiajs/react';
|
||||
import {
|
||||
CheckCircle,
|
||||
Camera,
|
||||
Eye,
|
||||
EyeOff,
|
||||
X,
|
||||
Loader2,
|
||||
FileText,
|
||||
AlertCircle,
|
||||
CreditCard,
|
||||
ShieldCheck,
|
||||
ChevronRight,
|
||||
Lock,
|
||||
User,
|
||||
Mail,
|
||||
Phone as PhoneIcon,
|
||||
ArrowRight,
|
||||
ArrowLeft,
|
||||
Check,
|
||||
Briefcase
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function Register() {
|
||||
const [step, setStep] = useState(1);
|
||||
const { data, setData, post, processing, errors } = useForm({
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
emirates_id_front: null,
|
||||
emirates_id_back: null,
|
||||
selected_plan: 'premium',
|
||||
card_number: '',
|
||||
expiry: '',
|
||||
cvc: '',
|
||||
});
|
||||
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [frontFileName, setFrontFileName] = useState(null);
|
||||
const [backFileName, setBackFileName] = useState(null);
|
||||
|
||||
const plans = [
|
||||
{ id: 'basic', name: 'Basic Search', price: '99', features: ['Browse 500+ workers', 'Shortlist up to 10', 'Standard vetting'] },
|
||||
{ id: 'premium', name: 'Premium Pass', price: '199', features: ['Unlimited shortlisting', 'Direct messaging', 'Priority scheduling'], popular: true },
|
||||
{ id: 'vip', name: 'VIP Concierge', price: '499', features: ['Assigned manager', 'Medical guarantee', 'Free replacements'] },
|
||||
];
|
||||
|
||||
const nextStep = () => setStep(prev => prev + 1);
|
||||
const prevStep = () => setStep(prev => prev - 1);
|
||||
|
||||
const handleFileChange = (e, field) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
setData(field, file);
|
||||
const sizeMb = (file.size / (1024 * 1024)).toFixed(1);
|
||||
if (field === 'emirates_id_front') {
|
||||
setFrontFileName(`${file.name} (${sizeMb} MB)`);
|
||||
} else {
|
||||
setBackFileName(`${file.name} (${sizeMb} MB)`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const removeFile = (field) => {
|
||||
setData(field, null);
|
||||
if (field === 'emirates_id_front') {
|
||||
setFrontFileName(null);
|
||||
} else {
|
||||
setBackFileName(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
post('/employer/register');
|
||||
};
|
||||
|
||||
const renderStepIndicator = () => (
|
||||
<div className="flex items-center justify-between mb-12 relative max-w-md mx-auto px-4">
|
||||
<div className="absolute top-1/2 left-0 w-full h-0.5 bg-slate-100 -translate-y-1/2 -z-10" />
|
||||
<div className="absolute top-1/2 left-0 h-0.5 bg-[#185FA5] -translate-y-1/2 -z-10 transition-all duration-500" style={{ width: `${(step - 1) * 33.33}%` }} />
|
||||
|
||||
{[1, 2, 3, 4].map((s) => (
|
||||
<div key={s} className="flex flex-col items-center">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center font-black text-sm transition-all duration-500 border-4 ${
|
||||
step >= s ? 'bg-[#185FA5] text-white border-blue-100 scale-110 shadow-lg' : 'bg-white text-slate-400 border-slate-50'
|
||||
}`}>
|
||||
{step > s ? <Check className="w-5 h-5" /> : s}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen grid grid-cols-1 lg:grid-cols-12 bg-[#F8FAFC] font-sans selection:bg-blue-100 selection:text-[#185FA5]">
|
||||
<Head title="Employer Registration - Secure Portal" />
|
||||
|
||||
{/* Left Brand Panel */}
|
||||
<div className="hidden lg:flex lg:col-span-4 bg-[#185FA5] p-16 flex-col justify-between text-white relative overflow-hidden select-none sticky top-0 h-screen">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(255,255,255,0.1),transparent)] pointer-events-none" />
|
||||
<div className="absolute bottom-0 left-0 right-0 h-64 bg-gradient-to-t from-black/20 to-transparent pointer-events-none" />
|
||||
|
||||
<div className="relative z-10 space-y-10">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-12 h-12 bg-white text-[#185FA5] rounded-2xl flex items-center justify-center font-black text-2xl shadow-2xl">
|
||||
M
|
||||
</div>
|
||||
<span className="font-black text-2xl tracking-tighter uppercase">Marketplace</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 pt-10">
|
||||
<h1 className="text-5xl font-black tracking-tighter leading-[1.1] drop-shadow-sm">
|
||||
The Secure Hub for Direct Hiring.
|
||||
</h1>
|
||||
<p className="text-blue-100 text-lg font-medium leading-relaxed opacity-90">
|
||||
Join 12,000+ employers hiring domestic workers directly with verified documentation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 pt-12 border-t border-white/10">
|
||||
{[
|
||||
{ title: 'Verified Profiles', desc: 'Every worker is OCR vetted and legally checked.', icon: ShieldCheck },
|
||||
{ title: 'Instant Access', desc: 'Connect with candidates in seconds after verification.', icon: ArrowRight },
|
||||
{ title: 'Zero Fees', desc: 'No recruitment agency commissions, ever.', icon: CheckCircle }
|
||||
].map((feat, i) => (
|
||||
<div key={i} className="flex items-start space-x-4 group">
|
||||
<div className="p-2.5 bg-white/10 rounded-xl group-hover:bg-white/20 transition-colors">
|
||||
<feat.icon className="w-6 h-6 text-emerald-300" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-black text-base uppercase tracking-wider">{feat.title}</h3>
|
||||
<p className="text-sm text-blue-100 font-medium opacity-80">{feat.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 text-[10px] font-black uppercase tracking-[0.3em] text-blue-300 opacity-60">
|
||||
Empowering Household Recruitment Since 2024
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Registration Content */}
|
||||
<div className="col-span-1 lg:col-span-8 flex flex-col items-center py-12 px-6 sm:px-12 overflow-y-auto">
|
||||
<div className="w-full max-w-2xl">
|
||||
<div className="text-center mb-10">
|
||||
<h2 className="text-3xl font-black text-slate-900 tracking-tight uppercase">Employer Enrollment</h2>
|
||||
<p className="text-sm font-bold text-slate-400 mt-2 uppercase tracking-widest">Step {step} of 4: {
|
||||
step === 1 ? 'Account Security' :
|
||||
step === 2 ? 'Identity Verification' :
|
||||
step === 3 ? 'Choose Your Plan' : 'Secure Payment'
|
||||
}</p>
|
||||
</div>
|
||||
|
||||
{renderStepIndicator()}
|
||||
|
||||
<div className="bg-white rounded-[40px] shadow-[0_20px_50px_rgba(0,0,0,0.04)] border border-slate-100 p-8 sm:p-12 transition-all duration-500 animate-in fade-in slide-in-from-bottom-4">
|
||||
<form onSubmit={handleSubmit} className="space-y-8">
|
||||
|
||||
{/* Step 1: Account Creation */}
|
||||
{step === 1 && (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="md:col-span-2 space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Full Legal Name</label>
|
||||
<div className="relative group">
|
||||
<User className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 group-focus-within:text-[#185FA5] transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
value={data.name}
|
||||
onChange={(e) => setData('name', e.target.value)}
|
||||
placeholder="e.g. Abdullah Bin Ahmed"
|
||||
className="w-full pl-12 pr-4 py-4 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-4 focus:ring-blue-100 transition-all outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Work Email</label>
|
||||
<div className="relative group">
|
||||
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 group-focus-within:text-[#185FA5] transition-colors" />
|
||||
<input
|
||||
type="email"
|
||||
value={data.email}
|
||||
onChange={(e) => setData('email', e.target.value)}
|
||||
placeholder="employer@domain.com"
|
||||
className="w-full pl-12 pr-4 py-4 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-4 focus:ring-blue-100 transition-all outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Mobile Access</label>
|
||||
<div className="relative group">
|
||||
<PhoneIcon className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 group-focus-within:text-[#185FA5] transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
value={data.phone}
|
||||
onChange={(e) => setData('phone', e.target.value)}
|
||||
placeholder="+971 XX XXX XXXX"
|
||||
className="w-full pl-12 pr-4 py-4 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-4 focus:ring-blue-100 transition-all outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Secure Password</label>
|
||||
<div className="relative group">
|
||||
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 group-focus-within:text-[#185FA5] transition-colors" />
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={data.password}
|
||||
onChange={(e) => setData('password', e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className="w-full pl-12 pr-12 py-4 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-4 focus:ring-blue-100 transition-all outline-none"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 p-2 text-slate-400 hover:text-slate-600"
|
||||
>
|
||||
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Confirm Access</label>
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={data.password_confirmation}
|
||||
onChange={(e) => setData('password_confirmation', e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className="w-full px-5 py-4 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-4 focus:ring-blue-100 transition-all outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={nextStep}
|
||||
className="w-full bg-[#185FA5] hover:bg-[#144f8a] text-white py-5 rounded-[24px] font-black text-xs uppercase tracking-[0.2em] transition-all shadow-xl shadow-blue-500/20 flex items-center justify-center space-x-2 mt-8"
|
||||
>
|
||||
<span>Verify Identity</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: Emirates ID */}
|
||||
{step === 2 && (
|
||||
<div className="space-y-8">
|
||||
<div className="bg-amber-50 border border-amber-100 p-6 rounded-3xl flex items-start space-x-4">
|
||||
<AlertCircle className="w-6 h-6 text-amber-600 shrink-0" />
|
||||
<p className="text-[11px] font-bold text-amber-800 leading-relaxed uppercase tracking-wider">
|
||||
In compliance with UAE Labor Law, all employers must verify their identity using a valid Emirates ID before accessing the marketplace.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
{[
|
||||
{ field: 'emirates_id_front', label: 'Emirates ID Front', file: frontFileName },
|
||||
{ field: 'emirates_id_back', label: 'Emirates ID Back', file: backFileName }
|
||||
].map((slot) => (
|
||||
<div key={slot.field} className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">{slot.label}</label>
|
||||
{slot.file ? (
|
||||
<div className="relative aspect-[1.6/1] bg-slate-50 rounded-[32px] border-2 border-[#185FA5] flex flex-col items-center justify-center p-6 text-center shadow-lg shadow-blue-500/5">
|
||||
<FileText className="w-10 h-10 text-[#185FA5] mb-2" />
|
||||
<span className="text-[10px] font-black text-slate-900 uppercase truncate max-w-full px-4">{slot.file}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeFile(slot.field)}
|
||||
className="absolute -top-2 -right-2 bg-rose-500 text-white p-2 rounded-full shadow-lg hover:bg-rose-600 transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<label className="relative aspect-[1.6/1] bg-slate-50 rounded-[32px] border-4 border-dashed border-slate-200 flex flex-col items-center justify-center p-8 text-center cursor-pointer hover:border-[#185FA5] hover:bg-blue-50 transition-all group">
|
||||
<div className="w-16 h-16 bg-white rounded-2xl flex items-center justify-center shadow-md mb-4 group-hover:scale-110 transition-transform">
|
||||
<Camera className="w-8 h-8 text-slate-300 group-hover:text-[#185FA5]" />
|
||||
</div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest group-hover:text-[#185FA5]">Tap to upload</span>
|
||||
<input
|
||||
type="file"
|
||||
accept=".jpg,.jpeg,.png,.pdf"
|
||||
className="hidden"
|
||||
onChange={(e) => handleFileChange(e, slot.field)}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 pt-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={prevStep}
|
||||
className="flex-1 bg-slate-100 hover:bg-slate-200 text-slate-600 py-5 rounded-[24px] font-black text-xs uppercase tracking-[0.2em] transition-all flex items-center justify-center space-x-2"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={nextStep}
|
||||
disabled={!frontFileName || !backFileName}
|
||||
className="flex-[2] bg-[#185FA5] hover:bg-[#144f8a] disabled:opacity-50 text-white py-5 rounded-[24px] font-black text-xs uppercase tracking-[0.2em] transition-all shadow-xl shadow-blue-500/20 flex items-center justify-center space-x-2"
|
||||
>
|
||||
<span>Select Plan</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Plan Selection */}
|
||||
{step === 3 && (
|
||||
<div className="space-y-8">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{plans.map((p) => (
|
||||
<button
|
||||
key={p.id}
|
||||
type="button"
|
||||
onClick={() => setData('selected_plan', p.id)}
|
||||
className={`p-6 rounded-[32px] border-4 text-left transition-all relative overflow-hidden flex items-center justify-between group ${
|
||||
data.selected_plan === p.id
|
||||
? 'bg-blue-50 border-[#185FA5] shadow-xl shadow-blue-500/10'
|
||||
: 'bg-white border-slate-50 hover:border-slate-200'
|
||||
}`}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center space-x-3">
|
||||
<h4 className="text-lg font-black text-slate-900 uppercase tracking-tight">{p.name}</h4>
|
||||
{p.popular && <span className="bg-emerald-500 text-white px-2 py-0.5 rounded-full text-[9px] font-black uppercase">Most Popular</span>}
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
{p.features.slice(0, 2).map((f, i) => (
|
||||
<div key={i} className="flex items-center space-x-1.5 text-[10px] font-bold text-slate-500">
|
||||
<CheckCircle className="w-3.5 h-3.5 text-emerald-500" />
|
||||
<span>{f}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-black text-[#185FA5]">{p.price} <span className="text-[10px] text-slate-400 font-bold uppercase tracking-widest">AED</span></div>
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase tracking-widest mt-1">One Time Access</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 pt-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={prevStep}
|
||||
className="flex-1 bg-slate-100 hover:bg-slate-200 text-slate-600 py-5 rounded-[24px] font-black text-xs uppercase tracking-[0.2em] transition-all flex items-center justify-center space-x-2"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={nextStep}
|
||||
className="flex-[2] bg-[#185FA5] hover:bg-[#144f8a] text-white py-5 rounded-[24px] font-black text-xs uppercase tracking-[0.2em] transition-all shadow-xl shadow-blue-500/20 flex items-center justify-center space-x-2"
|
||||
>
|
||||
<span>Pay & Register</span>
|
||||
<CreditCard className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 4: Secure Payment */}
|
||||
{step === 4 && (
|
||||
<div className="space-y-8 animate-in zoom-in-95 duration-500">
|
||||
<div className="bg-slate-900 rounded-[32px] p-8 text-white relative overflow-hidden shadow-2xl">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-blue-500/20 rounded-full blur-3xl -mr-32 -mt-32" />
|
||||
<div className="flex justify-between items-start mb-12 relative z-10">
|
||||
<div className="w-12 h-10 bg-slate-800 rounded-lg border border-slate-700 flex items-center justify-center">
|
||||
<div className="w-8 h-6 bg-gradient-to-br from-amber-200 to-amber-500 rounded" />
|
||||
</div>
|
||||
<CreditCard className="w-10 h-10 text-white/20" />
|
||||
</div>
|
||||
<div className="space-y-4 relative z-10">
|
||||
<div className="text-xl font-medium tracking-[0.2em] h-8 font-mono">
|
||||
{data.card_number || '•••• •••• •••• ••••'}
|
||||
</div>
|
||||
<div className="flex justify-between items-end">
|
||||
<div>
|
||||
<div className="text-[10px] font-black text-white/40 uppercase tracking-widest mb-1">Card Holder</div>
|
||||
<div className="text-sm font-bold uppercase tracking-widest">{data.name || 'Your Name'}</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-[10px] font-black text-white/40 uppercase tracking-widest mb-1">Expires</div>
|
||||
<div className="text-sm font-bold uppercase tracking-widest">{data.expiry || 'MM/YY'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Card Number</label>
|
||||
<div className="relative group">
|
||||
<CreditCard className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 group-focus-within:text-[#185FA5] transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
maxLength="19"
|
||||
value={data.card_number}
|
||||
onChange={(e) => setData('card_number', e.target.value.replace(/\W/gi, '').replace(/(.{4})/g, '$1 ').trim())}
|
||||
placeholder="4242 4242 4242 4242"
|
||||
className="w-full pl-12 pr-4 py-4 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-4 focus:ring-blue-100 transition-all outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Expiry Date</label>
|
||||
<input
|
||||
type="text"
|
||||
maxLength="5"
|
||||
value={data.expiry}
|
||||
onChange={(e) => setData('expiry', e.target.value.replace(/^(\d{2})/, '$1/'))}
|
||||
placeholder="MM/YY"
|
||||
className="w-full px-5 py-4 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-4 focus:ring-blue-100 transition-all outline-none text-center"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Security Code</label>
|
||||
<input
|
||||
type="password"
|
||||
maxLength="4"
|
||||
value={data.cvc}
|
||||
onChange={(e) => setData('cvc', e.target.value)}
|
||||
placeholder="CVC"
|
||||
className="w-full px-5 py-4 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-4 focus:ring-blue-100 transition-all outline-none text-center"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-blue-50 rounded-3xl border border-blue-100 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-[10px] font-black text-blue-600 uppercase tracking-widest">Total Amount</div>
|
||||
<div className="text-xl font-black text-slate-900">{plans.find(p => p.id === data.selected_plan)?.price} AED</div>
|
||||
</div>
|
||||
<ShieldCheck className="w-8 h-8 text-[#185FA5] opacity-20" />
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={prevStep}
|
||||
className="flex-1 bg-slate-100 hover:bg-slate-200 text-slate-600 py-5 rounded-[24px] font-black text-xs uppercase tracking-[0.2em] transition-all flex items-center justify-center space-x-2"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back</span>
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={processing}
|
||||
className="flex-[2] bg-emerald-600 hover:bg-emerald-700 active:bg-emerald-800 text-white py-5 rounded-[24px] font-black text-xs uppercase tracking-[0.2em] transition-all shadow-xl shadow-emerald-500/20 flex items-center justify-center space-x-2"
|
||||
>
|
||||
{processing ? <Loader2 className="w-5 h-5 animate-spin mr-2" /> : (
|
||||
<>
|
||||
<span>Complete & Pay</span>
|
||||
<Lock className="w-4 h-4" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-400 text-center font-bold uppercase tracking-widest">
|
||||
Secured by 256-bit AES encryption
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 text-center">
|
||||
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest">
|
||||
Already part of the network?{' '}
|
||||
<Link href="/employer/login" className="text-[#185FA5] hover:underline">
|
||||
Authorize Account
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
233
resources/js/Pages/Employer/Dashboard.jsx
Normal file
233
resources/js/Pages/Employer/Dashboard.jsx
Normal file
@ -0,0 +1,233 @@
|
||||
import React from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import EmployerLayout from '../../Layouts/EmployerLayout';
|
||||
import {
|
||||
Bookmark,
|
||||
MessageSquare,
|
||||
CalendarDays,
|
||||
CreditCard,
|
||||
Search,
|
||||
ArrowRight,
|
||||
CheckCircle2,
|
||||
Info,
|
||||
Clock,
|
||||
ChevronRight,
|
||||
UserCircle2
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function Dashboard({ employer, stats, shortlisted_workers, recent_messages, announcements }) {
|
||||
return (
|
||||
<EmployerLayout title="Employer Dashboard">
|
||||
<Head title="Employer Dashboard - Marketplace Portal" />
|
||||
|
||||
<div className="space-y-8">
|
||||
{/* Section 1: Welcome Banner */}
|
||||
<div className="bg-gradient-to-r from-[#185FA5] to-blue-700 rounded-2xl p-6 sm:p-8 text-white shadow-sm flex flex-col sm:flex-row sm:items-center sm:justify-between gap-6 relative overflow-hidden select-none">
|
||||
<div className="absolute -right-10 -bottom-10 w-48 h-48 bg-white/10 rounded-full blur-2xl pointer-events-none" />
|
||||
|
||||
<div className="space-y-3 z-10">
|
||||
<div className="inline-flex items-center space-x-2 px-3 py-1 bg-white/10 rounded-full text-xs font-semibold backdrop-blur-sm border border-white/10">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse" />
|
||||
<span>Subscription Active</span>
|
||||
<span className="text-blue-200">|</span>
|
||||
<span>{stats.days_remaining} days left</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-3xl sm:text-4xl font-extrabold tracking-tight">
|
||||
Good morning, {employer.name}
|
||||
</h1>
|
||||
|
||||
<p className="text-blue-100 text-sm max-w-xl font-medium">
|
||||
Welcome to your employer control center. You have {stats.days_remaining} days remaining on your {employer.plan_name}.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="z-10 flex-shrink-0">
|
||||
<Link
|
||||
href="/employer/workers"
|
||||
className="bg-white text-[#185FA5] hover:bg-slate-100 px-6 py-3 rounded-xl font-bold text-sm shadow-md transition-all flex items-center justify-center space-x-2 group w-full sm:w-auto"
|
||||
>
|
||||
<Search className="w-4 h-4 text-[#185FA5] group-hover:scale-110 transition-transform" />
|
||||
<span>Find Workers</span>
|
||||
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 2: Stat Cards */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* Shortlisted */}
|
||||
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex items-center space-x-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-blue-50 text-blue-600 flex items-center justify-center flex-shrink-0">
|
||||
<Bookmark className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-slate-800">{stats.shortlisted_count}</div>
|
||||
<div className="text-xs text-slate-500 font-medium">Workers Shortlisted</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages Sent */}
|
||||
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex items-center space-x-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-teal-50 text-teal-600 flex items-center justify-center flex-shrink-0">
|
||||
<MessageSquare className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-slate-800">{stats.messages_sent}</div>
|
||||
<div className="text-xs text-slate-500 font-medium">Messages Sent</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Days Remaining */}
|
||||
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex items-center space-x-4">
|
||||
<div className={`w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0 ${
|
||||
stats.days_remaining < 7 ? 'bg-red-50 text-red-600' : 'bg-amber-50 text-amber-600'
|
||||
}`}>
|
||||
<CalendarDays className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<div className={`text-2xl font-bold ${stats.days_remaining < 7 ? 'text-red-600' : 'text-slate-800'}`}>
|
||||
{stats.days_remaining}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 font-medium">Days Remaining</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Plan Name */}
|
||||
<div className="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex items-center space-x-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-slate-100 text-slate-600 flex items-center justify-center flex-shrink-0">
|
||||
<CreditCard className="w-6 h-6" />
|
||||
</div>
|
||||
<div className="truncate">
|
||||
<div className="text-lg font-bold text-slate-800 truncate">{employer.plan_name}</div>
|
||||
<div className="text-xs text-slate-500 font-medium">Current Plan</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||
{/* Main Content Column (Col 8) */}
|
||||
<div className="lg:col-span-8 space-y-8">
|
||||
{/* Section 3: My Shortlist */}
|
||||
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Bookmark className="w-5 h-5 text-[#185FA5]" />
|
||||
<h2 className="text-lg font-bold text-slate-800">My Shortlist</h2>
|
||||
</div>
|
||||
<Link href="/employer/shortlist" className="text-xs font-bold text-[#185FA5] hover:underline flex items-center space-x-1">
|
||||
<span>View all</span>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{shortlisted_workers?.length > 0 ? (
|
||||
<div className="flex space-x-4 overflow-x-auto pb-2 scrollbar-thin">
|
||||
{shortlisted_workers.map((worker) => (
|
||||
<div key={worker.id} className="w-56 bg-slate-50 rounded-xl p-4 border border-slate-200 flex-shrink-0 space-y-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-12 h-12 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center font-bold text-lg flex-shrink-0 border border-blue-200">
|
||||
{worker.name.charAt(0)}
|
||||
</div>
|
||||
<div className="truncate">
|
||||
<div className="font-bold text-sm text-slate-800 truncate flex items-center space-x-1">
|
||||
<span>{worker.name}</span>
|
||||
{worker.verified && <CheckCircle2 className="w-3.5 h-3.5 text-emerald-600 flex-shrink-0" />}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 truncate">{worker.nationality}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="text-[11px] font-semibold text-slate-500">Top Skill:</div>
|
||||
<div className="inline-block px-2.5 py-1 bg-white border border-slate-200 rounded-lg text-xs font-medium text-slate-700">
|
||||
{worker.skills[0] || 'General Work'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-slate-200 flex items-center justify-between text-xs">
|
||||
<span className="text-slate-500 font-medium">Availability:</span>
|
||||
<span className="font-bold text-[#185FA5] bg-blue-50 px-2 py-0.5 rounded border border-blue-100">
|
||||
{worker.availability}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 bg-slate-50 rounded-xl border-2 border-dashed border-slate-200 space-y-2">
|
||||
<Bookmark className="w-8 h-8 text-slate-300 mx-auto" />
|
||||
<div className="text-sm font-semibold text-slate-600">Shortlist workers to see them here</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Section 4: Recent Messages */}
|
||||
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<MessageSquare className="w-5 h-5 text-[#185FA5]" />
|
||||
<h2 className="text-lg font-bold text-slate-800">Recent Messages</h2>
|
||||
</div>
|
||||
<Link href="/employer/messages" className="text-xs font-bold text-[#185FA5] hover:underline flex items-center space-x-1">
|
||||
<span>View all messages</span>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-slate-100">
|
||||
{recent_messages?.map((msg) => (
|
||||
<Link
|
||||
key={msg.id}
|
||||
href={`/employer/messages/${msg.id}`}
|
||||
className="flex items-start space-x-4 py-4 hover:bg-slate-50 px-3 rounded-xl transition-colors group"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-full bg-blue-50 text-[#185FA5] flex items-center justify-center font-bold text-sm flex-shrink-0 border border-blue-100">
|
||||
{msg.worker_name.charAt(0)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="font-bold text-sm text-slate-800 group-hover:text-[#185FA5] transition-colors truncate">
|
||||
{msg.worker_name}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-[10px] font-medium text-slate-400">{msg.sent_at}</span>
|
||||
{msg.unread && <span className="w-2 h-2 rounded-full bg-red-500 animate-pulse" />}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-slate-600 truncate mt-0.5">{msg.last_message}</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column (Col 4) */}
|
||||
<div className="lg:col-span-4 space-y-8">
|
||||
{/* Section 5: Announcements */}
|
||||
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6 space-y-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Info className="w-5 h-5 text-[#185FA5]" />
|
||||
<h2 className="text-lg font-bold text-slate-800">Announcements</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{announcements?.map((ann) => (
|
||||
<div key={ann.id} className="p-4 bg-blue-50/70 border border-blue-100 rounded-xl space-y-2">
|
||||
<div className="flex items-center justify-between text-[10px] font-bold text-blue-600 uppercase tracking-wider">
|
||||
<span>System Alert</span>
|
||||
<span>{ann.created_at}</span>
|
||||
</div>
|
||||
<h3 className="font-bold text-xs text-slate-900 tracking-tight">{ann.title}</h3>
|
||||
<p className="text-xs text-slate-600 leading-relaxed">{ann.body}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</EmployerLayout>
|
||||
);
|
||||
}
|
||||
154
resources/js/Pages/Employer/Hiring/Confirm.jsx
Normal file
154
resources/js/Pages/Employer/Hiring/Confirm.jsx
Normal file
@ -0,0 +1,154 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Head, Link, router } from '@inertiajs/react';
|
||||
import EmployerLayout from '@/Layouts/EmployerLayout';
|
||||
import {
|
||||
ArrowLeft,
|
||||
Calendar,
|
||||
MapPin,
|
||||
DollarSign,
|
||||
Send,
|
||||
CheckCircle2,
|
||||
ShieldCheck,
|
||||
Clock
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function Confirm({ worker }) {
|
||||
const [startDate, setStartDate] = useState('');
|
||||
const [location, setLocation] = useState('');
|
||||
const [offeredSalary, setOfferedSalary] = useState(worker.salary);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
// Mock submission
|
||||
router.get(`/employer/workers/${worker.id}/hire/success`);
|
||||
};
|
||||
|
||||
return (
|
||||
<EmployerLayout title="Confirm Hire">
|
||||
<Head title={`Hire ${worker.name}`} />
|
||||
|
||||
<div className="max-w-3xl mx-auto space-y-6 pb-12">
|
||||
<Link
|
||||
href={`/employer/workers/${worker.id}`}
|
||||
className="inline-flex items-center space-x-2 text-xs font-bold text-slate-600 hover:text-[#185FA5] transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back to Profile</span>
|
||||
</Link>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Worker Summary Card */}
|
||||
<div className="lg:col-span-1 space-y-6">
|
||||
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
|
||||
<div className="bg-slate-50 p-6 border-b border-slate-100 flex flex-col items-center text-center">
|
||||
<div className="w-20 h-20 rounded-full bg-blue-100 text-[#185FA5] flex items-center justify-center font-bold text-3xl border-4 border-white shadow-sm mb-3">
|
||||
{worker.name.charAt(0)}
|
||||
</div>
|
||||
<h3 className="font-extrabold text-slate-900 tracking-tight">{worker.name}</h3>
|
||||
<p className="text-xs font-bold text-[#185FA5] uppercase tracking-widest mt-1">{worker.category}</p>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="flex items-center justify-between text-xs font-medium">
|
||||
<span className="text-slate-500 uppercase tracking-tighter">Expected Salary</span>
|
||||
<span className="text-slate-900 font-bold">{worker.salary} AED</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs font-medium">
|
||||
<span className="text-slate-500 uppercase tracking-tighter">Nationality</span>
|
||||
<span className="text-slate-900 font-bold">{worker.nationality}</span>
|
||||
</div>
|
||||
<div className="pt-4 border-t border-slate-50">
|
||||
<div className="flex items-center space-x-2 text-[10px] text-emerald-600 font-bold uppercase">
|
||||
<ShieldCheck className="w-3.5 h-3.5" />
|
||||
<span>Documents Verified</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Offer Form */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden h-full flex flex-col">
|
||||
<div className="p-6 border-b border-slate-100 bg-white">
|
||||
<h2 className="text-lg font-black text-slate-900 tracking-tight">Step 1: Customize Your Offer</h2>
|
||||
<p className="text-xs text-slate-500 font-medium mt-1 uppercase tracking-widest">Enter the employment details below</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-8 space-y-6 flex-1">
|
||||
<div className="space-y-4">
|
||||
{/* Start Date */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest flex items-center space-x-1.5 ml-1">
|
||||
<Calendar className="w-3.5 h-3.5" />
|
||||
<span>Requested Start Date</span>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
required
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
className="w-full px-4 py-3 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Location */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest flex items-center space-x-1.5 ml-1">
|
||||
<MapPin className="w-3.5 h-3.5" />
|
||||
<span>Job Location</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="e.g. Dubai Marina, Villa 12"
|
||||
value={location}
|
||||
onChange={(e) => setLocation(e.target.value)}
|
||||
className="w-full px-4 py-3 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Salary */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest flex items-center space-x-1.5 ml-1">
|
||||
<DollarSign className="w-3.5 h-3.5" />
|
||||
<span>Offered Salary (AED / month)</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="number"
|
||||
required
|
||||
value={offeredSalary}
|
||||
onChange={(e) => setOfferedSalary(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-3 bg-slate-50 border border-slate-100 rounded-xl text-sm font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all"
|
||||
/>
|
||||
<div className="absolute left-4 top-1/2 -translate-y-1/2 font-bold text-slate-400">
|
||||
$
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-6 border-t border-slate-50 space-y-4">
|
||||
<div className="bg-blue-50/50 p-4 rounded-xl border border-blue-100 flex items-start space-x-3">
|
||||
<Clock className="w-5 h-5 text-[#185FA5] flex-shrink-0 mt-0.5" />
|
||||
<p className="text-[11px] text-[#185FA5] font-bold leading-relaxed uppercase tracking-tight">
|
||||
Once you send this offer, the worker will be notified immediately. You can track the status in your Candidates page.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-[#185FA5] text-white py-4 rounded-xl font-black text-sm uppercase tracking-widest shadow-xl shadow-blue-500/20 hover:bg-[#144f8a] active:scale-[0.98] transition-all flex items-center justify-center space-x-3"
|
||||
>
|
||||
<Send className="w-5 h-5" />
|
||||
<span>Send Offer to {worker.name}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</EmployerLayout>
|
||||
);
|
||||
}
|
||||
69
resources/js/Pages/Employer/Hiring/Success.jsx
Normal file
69
resources/js/Pages/Employer/Hiring/Success.jsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import EmployerLayout from '@/Layouts/EmployerLayout';
|
||||
import {
|
||||
CheckCircle2,
|
||||
ArrowRight,
|
||||
Users,
|
||||
MessageSquare,
|
||||
Sparkles
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function Success({ worker }) {
|
||||
return (
|
||||
<EmployerLayout title="Offer Sent">
|
||||
<Head title="Hiring Offer Sent" />
|
||||
|
||||
<div className="max-w-xl mx-auto py-12 px-4 select-none">
|
||||
<div className="bg-white rounded-3xl border border-slate-200 shadow-xl overflow-hidden text-center p-12 space-y-8 relative">
|
||||
{/* Decorative elements */}
|
||||
<div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-emerald-400 to-[#185FA5]" />
|
||||
<Sparkles className="absolute top-8 left-8 w-6 h-6 text-emerald-200" />
|
||||
<Sparkles className="absolute bottom-8 right-8 w-8 h-8 text-blue-100" />
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="w-24 h-24 bg-emerald-50 rounded-full flex items-center justify-center mx-auto border-4 border-white shadow-lg animate-bounce">
|
||||
<CheckCircle2 className="w-12 h-12 text-emerald-500" />
|
||||
</div>
|
||||
<h1 className="text-3xl font-black text-slate-900 tracking-tight">Offer Sent!</h1>
|
||||
<p className="text-slate-500 font-bold text-sm max-w-xs mx-auto leading-relaxed uppercase tracking-widest">
|
||||
Your hiring offer has been successfully sent to <span className="text-[#185FA5]">{worker.name}</span>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-50 rounded-2xl p-6 border border-slate-100 space-y-3">
|
||||
<h3 className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]">What's next?</h3>
|
||||
<ul className="text-left space-y-4">
|
||||
<li className="flex items-start space-x-3 text-xs font-bold text-slate-700">
|
||||
<div className="w-5 h-5 bg-white rounded-lg flex items-center justify-center flex-shrink-0 shadow-sm border border-slate-100 text-[#185FA5]">1</div>
|
||||
<span>The worker will receive a notification and SMS with your offer details.</span>
|
||||
</li>
|
||||
<li className="flex items-start space-x-3 text-xs font-bold text-slate-700">
|
||||
<div className="w-5 h-5 bg-white rounded-lg flex items-center justify-center flex-shrink-0 shadow-sm border border-slate-100 text-[#185FA5]">2</div>
|
||||
<span>You can track their response in your "Candidates" dashboard.</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-3 pt-4">
|
||||
<Link
|
||||
href="/employer/candidates"
|
||||
className="w-full bg-[#185FA5] text-white py-4 rounded-xl font-black text-xs uppercase tracking-widest shadow-xl shadow-blue-500/20 hover:bg-[#144f8a] transition-all flex items-center justify-center space-x-2 group"
|
||||
>
|
||||
<span>Go to Candidates</span>
|
||||
<Users className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/employer/workers"
|
||||
className="w-full bg-white text-slate-600 border border-slate-200 py-4 rounded-xl font-black text-xs uppercase tracking-widest hover:bg-slate-50 transition-all flex items-center justify-center space-x-2"
|
||||
>
|
||||
<span>Find More Workers</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</EmployerLayout>
|
||||
);
|
||||
}
|
||||
170
resources/js/Pages/Employer/Jobs/Applicants.jsx
Normal file
170
resources/js/Pages/Employer/Jobs/Applicants.jsx
Normal file
@ -0,0 +1,170 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import EmployerLayout from '@/Layouts/EmployerLayout';
|
||||
import {
|
||||
ArrowLeft,
|
||||
MessageSquare,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Globe2,
|
||||
Briefcase,
|
||||
DollarSign,
|
||||
MoreHorizontal,
|
||||
Search,
|
||||
Filter,
|
||||
ShieldCheck,
|
||||
Star
|
||||
} from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
export default function Applicants({ job, applicants: initialApplicants }) {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [applicants, setApplicants] = useState(initialApplicants || [
|
||||
{ id: 101, name: 'Anjali Sharma', nationality: 'Indian', category: 'Nanny', salary: 2500, experience: '5 Years', status: 'Reviewing', match_score: 95 },
|
||||
{ id: 102, name: 'Siti Rahma', nationality: 'Indonesian', category: 'Housemaid', salary: 1800, experience: '3 Years', status: 'Hired', match_score: 88 },
|
||||
{ id: 103, name: 'Maricel Cruz', nationality: 'Filipino', category: 'Domestic Helper', salary: 2200, experience: '7 Years', status: 'Rejected', match_score: 72 },
|
||||
{ id: 104, name: 'Bebeth Santos', nationality: 'Filipino', category: 'Cook', salary: 3000, experience: '10 Years', status: 'Reviewing', match_score: 98 },
|
||||
]);
|
||||
|
||||
const filteredApplicants = applicants.filter(a =>
|
||||
a.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
a.nationality.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<EmployerLayout title="Job Applicants">
|
||||
<Head title={`Applicants for ${job?.title || 'Job'}`} />
|
||||
|
||||
<div className="space-y-8 select-none pb-12">
|
||||
{/* Job Header Info */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||
<div className="space-y-2">
|
||||
<Link
|
||||
href="/employer/jobs"
|
||||
className="inline-flex items-center space-x-2 text-[10px] font-black text-[#185FA5] uppercase tracking-widest hover:translate-x-[-4px] transition-transform"
|
||||
>
|
||||
<ArrowLeft className="w-3.5 h-3.5" />
|
||||
<span>Back to My Jobs</span>
|
||||
</Link>
|
||||
<h1 className="text-2xl font-black text-slate-900 tracking-tight">
|
||||
Applicants for <span className="text-[#185FA5]">{job?.title || 'Senior Mason Project'}</span>
|
||||
</h1>
|
||||
<div className="flex items-center space-x-3 text-[10px] font-bold text-slate-400 uppercase tracking-widest">
|
||||
<span className="flex items-center"><Briefcase className="w-3 h-3 mr-1" /> {job?.category || 'Mason'}</span>
|
||||
<span className="w-1 h-1 bg-slate-200 rounded-full" />
|
||||
<span className="flex items-center"><DollarSign className="w-3 h-3 mr-1" /> {job?.salary || '2800'} AED</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white px-6 py-4 rounded-2xl border border-slate-200 shadow-sm flex items-center space-x-4 flex-shrink-0">
|
||||
<div className="text-right">
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Hiring Progress</div>
|
||||
<div className="text-lg font-black text-slate-900 tracking-tight">{applicants.filter(a => a.status === 'Hired').length} / 5 <span className="text-slate-300 text-sm">Positions</span></div>
|
||||
</div>
|
||||
<div className="w-12 h-12 rounded-xl bg-blue-50 flex items-center justify-center">
|
||||
<Star className="w-6 h-6 text-[#185FA5]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="bg-white p-4 rounded-2xl border border-slate-200 shadow-sm flex flex-col sm:flex-row gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Find applicant by name or nationality..."
|
||||
className="w-full pl-11 pr-4 py-2.5 bg-slate-50 border border-slate-100 rounded-xl text-xs font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all"
|
||||
value={searchTerm}
|
||||
onChange={e => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button className="flex items-center justify-center space-x-2 px-6 py-2.5 bg-white border border-slate-200 rounded-xl text-[10px] font-black text-slate-600 hover:bg-slate-50 transition-colors uppercase tracking-widest">
|
||||
<Filter className="w-3.5 h-3.5" />
|
||||
<span>Filter Matches</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Applicants List */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredApplicants.length > 0 ? (
|
||||
filteredApplicants.map((applicant) => (
|
||||
<div key={applicant.id} className="bg-white rounded-[24px] border border-slate-200 shadow-sm overflow-hidden flex flex-col group hover:shadow-md transition-all">
|
||||
<div className="p-6 space-y-5">
|
||||
{/* Applicant Header */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-12 h-12 rounded-full bg-blue-50 text-[#185FA5] flex items-center justify-center font-bold text-lg border-2 border-white shadow-sm ring-1 ring-slate-100">
|
||||
{applicant.name.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-slate-900 group-hover:text-[#185FA5] transition-colors line-clamp-1">{applicant.name}</h3>
|
||||
<div className="flex items-center space-x-2 text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-0.5">
|
||||
<Globe2 className="w-3 h-3" />
|
||||
<span>{applicant.nationality}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-end space-y-1">
|
||||
<div className="flex items-center space-x-1 text-emerald-600 font-black text-[10px] bg-emerald-50 px-2 py-0.5 rounded-full">
|
||||
<ShieldCheck className="w-3 h-3" />
|
||||
<span>{applicant.match_score}% Match</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-2 gap-3 p-4 bg-slate-50/50 rounded-2xl border border-slate-50">
|
||||
<div className="space-y-1">
|
||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Experience</div>
|
||||
<div className="text-[11px] font-bold text-slate-700">{applicant.experience}</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Expected Pay</div>
|
||||
<div className="text-[11px] font-bold text-slate-700">{applicant.salary} AED</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current Status Badge */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={`px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest ${
|
||||
applicant.status === 'Hired' ? 'bg-emerald-100 text-emerald-700' :
|
||||
applicant.status === 'Rejected' ? 'bg-rose-100 text-rose-700' :
|
||||
'bg-amber-100 text-amber-700'
|
||||
}`}>
|
||||
{applicant.status}
|
||||
</span>
|
||||
<div className="flex items-center space-x-1 text-[10px] font-bold text-slate-400">
|
||||
<Link href={`/employer/workers/${applicant.id}`} className="hover:text-[#185FA5] flex items-center underline underline-offset-2">
|
||||
View Full Profile
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions Bar */}
|
||||
<div className="p-4 bg-slate-50 border-t border-slate-100 flex items-center space-x-2">
|
||||
<button className="flex-1 bg-white border border-slate-200 text-[#185FA5] hover:bg-blue-50 py-2.5 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all flex items-center justify-center space-x-2">
|
||||
<MessageSquare className="w-3.5 h-3.5" />
|
||||
<span>Chat</span>
|
||||
</button>
|
||||
<button className="p-2.5 bg-[#185FA5] text-white hover:bg-[#144f8a] rounded-xl transition-all shadow-sm shadow-blue-500/10">
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
</button>
|
||||
<button className="p-2.5 bg-white border border-slate-200 text-rose-500 hover:bg-rose-50 rounded-xl transition-all">
|
||||
<XCircle className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="col-span-full py-20 text-center">
|
||||
<Users className="w-12 h-12 text-slate-200 mx-auto mb-3" />
|
||||
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest">No applicants found for this job yet.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</EmployerLayout>
|
||||
);
|
||||
}
|
||||
261
resources/js/Pages/Employer/Jobs/Create.jsx
Normal file
261
resources/js/Pages/Employer/Jobs/Create.jsx
Normal file
@ -0,0 +1,261 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Head, Link, useForm } from '@inertiajs/react';
|
||||
import EmployerLayout from '@/Layouts/EmployerLayout';
|
||||
import {
|
||||
Plus,
|
||||
ArrowLeft,
|
||||
Briefcase,
|
||||
MapPin,
|
||||
DollarSign,
|
||||
Calendar,
|
||||
LayoutGrid,
|
||||
Users,
|
||||
FileText,
|
||||
Sparkles,
|
||||
ShieldCheck
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function Create({ categories: initialCategories }) {
|
||||
const categories = initialCategories || [
|
||||
'Electrician', 'Mason', 'Plumber', 'Cleaner', 'Site Supervisor', 'Driver', 'General Helper'
|
||||
];
|
||||
|
||||
const { data, setData, post, processing, errors } = useForm({
|
||||
title: '',
|
||||
category: '',
|
||||
workers_needed: 1,
|
||||
location: '',
|
||||
salary: '',
|
||||
job_type: 'Full Time',
|
||||
start_date: '',
|
||||
description: '',
|
||||
requirements: ''
|
||||
});
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
// In a real app, we'd post to /employer/jobs
|
||||
// post('/employer/jobs');
|
||||
alert('Job posted successfully! Redirecting to Job List...');
|
||||
window.location.href = '/employer/jobs';
|
||||
};
|
||||
|
||||
return (
|
||||
<EmployerLayout title="Post a Job">
|
||||
<Head title="Post a New Job - Employer Portal" />
|
||||
|
||||
<div className="max-w-4xl mx-auto space-y-6 pb-12 select-none">
|
||||
<Link
|
||||
href="/employer/jobs"
|
||||
className="inline-flex items-center space-x-2 text-xs font-bold text-slate-600 hover:text-[#185FA5] transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back to My Jobs</span>
|
||||
</Link>
|
||||
|
||||
<div className="bg-white rounded-[32px] border border-slate-200 shadow-sm overflow-hidden">
|
||||
<div className="p-8 border-b border-slate-50 bg-slate-50/30 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="p-3 bg-blue-50 rounded-2xl">
|
||||
<Plus className="w-6 h-6 text-[#185FA5]" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-black text-slate-900 tracking-tight uppercase">Post a New Job</h1>
|
||||
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest mt-1">Fill in the details to find the perfect workers</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden sm:flex items-center space-x-2 text-emerald-600 bg-emerald-50 px-4 py-2 rounded-xl border border-emerald-100">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
<span className="text-[10px] font-black uppercase">Premium Listing Active</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-8 space-y-8">
|
||||
{/* Section 1: Basic Info */}
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-xs font-black text-[#185FA5] uppercase tracking-[0.2em] flex items-center">
|
||||
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full mr-2" />
|
||||
Basic Information
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Job Title</label>
|
||||
<div className="relative">
|
||||
<Briefcase className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. Senior Electrician for Villa Project"
|
||||
className="w-full pl-11 pr-4 py-3 bg-slate-50 border border-slate-100 rounded-2xl text-sm font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all"
|
||||
value={data.title}
|
||||
onChange={e => setData('title', e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Worker Category</label>
|
||||
<div className="relative">
|
||||
<LayoutGrid className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<select
|
||||
className="w-full pl-11 pr-4 py-3 bg-slate-50 border border-slate-100 rounded-2xl text-sm font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all appearance-none"
|
||||
value={data.category}
|
||||
onChange={e => setData('category', e.target.value)}
|
||||
required
|
||||
>
|
||||
<option value="">Select Category</option>
|
||||
{categories.map(cat => <option key={cat} value={cat}>{cat}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Workers Needed</label>
|
||||
<div className="relative">
|
||||
<Users className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
className="w-full pl-11 pr-4 py-3 bg-slate-50 border border-slate-100 rounded-2xl text-sm font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all"
|
||||
value={data.workers_needed}
|
||||
onChange={e => setData('workers_needed', e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Job Type</label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{['Full Time', 'Part Time', 'Contract'].map(type => (
|
||||
<button
|
||||
key={type}
|
||||
type="button"
|
||||
onClick={() => setData('job_type', type)}
|
||||
className={`py-3 text-[10px] font-black rounded-xl border transition-all ${
|
||||
data.job_type === type
|
||||
? 'bg-[#185FA5] text-white border-[#185FA5] shadow-md shadow-blue-500/20'
|
||||
: 'bg-white text-slate-400 border-slate-100 hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
{type.toUpperCase()}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 2: Logistics & Pay */}
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-xs font-black text-[#185FA5] uppercase tracking-[0.2em] flex items-center">
|
||||
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full mr-2" />
|
||||
Logistics & Compensation
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Job Location</label>
|
||||
<div className="relative">
|
||||
<MapPin className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. Dubai Marina"
|
||||
className="w-full pl-11 pr-4 py-3 bg-slate-50 border border-slate-100 rounded-2xl text-sm font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all"
|
||||
value={data.location}
|
||||
onChange={e => setData('location', e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Salary (AED / mo)</label>
|
||||
<div className="relative">
|
||||
<DollarSign className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="number"
|
||||
placeholder="e.g. 2500"
|
||||
className="w-full pl-11 pr-4 py-3 bg-slate-50 border border-slate-100 rounded-2xl text-sm font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all"
|
||||
value={data.salary}
|
||||
onChange={e => setData('salary', e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Start Date</label>
|
||||
<div className="relative">
|
||||
<Calendar className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="date"
|
||||
className="w-full pl-11 pr-4 py-3 bg-slate-50 border border-slate-100 rounded-2xl text-sm font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all"
|
||||
value={data.start_date}
|
||||
onChange={e => setData('start_date', e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 3: Details */}
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-xs font-black text-[#185FA5] uppercase tracking-[0.2em] flex items-center">
|
||||
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full mr-2" />
|
||||
Detailed Requirements
|
||||
</h3>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Job Description</label>
|
||||
<textarea
|
||||
placeholder="Briefly describe the responsibilities..."
|
||||
maxLength={300}
|
||||
className="w-full px-5 py-4 bg-slate-50 border border-slate-100 rounded-2xl text-sm font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all min-h-[100px] resize-none"
|
||||
value={data.description}
|
||||
onChange={e => setData('description', e.target.value)}
|
||||
/>
|
||||
<div className="text-right text-[10px] font-bold text-slate-400 uppercase tracking-widest pr-2">
|
||||
{data.description.length} / 300 Characters
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Special Requirements (Optional)</label>
|
||||
<div className="relative">
|
||||
<FileText className="absolute left-4 top-5 w-4 h-4 text-slate-400" />
|
||||
<textarea
|
||||
placeholder="e.g. Must speak Arabic, 5+ years experience, transferable visa..."
|
||||
className="w-full pl-11 pr-4 py-4 bg-slate-50 border border-slate-100 rounded-2xl text-sm font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all min-h-[100px] resize-none"
|
||||
value={data.requirements}
|
||||
onChange={e => setData('requirements', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="pt-6 border-t border-slate-100">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={processing}
|
||||
className="w-full bg-[#185FA5] text-white py-5 rounded-2xl font-black text-sm uppercase tracking-widest shadow-xl shadow-blue-500/20 hover:bg-[#144f8a] active:scale-[0.98] transition-all flex items-center justify-center space-x-3"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
<span>Post Job Now</span>
|
||||
</button>
|
||||
<p className="text-center text-[10px] text-slate-400 font-bold uppercase tracking-widest mt-4 flex items-center justify-center space-x-2">
|
||||
<ShieldCheck className="w-3.5 h-3.5 text-emerald-500" />
|
||||
<span>All job posts are reviewed for compliance with UAE Labor Laws</span>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</EmployerLayout>
|
||||
);
|
||||
}
|
||||
233
resources/js/Pages/Employer/Jobs/Index.jsx
Normal file
233
resources/js/Pages/Employer/Jobs/Index.jsx
Normal file
@ -0,0 +1,233 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import EmployerLayout from '@/Layouts/EmployerLayout';
|
||||
import {
|
||||
Plus,
|
||||
Search,
|
||||
Filter,
|
||||
Download,
|
||||
MoreHorizontal,
|
||||
MapPin,
|
||||
DollarSign,
|
||||
Users,
|
||||
Calendar,
|
||||
Briefcase,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Clock,
|
||||
Eye,
|
||||
Edit2
|
||||
} from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
export default function Index({ initialJobs }) {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState('All');
|
||||
|
||||
const [jobs, setJobs] = useState(initialJobs || [
|
||||
{ id: 1, title: 'Experienced Mason', category: 'Mason', location: 'Dubai Marina', salary: 2800, workers_needed: 5, applied_count: 3, posted_at: 'Nov 12, 2026', status: 'Active' },
|
||||
{ id: 2, title: 'Villa Cleaner', category: 'Cleaner', location: 'Al Barsha', salary: 1800, workers_needed: 2, applied_count: 12, posted_at: 'Nov 10, 2026', status: 'Active' },
|
||||
{ id: 3, title: 'Site Supervisor', category: 'Site Supervisor', location: 'Sharjah', salary: 4500, workers_needed: 1, applied_count: 8, posted_at: 'Nov 05, 2026', status: 'Closed' },
|
||||
{ id: 4, title: 'Plumber for Project', category: 'Plumber', location: 'Jumeirah', salary: 2600, workers_needed: 3, applied_count: 0, posted_at: 'Nov 14, 2026', status: 'Draft' },
|
||||
]);
|
||||
|
||||
const filteredJobs = useMemo(() => {
|
||||
return jobs.filter(job => {
|
||||
const matchesSearch = job.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
job.category.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesStatus = statusFilter === 'All' || job.status === statusFilter;
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
}, [jobs, searchTerm, statusFilter]);
|
||||
|
||||
return (
|
||||
<EmployerLayout title="My Jobs">
|
||||
<Head title="My Jobs - Employer Portal" />
|
||||
|
||||
<div className="space-y-8 select-none">
|
||||
{/* Header Actions */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-black text-slate-900 tracking-tight">Active Job Postings</h1>
|
||||
<p className="text-xs font-medium text-slate-500 mt-1">Manage and track your recruitment status</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/employer/jobs/create"
|
||||
className="bg-[#185FA5] text-white px-6 py-3 rounded-2xl font-black text-xs uppercase tracking-widest shadow-xl shadow-blue-500/20 hover:bg-[#144f8a] transition-all flex items-center justify-center space-x-2"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>Post a Job</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Filters & Search Bar */}
|
||||
<div className="bg-white p-6 rounded-3xl border border-slate-200 shadow-sm flex flex-col md:flex-row gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by job title or category..."
|
||||
className="w-full pl-11 pr-4 py-3 bg-slate-50 border border-slate-100 rounded-2xl text-sm font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all"
|
||||
value={searchTerm}
|
||||
onChange={e => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center bg-slate-50 border border-slate-100 p-1.5 rounded-2xl">
|
||||
{['All', 'Active', 'Closed', 'Draft'].map(status => (
|
||||
<button
|
||||
key={status}
|
||||
onClick={() => setStatusFilter(status)}
|
||||
className={`px-4 py-2 text-[10px] font-black rounded-xl transition-all ${
|
||||
statusFilter === status
|
||||
? 'bg-white text-[#185FA5] shadow-sm border border-slate-200/50'
|
||||
: 'text-slate-400 hover:text-slate-600'
|
||||
}`}
|
||||
>
|
||||
{status.toUpperCase()}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<button className="p-3 bg-white border border-slate-200 text-slate-400 hover:text-[#185FA5] rounded-xl transition-all shadow-sm">
|
||||
<Download className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Jobs Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{filteredJobs.length > 0 ? (
|
||||
filteredJobs.map((job) => (
|
||||
<div key={job.id} className="bg-white rounded-[32px] border border-slate-200 shadow-sm hover:shadow-md transition-all overflow-hidden group">
|
||||
<div className="p-8 space-y-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="w-12 h-12 bg-blue-50 rounded-2xl flex items-center justify-center flex-shrink-0">
|
||||
<Briefcase className="w-6 h-6 text-[#185FA5]" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-black text-slate-900 tracking-tight group-hover:text-[#185FA5] transition-colors line-clamp-1">{job.title}</h3>
|
||||
<div className="flex items-center space-x-2 mt-1">
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">{job.category}</span>
|
||||
<span className="w-1 h-1 bg-slate-300 rounded-full" />
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">{job.posted_at}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest ${
|
||||
job.status === 'Active' ? 'bg-emerald-50 text-emerald-700 border-emerald-100' :
|
||||
job.status === 'Closed' ? 'bg-rose-50 text-rose-700 border-rose-100' :
|
||||
'bg-slate-50 text-slate-500 border-slate-100'
|
||||
}`}
|
||||
>
|
||||
{job.status === 'Active' && <CheckCircle2 className="w-3 h-3 mr-1" />}
|
||||
{job.status === 'Closed' && <XCircle className="w-3 h-3 mr-1" />}
|
||||
{job.status === 'Draft' && <Clock className="w-3 h-3 mr-1" />}
|
||||
{job.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 py-4 border-y border-slate-50">
|
||||
<div className="space-y-1">
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase tracking-[0.15em]">Location</div>
|
||||
<div className="flex items-center space-x-1.5 text-xs font-bold text-slate-700">
|
||||
<MapPin className="w-3.5 h-3.5 text-slate-400" />
|
||||
<span>{job.location}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase tracking-[0.15em]">Monthly Salary</div>
|
||||
<div className="flex items-center space-x-1.5 text-xs font-bold text-slate-700">
|
||||
<DollarSign className="w-3.5 h-3.5 text-emerald-600" />
|
||||
<span>{job.salary} AED</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex -space-x-2">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<div key={i} className="w-6 h-6 rounded-full border-2 border-white bg-slate-100 flex items-center justify-center text-[8px] font-bold text-slate-400">
|
||||
{i === 2 ? `+${job.applied_count}` : ''}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-[10px] font-black text-slate-600 uppercase tracking-tight">
|
||||
{job.applied_count} <span className="text-slate-400">Applications</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[10px] font-black text-slate-600 uppercase tracking-tight">
|
||||
{job.applied_count < job.workers_needed ? (
|
||||
<span>{job.applied_count}/{job.workers_needed} <span className="text-slate-400">Filled</span></span>
|
||||
) : (
|
||||
<span className="text-emerald-600 flex items-center"><CheckCircle2 className="w-3 h-3 mr-1" /> Fully Staffed</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3 pt-2">
|
||||
<Link
|
||||
href={`/employer/jobs/${job.id}/applicants`}
|
||||
className="flex-1 bg-[#185FA5] text-white py-3 rounded-xl font-black text-[10px] uppercase tracking-[0.15em] shadow-lg shadow-blue-500/10 hover:bg-[#144f8a] transition-all flex items-center justify-center space-x-2"
|
||||
>
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
<span>View Applicants</span>
|
||||
</Link>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className="p-3 bg-slate-50 text-slate-400 hover:text-slate-900 rounded-xl transition-all">
|
||||
<MoreHorizontal className="w-5 h-5" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48 p-2 rounded-2xl shadow-xl">
|
||||
<DropdownMenuLabel className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-2 py-2">Quick Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="p-3 rounded-xl focus:bg-blue-50 cursor-pointer">
|
||||
<div className="flex items-center space-x-3 text-[#185FA5]">
|
||||
<Edit2 className="w-4 h-4" />
|
||||
<span className="text-xs font-bold">Edit Posting</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="p-3 rounded-xl focus:bg-rose-50 cursor-pointer">
|
||||
<div className="flex items-center space-x-3 text-rose-600">
|
||||
<XCircle className="w-4 h-4" />
|
||||
<span className="text-xs font-bold">Close Job</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="col-span-full py-20 text-center bg-white rounded-[32px] border border-dashed border-slate-200">
|
||||
<Briefcase className="w-12 h-12 text-slate-200 mx-auto mb-4" />
|
||||
<h3 className="text-base font-bold text-slate-400 uppercase tracking-widest">No jobs found</h3>
|
||||
<Link href="/employer/jobs/create" className="mt-4 inline-flex items-center space-x-2 text-[#185FA5] font-black text-xs uppercase tracking-widest hover:underline">
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>Post your first job</span>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</EmployerLayout>
|
||||
);
|
||||
}
|
||||
115
resources/js/Pages/Employer/Messages/Index.jsx
Normal file
115
resources/js/Pages/Employer/Messages/Index.jsx
Normal file
@ -0,0 +1,115 @@
|
||||
import React from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import EmployerLayout from '../../../Layouts/EmployerLayout';
|
||||
import { MessageSquare, CheckCircle2, ChevronRight, Search, Plus } from 'lucide-react';
|
||||
|
||||
export default function Index({ conversations }) {
|
||||
return (
|
||||
<EmployerLayout title="Candidate Messages">
|
||||
<Head title="Messages - Employer Portal" />
|
||||
|
||||
<div className="max-w-5xl mx-auto space-y-6 select-none">
|
||||
{/* Header Actions */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search conversations..."
|
||||
className="w-full pl-10 pr-4 py-2.5 rounded-2xl border border-slate-200 text-sm focus:outline-none focus:ring-2 focus:ring-[#185FA5]/20 focus:border-[#185FA5] shadow-sm transition-all bg-white/80 backdrop-blur-sm"
|
||||
/>
|
||||
</div>
|
||||
<Link
|
||||
href="/employer/workers"
|
||||
className="bg-white hover:bg-slate-50 border border-slate-200 text-[#185FA5] px-4 py-2.5 rounded-2xl text-sm font-bold shadow-sm transition-all flex items-center justify-center space-x-2 whitespace-nowrap"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>New Conversation</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="bg-white/80 backdrop-blur-xl rounded-3xl border border-slate-200 shadow-xl shadow-blue-900/5 overflow-hidden">
|
||||
<div className="p-6 border-b border-slate-100 flex items-center justify-between bg-slate-50/50">
|
||||
<div className="flex items-center space-x-2">
|
||||
<MessageSquare className="w-5 h-5 text-[#185FA5]" />
|
||||
<h2 className="font-extrabold text-lg text-slate-800 tracking-tight">Active Threads</h2>
|
||||
</div>
|
||||
<span className="px-3 py-1 bg-white border border-slate-200 rounded-full text-xs text-slate-600 font-bold shadow-sm">
|
||||
{conversations?.length || 0} Total
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{conversations?.length > 0 ? (
|
||||
<div className="divide-y divide-slate-100/80">
|
||||
{conversations.map((thread) => (
|
||||
<Link
|
||||
key={thread.id}
|
||||
href={`/employer/messages/${thread.id}`}
|
||||
className={`flex items-center space-x-4 p-5 sm:p-6 transition-all duration-300 group hover:bg-slate-50/80 ${
|
||||
thread.unread ? 'bg-blue-50/30' : 'bg-white'
|
||||
}`}
|
||||
>
|
||||
<div className="relative flex-shrink-0">
|
||||
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#185FA5] to-blue-400 text-white flex items-center justify-center font-bold text-xl shadow-md group-hover:scale-105 transition-transform duration-300">
|
||||
{thread.worker_name.charAt(0)}
|
||||
</div>
|
||||
{thread.online && (
|
||||
<span className="absolute -bottom-1 -right-1 w-3.5 h-3.5 bg-emerald-400 border-2 border-white rounded-full shadow-sm animate-pulse" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0 pr-4">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center space-x-2 font-bold text-base text-slate-900 group-hover:text-[#185FA5] transition-colors truncate">
|
||||
<span>{thread.worker_name}</span>
|
||||
<span className="px-2 py-0.5 bg-slate-100 text-slate-500 rounded-md text-[10px] font-bold uppercase tracking-wider hidden sm:inline-block">
|
||||
{thread.category}
|
||||
</span>
|
||||
</div>
|
||||
<span className={`text-xs font-semibold whitespace-nowrap ${thread.unread ? 'text-[#185FA5]' : 'text-slate-400'}`}>
|
||||
{thread.sent_at}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className={`text-sm truncate flex-1 ${thread.unread ? 'font-semibold text-slate-800' : 'text-slate-500'}`}>
|
||||
{thread.last_message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center justify-center space-y-2 flex-shrink-0">
|
||||
{thread.unread ? (
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-red-500 animate-pulse shadow-sm shadow-red-500/50" />
|
||||
) : (
|
||||
<span className="w-2.5 h-2.5 opacity-0" />
|
||||
)}
|
||||
<div className="w-8 h-8 rounded-full bg-slate-50 group-hover:bg-[#185FA5] flex items-center justify-center transition-colors shadow-sm">
|
||||
<ChevronRight className="w-4 h-4 text-slate-400 group-hover:text-white transition-colors" />
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-20 space-y-4 bg-slate-50/30">
|
||||
<div className="w-16 h-16 bg-white rounded-3xl shadow-sm border border-slate-100 flex items-center justify-center mx-auto mb-2">
|
||||
<MessageSquare className="w-8 h-8 text-slate-300" />
|
||||
</div>
|
||||
<div className="text-lg font-extrabold text-slate-800">No active messages</div>
|
||||
<p className="text-sm text-slate-500 max-w-sm mx-auto leading-relaxed">
|
||||
When you message workers or schedule interviews, your conversation threads will appear here.
|
||||
</p>
|
||||
<Link
|
||||
href="/employer/workers"
|
||||
className="inline-block mt-4 bg-[#185FA5] text-white px-6 py-2.5 rounded-xl text-sm font-bold shadow-md shadow-blue-900/20 hover:-translate-y-0.5 active:translate-y-0 transition-all"
|
||||
>
|
||||
Find Workers
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</EmployerLayout>
|
||||
);
|
||||
}
|
||||
349
resources/js/Pages/Employer/Messages/Show.jsx
Normal file
349
resources/js/Pages/Employer/Messages/Show.jsx
Normal file
@ -0,0 +1,349 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import EmployerLayout from '../../../Layouts/EmployerLayout';
|
||||
import {
|
||||
Send,
|
||||
ArrowLeft,
|
||||
CheckCircle2,
|
||||
Globe2,
|
||||
DollarSign,
|
||||
Sparkles,
|
||||
MoreVertical,
|
||||
Phone,
|
||||
Video,
|
||||
Paperclip,
|
||||
Search,
|
||||
Info,
|
||||
Calendar,
|
||||
Briefcase,
|
||||
ShieldCheck,
|
||||
ChevronRight,
|
||||
MapPin,
|
||||
Smile
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function Show({ conversation, initialMessages, conversations = [] }) {
|
||||
const [messages, setMessages] = useState(initialMessages || []);
|
||||
const [input, setInput] = useState('');
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [showProfile, setShowProfile] = useState(false);
|
||||
const messagesEndRef = useRef(null);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages, isTyping]);
|
||||
|
||||
const filteredConversations = (conversations || []).filter(c =>
|
||||
c.worker_name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const sendMsg = (text) => {
|
||||
if (!text.trim()) return;
|
||||
|
||||
const newMsg = {
|
||||
id: Date.now(),
|
||||
sender: 'employer',
|
||||
text: text,
|
||||
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, newMsg]);
|
||||
setInput('');
|
||||
setIsTyping(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsTyping(false);
|
||||
setMessages(prev => [...prev, {
|
||||
id: Date.now() + 1,
|
||||
sender: 'worker',
|
||||
text: "Thank you for the message ma'am! I confirm I have received your request and look forward to speaking with you.",
|
||||
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
|
||||
}]);
|
||||
}, 1500 + Math.random() * 1000);
|
||||
};
|
||||
|
||||
const handleFormSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
sendMsg(input);
|
||||
};
|
||||
|
||||
const chips = [
|
||||
"Are you available for a video interview today?",
|
||||
"What is your final expected salary?",
|
||||
"Can you transfer your visa immediately?"
|
||||
];
|
||||
|
||||
return (
|
||||
<EmployerLayout title={null} fullPage={true}>
|
||||
<Head title={`Chat with ${conversation.worker_name} - Messages`} />
|
||||
|
||||
<div className="flex h-full bg-[#FAFBFF] overflow-hidden select-none">
|
||||
{/* Conversation List Sidebar (Desktop) */}
|
||||
<aside className="hidden xl:flex w-96 border-r border-slate-200 flex-col flex-shrink-0 bg-white shadow-sm z-30">
|
||||
<div className="p-6 border-b border-slate-100">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="font-black text-2xl text-slate-900 tracking-tight">Messages</h2>
|
||||
<button className="p-2 bg-slate-50 text-slate-400 rounded-xl hover:text-[#185FA5] transition-colors">
|
||||
<Search className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-5 pr-4 py-3.5 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all placeholder:text-slate-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto divide-y divide-slate-50">
|
||||
{filteredConversations.map((c) => (
|
||||
<Link
|
||||
key={c.id}
|
||||
href={`/employer/messages/${c.id}`}
|
||||
className={`flex items-center space-x-4 p-5 transition-all relative group ${
|
||||
c.id === conversation.id ? 'bg-blue-50/50' : 'hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
{c.id === conversation.id && (
|
||||
<div className="absolute left-0 top-0 bottom-0 w-1.5 bg-[#185FA5] rounded-r-full shadow-[0_0_15px_rgba(24,95,165,0.4)]" />
|
||||
)}
|
||||
|
||||
<div className="relative flex-shrink-0">
|
||||
<div className={`w-14 h-14 rounded-[20px] text-white flex items-center justify-center font-black text-xl shadow-lg ${
|
||||
c.id === conversation.id ? 'bg-gradient-to-br from-[#185FA5] to-blue-600' : 'bg-slate-200 text-slate-400'
|
||||
}`}>
|
||||
{c.worker_name.charAt(0)}
|
||||
</div>
|
||||
{c.online && (
|
||||
<span className="absolute -bottom-1 -right-1 w-4 h-4 bg-emerald-500 border-4 border-white rounded-full shadow-sm" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className={`text-sm font-black truncate ${c.id === conversation.id ? 'text-[#185FA5]' : 'text-slate-900'}`}>
|
||||
{c.worker_name}
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-400 font-bold uppercase tracking-tighter">{c.sent_at}</span>
|
||||
</div>
|
||||
<p className={`text-xs font-bold truncate leading-tight ${c.unread ? 'text-slate-900' : 'text-slate-400'}`}>
|
||||
{c.last_message}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{c.unread && (
|
||||
<div className="w-2.5 h-2.5 bg-red-500 rounded-full flex-shrink-0 shadow-[0_0_8px_rgba(239,68,68,0.4)]" />
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main Chat Interface */}
|
||||
<div className="flex-1 flex flex-col h-full bg-white relative z-10 shadow-2xl">
|
||||
{/* Glassmorphism Header */}
|
||||
<div className="bg-white/80 backdrop-blur-xl p-4 sm:p-6 border-b border-slate-100 flex items-center justify-between z-40 sticky top-0">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link
|
||||
href="/employer/messages"
|
||||
className="lg:hidden p-2.5 text-slate-400 hover:text-[#185FA5] hover:bg-blue-50 rounded-2xl transition-all"
|
||||
>
|
||||
<ArrowLeft className="w-6 h-6" />
|
||||
</Link>
|
||||
|
||||
<div className="relative group">
|
||||
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-[#185FA5] to-blue-600 text-white flex items-center justify-center font-black text-lg shadow-xl group-hover:scale-105 transition-transform">
|
||||
{conversation.worker_name.charAt(0)}
|
||||
</div>
|
||||
{conversation.online && (
|
||||
<span className="absolute -bottom-1 -right-1 w-4 h-4 bg-emerald-500 border-4 border-white rounded-full shadow-lg" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-black text-slate-900 text-lg tracking-tight">{conversation.worker_name}</h3>
|
||||
<span className="bg-emerald-50 text-emerald-600 p-0.5 rounded-full shadow-sm">
|
||||
<ShieldCheck className="w-4 h-4" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mt-0.5">
|
||||
<div className={`w-2 h-2 rounded-full ${conversation.online ? 'bg-emerald-500' : 'bg-slate-300'}`} />
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">
|
||||
{conversation.online ? 'Active Now' : 'Last seen 2h ago'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<Link
|
||||
href={`/employer/workers/${conversation.id}`}
|
||||
className="p-3 bg-slate-50 text-slate-400 hover:text-[#185FA5] hover:bg-blue-50 rounded-2xl transition-all shadow-sm"
|
||||
title="View Full Profile"
|
||||
>
|
||||
<Info className="w-5 h-5" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chat Messages */}
|
||||
<div className="flex-1 px-6 py-8 overflow-y-auto bg-[#F8FAFC]/50 space-y-8 scroll-smooth">
|
||||
<div className="flex flex-col items-center mb-10">
|
||||
<div className="bg-white border border-slate-200 px-6 py-2 rounded-full shadow-sm">
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Today, Dec 15</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{messages.map((msg, idx) => {
|
||||
const isEmp = msg.sender === 'employer';
|
||||
return (
|
||||
<div
|
||||
key={msg.id}
|
||||
className={`flex w-full ${isEmp ? 'justify-end' : 'justify-start'} animate-in fade-in slide-in-from-bottom-4 duration-500`}
|
||||
>
|
||||
<div className={`flex flex-col ${isEmp ? 'items-end' : 'items-start'} max-w-[80%] sm:max-w-[65%]`}>
|
||||
<div className={`px-6 py-4 rounded-[32px] text-[13px] font-bold leading-relaxed shadow-xl relative ${
|
||||
isEmp
|
||||
? 'bg-[#185FA5] text-white rounded-br-lg shadow-blue-500/10'
|
||||
: 'bg-white border border-slate-100 text-slate-800 rounded-bl-lg shadow-slate-200/40'
|
||||
}`}>
|
||||
{msg.text}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mt-2 px-2">
|
||||
<span className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">{msg.time}</span>
|
||||
{isEmp && <CheckCircle2 className="w-3 h-3 text-emerald-500" />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{isTyping && (
|
||||
<div className="flex w-full justify-start animate-in fade-in slide-in-from-bottom-2">
|
||||
<div className="bg-white border border-slate-100 px-6 py-4 rounded-[32px] rounded-bl-lg shadow-sm flex items-center space-x-1.5">
|
||||
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full animate-bounce [animation-delay:-0.3s]"></div>
|
||||
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full animate-bounce [animation-delay:-0.15s]"></div>
|
||||
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full animate-bounce"></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Quick Replies & Input */}
|
||||
<div className="bg-white border-t border-slate-100 p-6 space-y-4 shadow-[0_-10px_40px_rgba(0,0,0,0.02)]">
|
||||
<div className="flex items-center space-x-3 overflow-x-auto scrollbar-none pb-2">
|
||||
<div className="flex-shrink-0 p-2 bg-indigo-50 text-indigo-500 rounded-xl">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
</div>
|
||||
{chips.map((chip, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => sendMsg(chip)}
|
||||
className="bg-slate-50 hover:bg-[#185FA5] text-slate-600 hover:text-white px-5 py-2.5 rounded-2xl text-[11px] font-black uppercase tracking-widest flex-shrink-0 transition-all border border-slate-100 hover:border-transparent hover:scale-105 active:scale-95"
|
||||
>
|
||||
{chip}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleFormSubmit} className="flex items-center gap-4">
|
||||
<button type="button" className="p-4 bg-slate-50 text-slate-400 hover:text-slate-600 rounded-[24px] transition-all flex-shrink-0 border border-slate-100">
|
||||
<Paperclip className="w-6 h-6" />
|
||||
</button>
|
||||
<div className="flex-1 relative group">
|
||||
<input
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder="Type a secure message..."
|
||||
className="w-full pl-6 pr-14 py-5 bg-slate-50 border-none rounded-[32px] text-sm font-bold focus:ring-4 focus:ring-blue-100 transition-all outline-none group-focus-within:bg-white border-2 border-transparent group-focus-within:border-slate-100"
|
||||
/>
|
||||
<div className="absolute right-4 top-1/2 -translate-y-1/2 p-2 text-slate-300">
|
||||
<Smile className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!input.trim()}
|
||||
className="bg-[#185FA5] hover:bg-[#144f8a] disabled:bg-slate-200 text-white p-5 rounded-[24px] transition-all shadow-xl shadow-blue-500/20 flex items-center justify-center flex-shrink-0 group hover:-translate-y-1 active:translate-y-0"
|
||||
>
|
||||
<Send className="w-6 h-6 group-hover:translate-x-1 group-hover:-translate-y-1 transition-transform" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Candidate Context Sidebar (Right) */}
|
||||
{showProfile && (
|
||||
<aside className="hidden lg:flex w-80 2xl:w-96 border-l border-slate-200 flex-col flex-shrink-0 bg-white z-20 animate-in slide-in-from-right-10 duration-500">
|
||||
<div className="p-8 space-y-8 overflow-y-auto">
|
||||
<div className="text-center space-y-4">
|
||||
<div className="w-24 h-24 rounded-[40px] bg-gradient-to-br from-[#185FA5] to-blue-600 text-white flex items-center justify-center font-black text-3xl mx-auto shadow-2xl relative">
|
||||
{conversation.worker_name.charAt(0)}
|
||||
<span className="absolute -bottom-2 -right-2 bg-emerald-500 p-2 rounded-2xl border-4 border-white shadow-lg">
|
||||
<ShieldCheck className="w-5 h-5 text-white" />
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-black text-2xl text-slate-900 tracking-tight">{conversation.worker_name}</h4>
|
||||
<p className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em] mt-1">{conversation.category}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="bg-slate-50 p-4 rounded-3xl text-center">
|
||||
<DollarSign className="w-4 h-4 text-emerald-600 mx-auto mb-1.5" />
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Expected</div>
|
||||
<div className="text-sm font-black text-slate-900 mt-1 truncate">{conversation.salary}</div>
|
||||
</div>
|
||||
<div className="bg-slate-50 p-4 rounded-3xl text-center">
|
||||
<MapPin className="w-4 h-4 text-blue-600 mx-auto mb-1.5" />
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Region</div>
|
||||
<div className="text-sm font-black text-slate-900 mt-1 truncate">{conversation.nationality}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h5 className="text-[10px] font-black text-slate-400 uppercase tracking-widest pl-2">Recruitment Context</h5>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ label: 'Visa Status', value: 'Transferable', icon: Briefcase, color: 'text-blue-500' },
|
||||
{ label: 'Medical Test', value: 'Cleared', icon: ShieldCheck, color: 'text-emerald-500' },
|
||||
{ label: 'Last Active', value: 'Today, 10:15 AM', icon: Calendar, color: 'text-slate-500' }
|
||||
].map((item, i) => (
|
||||
<div key={i} className="flex items-center justify-between p-4 bg-slate-50/50 border border-slate-100 rounded-3xl">
|
||||
<div className="flex items-center space-x-3">
|
||||
<item.icon className={`w-4 h-4 ${item.color}`} />
|
||||
<span className="text-[11px] font-black text-slate-500 uppercase tracking-tighter">{item.label}</span>
|
||||
</div>
|
||||
<span className="text-[11px] font-black text-slate-900">{item.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<Link
|
||||
href={`/employer/workers/${conversation.id}`}
|
||||
className="w-full bg-slate-900 text-white py-4 rounded-3xl text-xs font-black uppercase tracking-widest flex items-center justify-center space-x-2 shadow-xl shadow-slate-900/10 hover:-translate-y-1 transition-all"
|
||||
>
|
||||
<span>Full Dossier</span>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
)}
|
||||
</div>
|
||||
</EmployerLayout>
|
||||
);
|
||||
}
|
||||
366
resources/js/Pages/Employer/Profile.jsx
Normal file
366
resources/js/Pages/Employer/Profile.jsx
Normal file
@ -0,0 +1,366 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Head } from '@inertiajs/react';
|
||||
import EmployerLayout from '../../Layouts/EmployerLayout';
|
||||
import {
|
||||
User,
|
||||
CheckCircle2,
|
||||
Save,
|
||||
Building2,
|
||||
Lock,
|
||||
Bell,
|
||||
ShieldCheck,
|
||||
Mail,
|
||||
Phone,
|
||||
Globe,
|
||||
CreditCard,
|
||||
ChevronRight,
|
||||
Camera
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function Profile({ employerProfile }) {
|
||||
const [profile, setProfile] = useState(employerProfile || {
|
||||
name: 'John Doe',
|
||||
company_name: 'Al Mansoor Household',
|
||||
email: 'employer@example.com',
|
||||
phone: '+971 50 123 4567',
|
||||
language: 'English',
|
||||
notifications: true
|
||||
});
|
||||
const [activeTab, setActiveTab] = useState('personal');
|
||||
const [saved, setSaved] = useState(false);
|
||||
|
||||
const handleSave = (e) => {
|
||||
e.preventDefault();
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 3000);
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{ id: 'personal', label: 'Personal Information', icon: User },
|
||||
{ id: 'company', label: 'Company & Verification', icon: Building2 },
|
||||
{ id: 'security', label: 'Security & Password', icon: Lock },
|
||||
{ id: 'billing', label: 'Billing & History', icon: CreditCard },
|
||||
{ id: 'notifications', label: 'Notification Settings', icon: Bell },
|
||||
];
|
||||
|
||||
return (
|
||||
<EmployerLayout title="Account Settings">
|
||||
<Head title="Profile - Employer Portal" />
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-8 select-none">
|
||||
{/* Left Side: Navigation Tabs */}
|
||||
<aside className="w-full lg:w-72 space-y-2">
|
||||
{tabs.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`w-full flex items-center justify-between p-4 rounded-2xl text-sm font-bold transition-all ${
|
||||
activeTab === tab.id
|
||||
? 'bg-[#185FA5] text-white shadow-lg shadow-blue-200'
|
||||
: 'bg-white text-slate-600 hover:bg-slate-50 border border-slate-100'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Icon className="w-5 h-5" />
|
||||
<span>{tab.label}</span>
|
||||
</div>
|
||||
<ChevronRight className={`w-4 h-4 transition-transform ${activeTab === tab.id ? 'rotate-90' : ''}`} />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</aside>
|
||||
|
||||
{/* Right Side: Content Area */}
|
||||
<div className="flex-1 space-y-6">
|
||||
{/* Header Info Card */}
|
||||
<div className="bg-white rounded-3xl border border-slate-200 p-8 shadow-sm relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-blue-50 rounded-full -mr-16 -mt-16 opacity-50" />
|
||||
|
||||
<div className="flex flex-col md:flex-row items-center md:items-start space-y-6 md:space-y-0 md:space-x-8 relative z-10">
|
||||
<div className="relative group">
|
||||
<div className="w-24 h-24 rounded-full bg-blue-100 text-[#185FA5] flex items-center justify-center font-black text-3xl border-4 border-white shadow-xl">
|
||||
{profile.name?.charAt(0)}
|
||||
</div>
|
||||
<button className="absolute bottom-0 right-0 p-2 bg-white rounded-full shadow-lg border border-slate-100 text-slate-600 hover:text-[#185FA5] transition-colors">
|
||||
<Camera className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 text-center md:text-left">
|
||||
<h2 className="text-2xl font-black text-slate-900 leading-tight">{profile.name}</h2>
|
||||
<p className="text-slate-500 font-bold text-sm mt-1">{profile.company_name}</p>
|
||||
|
||||
<div className="flex flex-wrap justify-center md:justify-start gap-3 mt-4">
|
||||
<span className="bg-emerald-50 text-emerald-700 px-3 py-1.5 rounded-xl text-[10px] font-black uppercase tracking-widest flex items-center space-x-1.5 border border-emerald-100">
|
||||
<ShieldCheck className="w-3.5 h-3.5" />
|
||||
<span>Emirates ID Vetted</span>
|
||||
</span>
|
||||
<span className="bg-blue-50 text-blue-700 px-3 py-1.5 rounded-xl text-[10px] font-black uppercase tracking-widest flex items-center space-x-1.5 border border-blue-100">
|
||||
<CheckCircle2 className="w-3.5 h-3.5" />
|
||||
<span>Verified Employer</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Specific Content */}
|
||||
<div className="bg-white rounded-3xl border border-slate-200 shadow-sm overflow-hidden">
|
||||
<form onSubmit={handleSave} className="p-8 space-y-8">
|
||||
{activeTab === 'personal' && (
|
||||
<div className="space-y-6">
|
||||
<div className="border-b border-slate-100 pb-4">
|
||||
<h3 className="text-lg font-black text-slate-900">Personal Information</h3>
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Manage your basic account details</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">Full Name</label>
|
||||
<div className="relative">
|
||||
<User className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300" />
|
||||
<input
|
||||
type="text"
|
||||
value={profile.name}
|
||||
onChange={(e) => setProfile({ ...profile, name: e.target.value })}
|
||||
className="w-full pl-12 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">Email Address</label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300" />
|
||||
<input
|
||||
type="email"
|
||||
value={profile.email}
|
||||
onChange={(e) => setProfile({ ...profile, email: e.target.value })}
|
||||
className="w-full pl-12 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">Phone Number</label>
|
||||
<div className="relative">
|
||||
<Phone className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300" />
|
||||
<input
|
||||
type="text"
|
||||
value={profile.phone}
|
||||
onChange={(e) => setProfile({ ...profile, phone: e.target.value })}
|
||||
className="w-full pl-12 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">Preferred Language</label>
|
||||
<div className="relative">
|
||||
<Globe className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300" />
|
||||
<select
|
||||
value={profile.language}
|
||||
onChange={(e) => setProfile({ ...profile, language: e.target.value })}
|
||||
className="w-full pl-12 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 appearance-none transition-all"
|
||||
>
|
||||
<option>English</option>
|
||||
<option>Arabic</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'company' && (
|
||||
<div className="space-y-6">
|
||||
<div className="border-b border-slate-100 pb-4">
|
||||
<h3 className="text-lg font-black text-slate-900">Company & Verification</h3>
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Manage your professional credentials</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">Company / Household Name</label>
|
||||
<div className="relative">
|
||||
<Building2 className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300" />
|
||||
<input
|
||||
type="text"
|
||||
value={profile.company_name}
|
||||
onChange={(e) => setProfile({ ...profile, company_name: e.target.value })}
|
||||
className="w-full pl-12 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-slate-50 rounded-3xl border border-slate-100 flex items-start space-x-4">
|
||||
<div className="p-3 bg-emerald-100 rounded-2xl">
|
||||
<ShieldCheck className="w-6 h-6 text-emerald-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-black text-slate-900">Emirates ID Verification</h4>
|
||||
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-widest mt-0.5">Reference: EID-9182-XJ</p>
|
||||
<p className="text-xs text-slate-600 mt-2 font-medium leading-relaxed">
|
||||
Your Emirates ID has been successfully vetted and matched with our internal records. This allows you to initiate direct contact with all verified workers.
|
||||
</p>
|
||||
<div className="flex items-center space-x-2 mt-4 text-emerald-600">
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
<span className="text-[10px] font-black uppercase">Verified on Jan 15, 2026</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'security' && (
|
||||
<div className="space-y-6">
|
||||
<div className="border-b border-slate-100 pb-4">
|
||||
<h3 className="text-lg font-black text-slate-900">Security & Password</h3>
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Keep your account protected</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 max-w-md">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">Current Password</label>
|
||||
<input type="password" placeholder="••••••••" className="w-full px-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">New Password</label>
|
||||
<input type="password" placeholder="••••••••" className="w-full px-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-black text-slate-500 uppercase tracking-tighter ml-1">Confirm New Password</label>
|
||||
<input type="password" placeholder="••••••••" className="w-full px-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'billing' && (
|
||||
<div className="space-y-8">
|
||||
<div className="border-b border-slate-100 pb-4">
|
||||
<h3 className="text-lg font-black text-slate-900">Billing & History</h3>
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Manage your subscriptions and payments</p>
|
||||
</div>
|
||||
|
||||
{/* Current Plan Card */}
|
||||
<div className="p-6 bg-gradient-to-br from-slate-900 to-slate-800 rounded-3xl text-white shadow-xl relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-48 h-48 bg-blue-500/10 rounded-full -mr-24 -mt-24 blur-3xl" />
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-slate-400">Current Plan</span>
|
||||
<h4 className="text-2xl font-black mt-1">Premium Employer</h4>
|
||||
</div>
|
||||
<div className="bg-white/10 backdrop-blur-md px-4 py-2 rounded-xl border border-white/10">
|
||||
<span className="text-sm font-black">Active</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 flex items-end justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="text-[10px] font-bold text-slate-400 uppercase">Next Payment</div>
|
||||
<div className="text-sm font-bold italic">Dec 15, 2026</div>
|
||||
</div>
|
||||
<div className="text-3xl font-black tracking-tighter">
|
||||
499 <span className="text-sm font-bold text-slate-400">AED/mo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Transaction History */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-xs font-black text-slate-400 uppercase tracking-widest">Recent Transactions</h4>
|
||||
<div className="bg-slate-50 rounded-2xl border border-slate-100 overflow-hidden">
|
||||
<table className="w-full text-left">
|
||||
<thead className="bg-slate-100/50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-[10px] font-black text-slate-400 uppercase tracking-tighter">Date</th>
|
||||
<th className="px-4 py-3 text-[10px] font-black text-slate-400 uppercase tracking-tighter">Description</th>
|
||||
<th className="px-4 py-3 text-[10px] font-black text-slate-400 uppercase tracking-tighter">Amount</th>
|
||||
<th className="px-4 py-3 text-[10px] font-black text-slate-400 uppercase tracking-tighter text-right">Receipt</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{[
|
||||
{ date: 'Nov 15, 2026', desc: 'Premium Monthly Subscription', amount: '499 AED' },
|
||||
{ date: 'Oct 15, 2026', desc: 'Premium Monthly Subscription', amount: '499 AED' }
|
||||
].map((t, i) => (
|
||||
<tr key={i}>
|
||||
<td className="px-4 py-4 text-xs font-bold text-slate-600">{t.date}</td>
|
||||
<td className="px-4 py-4 text-xs font-bold text-slate-900">{t.desc}</td>
|
||||
<td className="px-4 py-4 text-xs font-black text-slate-900">{t.amount}</td>
|
||||
<td className="px-4 py-4 text-right">
|
||||
<button className="p-2 text-slate-400 hover:text-[#185FA5] transition-colors">
|
||||
<CreditCard className="w-4 h-4" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'notifications' && (
|
||||
<div className="space-y-6">
|
||||
<div className="border-b border-slate-100 pb-4">
|
||||
<h3 className="text-lg font-black text-slate-900">Notification Settings</h3>
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Choose how you want to be notified</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{ title: 'Email Notifications', desc: 'Receive daily updates and recruitment alerts via email.' },
|
||||
{ title: 'SMS Alerts', desc: 'Get instant text messages for direct message replies.' },
|
||||
{ title: 'Browser Notifications', desc: 'Real-time alerts while using the portal.' },
|
||||
{ title: 'Marketing Updates', desc: 'Receive news about new features and promotions.' }
|
||||
].map((n, i) => (
|
||||
<div key={i} className="flex items-center justify-between p-4 bg-slate-50 rounded-2xl border border-slate-100">
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-sm font-black text-slate-900">{n.title}</div>
|
||||
<div className="text-[10px] font-bold text-slate-400 leading-tight">{n.desc}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setProfile({ ...profile, notifications: !profile.notifications })}
|
||||
className={`w-10 h-6 rounded-full p-1 transition-all ${profile.notifications ? 'bg-[#185FA5]' : 'bg-slate-200'}`}
|
||||
>
|
||||
<div className={`w-4 h-4 bg-white rounded-full transition-transform ${profile.notifications ? 'translate-x-4' : 'translate-x-0'}`} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-8 border-t border-slate-100 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
{saved ? (
|
||||
<div className="flex items-center space-x-2 text-emerald-600 animate-in fade-in slide-in-from-left-4">
|
||||
<CheckCircle2 className="w-5 h-5" />
|
||||
<span className="text-xs font-black uppercase">Changes saved successfully</span>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-[10px] font-bold text-slate-400 max-w-xs text-center sm:text-left">
|
||||
Some updates may require additional verification before taking effect.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full sm:w-auto bg-[#185FA5] hover:bg-[#144f8a] text-white px-8 py-3 rounded-2xl text-xs font-black uppercase tracking-widest flex items-center justify-center space-x-2 transition-all shadow-lg shadow-blue-200/50"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
<span>Update Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</EmployerLayout>
|
||||
);
|
||||
}
|
||||
253
resources/js/Pages/Employer/SelectedCandidates.jsx
Normal file
253
resources/js/Pages/Employer/SelectedCandidates.jsx
Normal file
@ -0,0 +1,253 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import EmployerLayout from '../../Layouts/EmployerLayout';
|
||||
import {
|
||||
CheckCircle2,
|
||||
Globe2,
|
||||
Briefcase,
|
||||
DollarSign,
|
||||
MessageSquare,
|
||||
UserCheck,
|
||||
Search,
|
||||
FileText,
|
||||
Download,
|
||||
Printer,
|
||||
Copy,
|
||||
FileSpreadsheet,
|
||||
FileJson,
|
||||
Filter,
|
||||
ChevronRight,
|
||||
MoreHorizontal,
|
||||
Users,
|
||||
Send
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
|
||||
export default function SelectedCandidates({ selectedWorkers }) {
|
||||
const [workers, setWorkers] = useState((selectedWorkers || []).filter(w => w.status !== 'Searching'));
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const stats = [
|
||||
{
|
||||
label: 'Total Candidates',
|
||||
count: workers.length,
|
||||
icon: Users,
|
||||
color: 'text-slate-600',
|
||||
bg: 'bg-slate-50'
|
||||
},
|
||||
{
|
||||
label: 'Reviewing',
|
||||
count: workers.filter(w => w.status === 'Reviewing' || !w.status).length,
|
||||
icon: Search,
|
||||
color: 'text-amber-600',
|
||||
bg: 'bg-amber-50'
|
||||
},
|
||||
{
|
||||
label: 'Offer Sent',
|
||||
count: workers.filter(w => w.status === 'Offer Sent').length,
|
||||
icon: Send,
|
||||
color: 'text-blue-600',
|
||||
bg: 'bg-blue-50'
|
||||
},
|
||||
{
|
||||
label: 'Hired',
|
||||
count: workers.filter(w => w.status === 'Hired').length,
|
||||
icon: CheckCircle2,
|
||||
color: 'text-emerald-600',
|
||||
bg: 'bg-emerald-50'
|
||||
},
|
||||
{
|
||||
label: 'Rejected',
|
||||
count: workers.filter(w => w.status === 'Rejected').length,
|
||||
icon: UserCheck,
|
||||
color: 'text-rose-600',
|
||||
bg: 'bg-rose-50'
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EmployerLayout>
|
||||
<Head title="Candidates - Employer Portal" />
|
||||
|
||||
<div className="space-y-8 select-none">
|
||||
{/* Header Section */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-black text-slate-900 tracking-tight">Candidates</h1>
|
||||
<p className="text-xs font-medium text-slate-500 mt-1">Manage your workforce recruitment pipeline</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{stats.map((stat) => (
|
||||
<div key={stat.label} className="bg-white p-5 rounded-2xl border border-slate-100 shadow-sm flex items-center space-x-4">
|
||||
<div className={`p-3 rounded-xl ${stat.bg}`}>
|
||||
<stat.icon className={`w-6 h-6 ${stat.color}`} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-black text-slate-900 leading-none">{stat.count}</div>
|
||||
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest mt-1">{stat.label}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Table Section */}
|
||||
<div className="bg-white rounded-3xl border border-slate-200 shadow-sm overflow-hidden">
|
||||
<div className="p-6 border-b border-slate-100 bg-slate-50/50 flex items-center justify-between">
|
||||
<div className="relative w-72">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name..."
|
||||
className="w-full pl-10 pr-4 py-2 bg-white border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-blue-100 focus:border-[#185FA5] transition-all outline-none"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center bg-white border border-slate-200 rounded-xl p-1">
|
||||
{['COPY', 'CSV', 'PDF', 'PRINT'].map(action => (
|
||||
<button key={action} className="px-3 py-1.5 text-[10px] font-bold text-slate-500 hover:text-[#185FA5] hover:bg-blue-50 rounded-lg transition-all">
|
||||
{action}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button className="p-2.5 bg-white border border-slate-200 text-slate-400 hover:text-[#185FA5] rounded-xl transition-all shadow-sm">
|
||||
<Filter className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-100">
|
||||
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">Candidate</th>
|
||||
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">Selected</th>
|
||||
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">Category</th>
|
||||
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">Salary</th>
|
||||
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">Status</th>
|
||||
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-50">
|
||||
{workers.length > 0 ? (
|
||||
workers.filter(w => w.name.toLowerCase().includes(searchTerm.toLowerCase())).map((worker) => (
|
||||
<tr key={worker.id} className="group hover:bg-slate-50/50 transition-colors">
|
||||
<td className="px-6 py-5">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 rounded-full bg-blue-100 text-[#185FA5] flex items-center justify-center font-bold text-sm">
|
||||
{worker.name.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-bold text-slate-900">{worker.name}</div>
|
||||
<div className="text-[10px] text-slate-400 font-medium">{worker.nationality}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-5 text-[11px] font-bold text-slate-500">Nov 14, 2026</td>
|
||||
<td className="px-6 py-5">
|
||||
<span className="px-2 py-1 bg-slate-100 text-slate-600 text-[10px] font-bold rounded-lg">{worker.category}</span>
|
||||
</td>
|
||||
<td className="px-6 py-5">
|
||||
<span className="text-sm font-bold text-slate-800">{worker.salary} <span className="text-[10px] text-slate-400">AED</span></span>
|
||||
</td>
|
||||
<td className="px-6 py-5">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className={`inline-flex items-center px-2.5 py-1 rounded-full text-[10px] font-black tracking-tight hover:ring-2 hover:ring-offset-1 transition-all ${
|
||||
worker.status === 'Hired' ? 'bg-emerald-50 text-emerald-700 hover:ring-emerald-200' :
|
||||
worker.status === 'Rejected' ? 'bg-rose-50 text-rose-700 hover:ring-rose-200' :
|
||||
worker.status === 'Offer Sent' ? 'bg-blue-50 text-blue-700 hover:ring-blue-200' :
|
||||
'bg-amber-50 text-amber-700 hover:ring-amber-200'
|
||||
}`}>
|
||||
<span className={`w-1 h-1 rounded-full mr-1.5 ${
|
||||
worker.status === 'Hired' ? 'bg-emerald-500' :
|
||||
worker.status === 'Rejected' ? 'bg-rose-500' :
|
||||
worker.status === 'Offer Sent' ? 'bg-blue-500' :
|
||||
'bg-amber-500'
|
||||
}`} />
|
||||
{(worker.status || 'Reviewing').toUpperCase()}
|
||||
<ChevronRight className="w-3 h-3 ml-1 rotate-90 opacity-50" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-44 p-2 rounded-xl">
|
||||
<DropdownMenuLabel className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-2 py-1.5">Change Status</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="text-xs font-bold text-slate-600 focus:text-amber-700 focus:bg-amber-50 rounded-lg">
|
||||
Reviewing
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="text-xs font-bold text-slate-600 focus:text-blue-700 focus:bg-blue-50 rounded-lg">
|
||||
Offer Sent
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="text-xs font-bold text-slate-600 focus:text-emerald-700 focus:bg-emerald-50 rounded-lg">
|
||||
Hired
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="text-xs font-bold text-rose-600 focus:text-rose-700 focus:bg-rose-50 rounded-lg">
|
||||
Rejected
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</td>
|
||||
<td className="px-6 py-5 text-right">
|
||||
<div className="flex items-center justify-end space-x-1">
|
||||
<Link
|
||||
href={`/employer/messages/${worker.id}`}
|
||||
className="p-2 bg-blue-50 text-[#185FA5] hover:bg-[#185FA5] hover:text-white rounded-lg transition-all"
|
||||
>
|
||||
<MessageSquare className="w-3.5 h-3.5" />
|
||||
</Link>
|
||||
<Link
|
||||
href={`/employer/workers/${worker.id}`}
|
||||
className="p-2 bg-slate-50 text-slate-400 hover:text-slate-900 rounded-lg transition-all"
|
||||
>
|
||||
<MoreHorizontal className="w-3.5 h-3.5" />
|
||||
</Link>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="7" className="p-20 text-center">
|
||||
<div className="space-y-3">
|
||||
<UserCheck className="w-12 h-12 text-slate-200 mx-auto" />
|
||||
<div className="text-base font-bold text-slate-400">No candidates found</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination Footer */}
|
||||
<div className="px-6 py-4 bg-slate-50/50 border-t border-slate-100 flex items-center justify-between">
|
||||
<div className="text-[10px] font-bold text-slate-500 uppercase tracking-widest">
|
||||
Showing 1 to {workers.length} of {workers.length} candidates
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button className="px-3 py-1.5 bg-white border border-slate-200 text-slate-400 rounded-lg text-[10px] font-black cursor-not-allowed">PREV</button>
|
||||
<div className="flex items-center space-x-1">
|
||||
<button className="w-8 h-8 flex items-center justify-center bg-[#185FA5] text-white rounded-lg text-[10px] font-black">1</button>
|
||||
<button className="w-8 h-8 flex items-center justify-center bg-white border border-slate-200 text-slate-600 hover:border-[#185FA5] rounded-lg text-[10px] font-black transition-colors">2</button>
|
||||
</div>
|
||||
<button className="px-3 py-1.5 bg-white border border-slate-200 text-[#185FA5] hover:bg-blue-50 rounded-lg text-[10px] font-black transition-colors">NEXT</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</EmployerLayout>
|
||||
);
|
||||
}
|
||||
140
resources/js/Pages/Employer/Shortlist.jsx
Normal file
140
resources/js/Pages/Employer/Shortlist.jsx
Normal file
@ -0,0 +1,140 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import EmployerLayout from '../../Layouts/EmployerLayout';
|
||||
import {
|
||||
Bookmark,
|
||||
CheckCircle2,
|
||||
Globe2,
|
||||
Briefcase,
|
||||
DollarSign,
|
||||
MessageSquare,
|
||||
Trash2,
|
||||
SlidersHorizontal
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function Shortlist({ shortlistedWorkers }) {
|
||||
const [workers, setWorkers] = useState(shortlistedWorkers || []);
|
||||
|
||||
const removeWorker = (id) => {
|
||||
setWorkers(workers.filter(w => w.id !== id));
|
||||
};
|
||||
|
||||
return (
|
||||
<EmployerLayout title="My Shortlist">
|
||||
<Head title="Shortlist - Employer Portal" />
|
||||
|
||||
<div className="space-y-6 select-none">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-slate-500 font-medium">
|
||||
You have <span className="text-slate-900 font-bold">{workers.length}</span> candidates saved for review
|
||||
</p>
|
||||
<Link
|
||||
href="/employer/workers"
|
||||
className="text-xs font-bold text-[#185FA5] hover:underline"
|
||||
>
|
||||
Browse more workers
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{workers.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{workers.map((worker) => (
|
||||
<div key={worker.id} className="bg-white rounded-2xl border border-slate-200 shadow-sm hover:shadow-md transition-all flex flex-col overflow-hidden">
|
||||
{/* Header Banner */}
|
||||
<div className="bg-slate-50 p-6 border-b border-slate-100 flex items-start justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-14 h-14 rounded-full bg-blue-100 text-[#185FA5] flex items-center justify-center font-bold text-xl border border-blue-200 shadow-inner">
|
||||
{worker.name.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-base text-slate-900 flex items-center space-x-1.5">
|
||||
<span>{worker.name}</span>
|
||||
{worker.verified && <CheckCircle2 className="w-4 h-4 text-emerald-600" />}
|
||||
</div>
|
||||
<div className="flex items-center space-x-1.5 text-xs text-slate-500 mt-0.5">
|
||||
<Globe2 className="w-3.5 h-3.5 text-slate-400" />
|
||||
<span>{worker.nationality}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeWorker(worker.id)}
|
||||
className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-xl transition-colors"
|
||||
title="Remove from shortlist"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="p-6 flex-1 flex flex-col justify-between space-y-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="px-2.5 py-1 bg-blue-50 text-[#185FA5] font-bold rounded-lg border border-blue-100">
|
||||
{worker.category}
|
||||
</span>
|
||||
<div className="flex items-center space-x-1 font-bold text-slate-800">
|
||||
<DollarSign className="w-4 h-4 text-emerald-600" />
|
||||
<span>{worker.salary} AED / mo</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 p-3 bg-slate-50 rounded-xl text-[11px] text-slate-600 font-medium">
|
||||
<div className="flex items-center space-x-1.5">
|
||||
<Briefcase className="w-3.5 h-3.5 text-slate-400" />
|
||||
<span>Exp: {worker.experience}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1.5">
|
||||
<span className="font-bold text-[#185FA5] truncate">Avail: {worker.availability}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{worker.skills?.map(s => (
|
||||
<span key={s} className="text-[10px] bg-slate-100 text-slate-600 px-2 py-0.5 rounded-md font-semibold">
|
||||
{s}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3 pt-2">
|
||||
<Link
|
||||
href={`/employer/workers/${worker.id}`}
|
||||
className="flex-1 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-xl h-10 font-bold text-xs flex items-center justify-center transition-colors"
|
||||
>
|
||||
View Profile
|
||||
</Link>
|
||||
<Link
|
||||
href={`/employer/messages/${worker.id}`}
|
||||
className="flex-1 bg-[#185FA5] hover:bg-[#144f8a] text-white rounded-xl h-10 font-bold text-xs flex items-center justify-center space-x-1.5 transition-colors shadow-sm"
|
||||
>
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
<span>Message</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-16 bg-white rounded-2xl border border-slate-200 shadow-sm space-y-3">
|
||||
<SlidersHorizontal className="w-12 h-12 text-slate-300 mx-auto" />
|
||||
<div className="text-base font-bold text-slate-800">Your shortlist is empty</div>
|
||||
<p className="text-xs text-slate-500 max-w-sm mx-auto">
|
||||
Explore candidate profiles and click the bookmark icon to save them for later comparison.
|
||||
</p>
|
||||
<Link
|
||||
href="/employer/workers"
|
||||
className="inline-block mt-2 bg-[#185FA5] text-white px-6 py-2.5 rounded-xl text-xs font-bold shadow-sm hover:bg-[#144f8a] transition-colors"
|
||||
>
|
||||
Find Workers Now
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</EmployerLayout>
|
||||
);
|
||||
}
|
||||
113
resources/js/Pages/Employer/Subscription.jsx
Normal file
113
resources/js/Pages/Employer/Subscription.jsx
Normal file
@ -0,0 +1,113 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import EmployerLayout from '../../Layouts/EmployerLayout';
|
||||
import { CreditCard, CheckCircle2, AlertCircle, ArrowRight, Sparkles } from 'lucide-react';
|
||||
|
||||
export default function Subscription({ currentPlan, expiresAt, plans }) {
|
||||
const [selectedId, setSelectedId] = useState('premium');
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
|
||||
const handleSelect = (id) => {
|
||||
setSelectedId(id);
|
||||
setShowSuccess(true);
|
||||
setTimeout(() => setShowSuccess(false), 4000);
|
||||
};
|
||||
|
||||
return (
|
||||
<EmployerLayout title="Manage Subscription">
|
||||
<Head title="Subscription - Employer Portal" />
|
||||
|
||||
<div className="space-y-8 select-none">
|
||||
{/* Current Status Banner */}
|
||||
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-blue-50 text-[#185FA5] flex items-center justify-center font-bold">
|
||||
<CreditCard className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wider">Current Active Plan</div>
|
||||
<div className="text-lg font-bold text-slate-900">{currentPlan}</div>
|
||||
<div className="text-xs text-slate-500 mt-0.5">Renews automatically on <span className="font-bold text-slate-700">{expiresAt}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 bg-emerald-50 border border-emerald-200 text-emerald-800 px-3.5 py-1.5 rounded-xl text-xs font-bold">
|
||||
<CheckCircle2 className="w-4 h-4 text-emerald-600" />
|
||||
<span>Active & Verified</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showSuccess && (
|
||||
<div className="p-4 bg-emerald-500 text-white rounded-xl text-xs font-bold flex items-center space-x-2 shadow-sm animate-fade-in">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
<span>Plan successfully updated! Mock PayTabs billing cycle applied.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pricing Grid */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-slate-900 tracking-tight">Available Subscription Tiers</h2>
|
||||
<p className="text-xs text-slate-500 mt-0.5">Upgrade or change your plan anytime. Zero agency recruitment commissions.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 pt-2">
|
||||
{plans.map((plan) => {
|
||||
const isSelected = selectedId === plan.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={plan.id}
|
||||
className={`bg-white rounded-2xl border-2 shadow-sm flex flex-col justify-between overflow-hidden transition-all relative ${
|
||||
isSelected
|
||||
? 'border-[#185FA5] ring-4 ring-[#185FA5]/10'
|
||||
: 'border-slate-200 hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
{plan.popular && (
|
||||
<div className="bg-[#185FA5] text-white text-[10px] font-bold uppercase tracking-wider text-center py-1 shadow-xs">
|
||||
Most Popular Tier
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-6 space-y-6 flex-1 flex flex-col justify-between">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="font-bold text-base text-slate-900">{plan.name}</h3>
|
||||
<div className="mt-2 flex items-baseline space-x-1">
|
||||
<span className="text-3xl font-extrabold text-slate-900">{plan.price}</span>
|
||||
<span className="text-xs font-semibold text-slate-500">/ {plan.period}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="space-y-3 pt-4 border-t border-slate-100">
|
||||
{plan.features.map(f => (
|
||||
<li key={f} className="flex items-start space-x-3 text-xs text-slate-600">
|
||||
<CheckCircle2 className="w-4 h-4 text-emerald-600 flex-shrink-0 mt-0.5" />
|
||||
<span>{f}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSelect(plan.id)}
|
||||
className={`w-full h-11 rounded-xl font-bold text-xs flex items-center justify-center transition-colors shadow-sm ${
|
||||
isSelected
|
||||
? 'bg-emerald-600 hover:bg-emerald-700 text-white'
|
||||
: 'bg-[#185FA5] hover:bg-[#144f8a] text-white'
|
||||
}`}
|
||||
>
|
||||
{isSelected ? 'Current Active Tier' : 'Select Plan Tier'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</EmployerLayout>
|
||||
);
|
||||
}
|
||||
341
resources/js/Pages/Employer/Workers/Index.jsx
Normal file
341
resources/js/Pages/Employer/Workers/Index.jsx
Normal file
@ -0,0 +1,341 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import EmployerLayout from '../../../Layouts/EmployerLayout';
|
||||
import {
|
||||
Search,
|
||||
SlidersHorizontal,
|
||||
CheckCircle2,
|
||||
MapPin,
|
||||
Briefcase,
|
||||
DollarSign,
|
||||
Calendar,
|
||||
Bookmark,
|
||||
RotateCcw,
|
||||
HeartHandshake,
|
||||
Globe2,
|
||||
Sparkles,
|
||||
Filter
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function Index({ initialWorkers, filtersMetadata }) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState('All Categories');
|
||||
const [selectedNationality, setSelectedNationality] = useState('All Nationalities');
|
||||
const [selectedAvailability, setSelectedAvailability] = useState('All Availabilities');
|
||||
const [selectedExperience, setSelectedExperience] = useState('All Experience');
|
||||
const [selectedReligion, setSelectedReligion] = useState('All Religions');
|
||||
const [maxSalary, setMaxSalary] = useState(3000);
|
||||
const [shortlistedIds, setShortlistedIds] = useState([101, 103]);
|
||||
|
||||
const toggleShortlist = (id) => {
|
||||
if (shortlistedIds.includes(id)) {
|
||||
setShortlistedIds(shortlistedIds.filter(i => i !== id));
|
||||
} else {
|
||||
setShortlistedIds([...shortlistedIds, id]);
|
||||
}
|
||||
};
|
||||
|
||||
const resetFilters = () => {
|
||||
setSearchQuery('');
|
||||
setSelectedCategory('All Categories');
|
||||
setSelectedNationality('All Nationalities');
|
||||
setSelectedAvailability('All Availabilities');
|
||||
setSelectedExperience('All Experience');
|
||||
setSelectedReligion('All Religions');
|
||||
setMaxSalary(3000);
|
||||
};
|
||||
|
||||
const filteredWorkers = useMemo(() => {
|
||||
return initialWorkers.filter(worker => {
|
||||
// Search Query
|
||||
if (searchQuery.trim() !== '') {
|
||||
const query = searchQuery.toLowerCase();
|
||||
const matchName = worker.name.toLowerCase().includes(query);
|
||||
const matchBio = worker.bio.toLowerCase().includes(query);
|
||||
const matchSkills = worker.skills.some(s => s.toLowerCase().includes(query));
|
||||
if (!matchName && !matchBio && !matchSkills) return false;
|
||||
}
|
||||
|
||||
// Category
|
||||
if (selectedCategory !== 'All Categories' && worker.category !== selectedCategory) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Nationality
|
||||
if (selectedNationality !== 'All Nationalities' && worker.nationality !== selectedNationality) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Availability
|
||||
if (selectedAvailability !== 'All Availabilities' && worker.availability !== selectedAvailability) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Experience
|
||||
if (selectedExperience !== 'All Experience' && worker.experience !== selectedExperience) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Religion
|
||||
if (selectedReligion !== 'All Religions' && worker.religion !== selectedReligion) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Salary Bracket
|
||||
if (worker.salary > maxSalary) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, [
|
||||
initialWorkers,
|
||||
searchQuery,
|
||||
selectedCategory,
|
||||
selectedNationality,
|
||||
selectedAvailability,
|
||||
selectedExperience,
|
||||
selectedReligion,
|
||||
maxSalary
|
||||
]);
|
||||
|
||||
return (
|
||||
<EmployerLayout title="Find Domestic Workers">
|
||||
<Head title="Find Workers - Employer Portal" />
|
||||
|
||||
<div className="space-y-6 select-none">
|
||||
{/* Search & Top Filters Bar */}
|
||||
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm space-y-4">
|
||||
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
|
||||
{/* Search Input */}
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search by name, bio, or skills (e.g., Cooking, Childcare, Arabic)..."
|
||||
className="w-full pl-11 pr-4 py-3 rounded-xl border border-slate-300 text-sm focus:outline-none focus:ring-2 focus:ring-[#185FA5]/20 focus:border-[#185FA5]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Reset Filters CTA */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={resetFilters}
|
||||
className="flex items-center justify-center space-x-2 px-4 py-3 rounded-xl border border-slate-200 hover:bg-slate-50 text-xs font-semibold text-slate-600 transition-colors flex-shrink-0"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4 text-slate-400" />
|
||||
<span>Reset Filters</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Detailed Filters Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-3 pt-4 border-t border-slate-100">
|
||||
{/* Category Dropdown */}
|
||||
<div>
|
||||
<label className="block text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">Category</label>
|
||||
<select
|
||||
value={selectedCategory}
|
||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-xl border border-slate-200 text-xs bg-slate-50/50 focus:bg-white focus:outline-none focus:border-[#185FA5]"
|
||||
>
|
||||
{filtersMetadata.categories.map(cat => (
|
||||
<option key={cat} value={cat}>{cat}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Nationality Dropdown */}
|
||||
<div>
|
||||
<label className="block text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">Nationality</label>
|
||||
<select
|
||||
value={selectedNationality}
|
||||
onChange={(e) => setSelectedNationality(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-xl border border-slate-200 text-xs bg-slate-50/50 focus:bg-white focus:outline-none focus:border-[#185FA5]"
|
||||
>
|
||||
{filtersMetadata.nationalities.map(nat => (
|
||||
<option key={nat} value={nat}>{nat}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Availability Dropdown */}
|
||||
<div>
|
||||
<label className="block text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">Availability</label>
|
||||
<select
|
||||
value={selectedAvailability}
|
||||
onChange={(e) => setSelectedAvailability(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-xl border border-slate-200 text-xs bg-slate-50/50 focus:bg-white focus:outline-none focus:border-[#185FA5]"
|
||||
>
|
||||
{filtersMetadata.availabilities.map(av => (
|
||||
<option key={av} value={av}>{av}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Experience Dropdown */}
|
||||
<div>
|
||||
<label className="block text-[11px] font-semibold text-slate-500 uppercase tracking-wider mb-1">Experience</label>
|
||||
<select
|
||||
value={selectedExperience}
|
||||
onChange={(e) => setSelectedExperience(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-xl border border-slate-200 text-xs bg-slate-50/50 focus:bg-white focus:outline-none focus:border-[#185FA5]"
|
||||
>
|
||||
{filtersMetadata.experienceLevels.map(exp => (
|
||||
<option key={exp} value={exp}>{exp}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Max Salary Slider */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1 text-[11px]">
|
||||
<span className="font-semibold text-slate-500 uppercase tracking-wider">Max Salary</span>
|
||||
<span className="font-bold text-[#185FA5]">{maxSalary} AED</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="1200"
|
||||
max="3000"
|
||||
step="100"
|
||||
value={maxSalary}
|
||||
onChange={(e) => setMaxSalary(Number(e.target.value))}
|
||||
className="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-[#185FA5]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Results Header */}
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<div className="text-xs font-semibold text-slate-500">
|
||||
Showing <span className="text-slate-900 font-bold">{filteredWorkers.length}</span> domestic workers matching your criteria
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 text-xs font-medium text-slate-600">
|
||||
<Filter className="w-3.5 h-3.5 text-[#185FA5]" />
|
||||
<span>OCR Vetted & Legal</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Workers Grid */}
|
||||
{filteredWorkers.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredWorkers.map((worker) => {
|
||||
const isShortlisted = shortlistedIds.includes(worker.id);
|
||||
|
||||
return (
|
||||
<div key={worker.id} className="bg-white rounded-2xl border border-slate-200 shadow-sm hover:shadow-md transition-all flex flex-col overflow-hidden group">
|
||||
{/* Card Header Banner */}
|
||||
<div className="bg-slate-50 p-6 border-b border-slate-100 flex items-start justify-between relative">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-14 h-14 rounded-full bg-blue-100 text-[#185FA5] flex items-center justify-center font-bold text-xl border border-blue-200 shadow-inner flex-shrink-0">
|
||||
{worker.name.charAt(0)}
|
||||
</div>
|
||||
<div className="truncate">
|
||||
<div className="font-bold text-base text-slate-900 group-hover:text-[#185FA5] transition-colors truncate flex items-center space-x-1.5">
|
||||
<span>{worker.name}</span>
|
||||
{worker.verified && <CheckCircle2 className="w-4 h-4 text-emerald-600 flex-shrink-0" />}
|
||||
</div>
|
||||
<div className="flex items-center space-x-1.5 text-xs text-slate-500 mt-0.5">
|
||||
<Globe2 className="w-3.5 h-3.5 text-slate-400" />
|
||||
<span>{worker.nationality}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Shortlist Toggle Button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleShortlist(worker.id)}
|
||||
className={`p-2 rounded-xl transition-all ${
|
||||
isShortlisted
|
||||
? 'bg-blue-50 text-[#185FA5] hover:bg-blue-100'
|
||||
: 'bg-white text-slate-400 hover:text-[#185FA5] border border-slate-200'
|
||||
}`}
|
||||
>
|
||||
<Bookmark className={`w-5 h-5 ${isShortlisted ? 'fill-[#185FA5]' : ''}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Card Body */}
|
||||
<div className="p-6 flex-1 flex flex-col justify-between space-y-4">
|
||||
<div className="space-y-4">
|
||||
{/* Top Skill Pill & Salary */}
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="px-2.5 py-1 bg-blue-50 text-[#185FA5] font-bold rounded-lg border border-blue-100">
|
||||
{worker.category}
|
||||
</span>
|
||||
<div className="flex items-center space-x-1 font-bold text-slate-800">
|
||||
<DollarSign className="w-4 h-4 text-emerald-600" />
|
||||
<span>{worker.salary} AED / mo</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bio Snippet */}
|
||||
<p className="text-xs text-slate-600 line-clamp-2 leading-relaxed">
|
||||
"{worker.bio}"
|
||||
</p>
|
||||
|
||||
{/* Details Grid */}
|
||||
<div className="grid grid-cols-2 gap-2 p-3 bg-slate-50 rounded-xl text-[11px] text-slate-600 font-medium">
|
||||
<div className="flex items-center space-x-1.5">
|
||||
<Briefcase className="w-3.5 h-3.5 text-slate-400" />
|
||||
<span>Exp: {worker.experience}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1.5 truncate">
|
||||
<Calendar className="w-3.5 h-3.5 text-slate-400" />
|
||||
<span className="truncate">Avail: {worker.availability}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1.5">
|
||||
<HeartHandshake className="w-3.5 h-3.5 text-slate-400" />
|
||||
<span>{worker.religion}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1.5 truncate">
|
||||
<Sparkles className="w-3.5 h-3.5 text-slate-400" />
|
||||
<span className="truncate">Age: {worker.age} yrs</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Skill Tags */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{worker.skills.map(s => (
|
||||
<span key={s} className="text-[10px] bg-slate-100 text-slate-600 px-2 py-0.5 rounded-md font-semibold">
|
||||
{s}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* View Profile Button */}
|
||||
<Link
|
||||
href={`/employer/workers/${worker.id}`}
|
||||
className="w-full bg-[#185FA5] hover:bg-[#144f8a] text-white rounded-xl h-10 font-bold text-xs flex items-center justify-center transition-colors shadow-sm"
|
||||
>
|
||||
View Full Profile
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-16 bg-white rounded-2xl border border-slate-200 shadow-sm space-y-3">
|
||||
<SlidersHorizontal className="w-12 h-12 text-slate-300 mx-auto" />
|
||||
<div className="text-base font-bold text-slate-800">No workers match your filter criteria</div>
|
||||
<p className="text-xs text-slate-500 max-w-sm mx-auto">
|
||||
Try adjusting your salary slider, broadening your category selection, or resetting all filters.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={resetFilters}
|
||||
className="mt-2 bg-[#185FA5] text-white px-5 py-2.5 rounded-xl text-xs font-bold shadow-sm hover:bg-[#144f8a] transition-colors"
|
||||
>
|
||||
Reset All Filters
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</EmployerLayout>
|
||||
);
|
||||
}
|
||||
282
resources/js/Pages/Employer/Workers/Show.jsx
Normal file
282
resources/js/Pages/Employer/Workers/Show.jsx
Normal file
@ -0,0 +1,282 @@
|
||||
import React from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import EmployerLayout from '../../../Layouts/EmployerLayout';
|
||||
import {
|
||||
CheckCircle2,
|
||||
Globe2,
|
||||
Briefcase,
|
||||
DollarSign,
|
||||
Calendar,
|
||||
HeartHandshake,
|
||||
Languages,
|
||||
MessageSquare,
|
||||
Bookmark,
|
||||
ArrowLeft,
|
||||
Sparkles,
|
||||
FileText,
|
||||
ShieldCheck,
|
||||
Search
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function Show({ worker }) {
|
||||
return (
|
||||
<EmployerLayout title="Worker Profile">
|
||||
<Head title={`${worker.name} - Worker Profile`} />
|
||||
|
||||
<div className="space-y-6 select-none w-full">
|
||||
<Link
|
||||
href="/employer/workers"
|
||||
className="inline-flex items-center space-x-2 text-xs font-bold text-slate-600 hover:text-[#185FA5] transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back to Find Workers</span>
|
||||
</Link>
|
||||
|
||||
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
|
||||
{/* Header Card */}
|
||||
<div className="bg-gradient-to-r from-[#185FA5] to-blue-700 p-8 text-white flex flex-col sm:flex-row items-start sm:items-center justify-between gap-6 relative overflow-hidden">
|
||||
<div className="absolute -right-10 -bottom-10 w-48 h-48 bg-white/10 rounded-full blur-2xl pointer-events-none" />
|
||||
|
||||
<div className="flex items-center space-x-5 z-10">
|
||||
<div className="w-20 h-20 rounded-full bg-white text-[#185FA5] flex items-center justify-center font-bold text-3xl shadow-md border-2 border-white flex-shrink-0">
|
||||
{worker.name.charAt(0)}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h1 className="text-2xl sm:text-3xl font-extrabold tracking-tight">{worker.name}</h1>
|
||||
{worker.verified && (
|
||||
<span className="bg-emerald-500 text-white p-0.5 rounded-full shadow-sm" title="OCR Verified">
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 text-blue-100 text-sm font-medium">
|
||||
<Globe2 className="w-4 h-4" />
|
||||
<span>{worker.nationality}</span>
|
||||
<span>•</span>
|
||||
<span>{worker.age} Years Old</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3 z-10 w-full sm:w-auto justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className="bg-white/10 hover:bg-white/20 backdrop-blur-sm border border-white/20 text-white p-3 rounded-xl shadow-sm transition-colors"
|
||||
title="Shortlist Worker"
|
||||
>
|
||||
<Bookmark className="w-5 h-5" />
|
||||
</button>
|
||||
<Link
|
||||
href={`/employer/workers/${worker.id}/hire`}
|
||||
className="bg-[#185FA5] text-white hover:bg-[#144f8a] px-8 py-3 rounded-xl font-bold text-sm shadow-lg shadow-blue-500/20 transition-all flex items-center justify-center space-x-2 flex-1 sm:flex-initial scale-105 active:scale-100"
|
||||
>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
<span>Hire Now</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/employer/messages"
|
||||
className="bg-white text-slate-600 border border-slate-200 hover:bg-slate-50 px-6 py-3 rounded-xl font-bold text-sm shadow-sm transition-colors flex items-center justify-center space-x-2 flex-1 sm:flex-initial"
|
||||
>
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
<span>Message</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Details Grid */}
|
||||
<div className="p-8 grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Left Column: Quick Stats */}
|
||||
<div className="space-y-6">
|
||||
<div className="bg-slate-50 p-6 rounded-2xl border border-slate-200 space-y-4">
|
||||
<h3 className="font-bold text-sm text-slate-800 uppercase tracking-wider">Verification & Cost</h3>
|
||||
|
||||
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
|
||||
<div className="flex items-center space-x-2 text-xs text-slate-600 font-medium">
|
||||
<DollarSign className="w-4 h-4 text-emerald-600" />
|
||||
<span>Monthly Salary</span>
|
||||
</div>
|
||||
<span className="font-bold text-slate-900 text-sm">{worker.salary} AED</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
|
||||
<div className="flex items-center space-x-2 text-xs text-slate-600 font-medium">
|
||||
<Calendar className="w-4 h-4 text-[#185FA5]" />
|
||||
<span>Availability</span>
|
||||
</div>
|
||||
<span className="font-bold text-[#185FA5] bg-blue-50 px-2 py-0.5 rounded text-xs">
|
||||
{worker.availability}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
|
||||
<div className="flex items-center space-x-2 text-xs text-slate-600 font-medium">
|
||||
<Briefcase className="w-4 h-4 text-amber-600" />
|
||||
<span>Experience</span>
|
||||
</div>
|
||||
<span className="font-bold text-slate-900 text-xs">{worker.experience}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pb-3 border-b border-slate-200">
|
||||
<div className="flex items-center space-x-2 text-xs text-slate-600 font-medium">
|
||||
<HeartHandshake className="w-4 h-4 text-purple-600" />
|
||||
<span>Religion</span>
|
||||
</div>
|
||||
<span className="font-bold text-slate-900 text-xs">{worker.religion}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2 text-xs text-slate-600 font-medium">
|
||||
<ShieldCheck className="w-4 h-4 text-emerald-600" />
|
||||
<span>Medical Cert</span>
|
||||
</div>
|
||||
<span className="font-bold text-emerald-700 bg-emerald-50 px-2 py-0.5 rounded text-xs">
|
||||
Passed
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Languages Spoken */}
|
||||
<div className="bg-slate-50 p-6 rounded-2xl border border-slate-200 space-y-3">
|
||||
<div className="flex items-center space-x-2 font-bold text-sm text-slate-800">
|
||||
<Languages className="w-4 h-4 text-[#185FA5]" />
|
||||
<span>Languages Spoken</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{worker.languages.map(lang => (
|
||||
<span key={lang} className="bg-white border border-slate-200 text-slate-800 px-3 py-1 rounded-xl text-xs font-bold shadow-sm">
|
||||
{lang}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Bio & Skills */}
|
||||
<div className="lg:col-span-2 space-y-8">
|
||||
{/* Professional Summary */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-bold text-base text-slate-900">Professional Summary</h3>
|
||||
<p className="text-sm text-slate-600 leading-relaxed bg-slate-50 p-6 rounded-2xl border border-slate-200 italic">
|
||||
"{worker.bio}"
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Verified Skills & Competencies */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-bold text-base text-slate-900">Verified Skills & Competencies</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{worker.skills.map(skill => (
|
||||
<div key={skill} className="flex items-center space-x-3 p-3.5 bg-white border border-slate-200 rounded-xl shadow-sm">
|
||||
<CheckCircle2 className="w-5 h-5 text-emerald-600 flex-shrink-0" />
|
||||
<span className="font-bold text-xs text-slate-800">{skill}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Verified Documents Section */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-bold text-base text-slate-900 flex items-center space-x-2">
|
||||
<ShieldCheck className="w-5 h-5 text-emerald-600" />
|
||||
<span>Verified Legal Documents</span>
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Document Card: Passport */}
|
||||
<div className="bg-white border border-slate-200 rounded-2xl overflow-hidden shadow-sm hover:shadow-md transition-shadow">
|
||||
<div className="bg-slate-50 px-4 py-3 border-b border-slate-100 flex items-center justify-between">
|
||||
<span className="text-[11px] font-black text-slate-500 uppercase tracking-widest">Passport (Verified)</span>
|
||||
<FileText className="w-4 h-4 text-slate-400" />
|
||||
</div>
|
||||
<div className="p-4 flex space-x-4">
|
||||
{/* Document Thumbnail Placeholder */}
|
||||
<div className="w-24 h-32 bg-slate-100 rounded-lg flex-shrink-0 border border-slate-200 flex flex-col items-center justify-center p-2 relative group cursor-zoom-in">
|
||||
<div className="w-full h-full bg-blue-50/50 rounded flex items-center justify-center">
|
||||
<Search className="w-6 h-6 text-blue-200 group-hover:text-blue-400 transition-colors" />
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-slate-900/0 group-hover:bg-slate-900/5 transition-colors" />
|
||||
<span className="absolute bottom-2 text-[8px] font-bold text-slate-400 uppercase">Click to view</span>
|
||||
</div>
|
||||
|
||||
{/* Extracted Data */}
|
||||
<div className="flex-1 space-y-3">
|
||||
<div>
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">Document Number</div>
|
||||
<div className="text-xs font-bold text-slate-800">L82739102-UAE</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">Issue Date</div>
|
||||
<div className="text-xs font-bold text-slate-800">12 Jan 2021</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">Expiry Date</div>
|
||||
<div className="text-xs font-bold text-emerald-600">11 Jan 2031</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">OCR Match Accuracy</div>
|
||||
<div className="flex items-center space-x-2 mt-1">
|
||||
<div className="flex-1 h-1 bg-slate-100 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-emerald-500 w-[98%]" />
|
||||
</div>
|
||||
<span className="text-[10px] font-black text-emerald-600">98%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Document Card: Visa */}
|
||||
<div className="bg-white border border-slate-200 rounded-2xl overflow-hidden shadow-sm hover:shadow-md transition-shadow">
|
||||
<div className="bg-slate-50 px-4 py-3 border-b border-slate-100 flex items-center justify-between">
|
||||
<span className="text-[11px] font-black text-slate-500 uppercase tracking-widest">Entry Visa (Verified)</span>
|
||||
<ShieldCheck className="w-4 h-4 text-emerald-500" />
|
||||
</div>
|
||||
<div className="p-4 flex space-x-4">
|
||||
{/* Document Thumbnail Placeholder */}
|
||||
<div className="w-24 h-32 bg-slate-100 rounded-lg flex-shrink-0 border border-slate-200 flex flex-col items-center justify-center p-2 relative group cursor-zoom-in">
|
||||
<div className="w-full h-full bg-blue-50/50 rounded flex items-center justify-center">
|
||||
<Search className="w-6 h-6 text-blue-200 group-hover:text-blue-400 transition-colors" />
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-slate-900/0 group-hover:bg-slate-900/5 transition-colors" />
|
||||
<span className="absolute bottom-2 text-[8px] font-bold text-slate-400 uppercase">Click to view</span>
|
||||
</div>
|
||||
|
||||
{/* Extracted Data */}
|
||||
<div className="flex-1 space-y-3">
|
||||
<div>
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">Visa Reference</div>
|
||||
<div className="text-xs font-bold text-slate-800">UAE-2024-918273</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">Visa Type</div>
|
||||
<div className="text-xs font-bold text-slate-800">Employment</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">Expiry Date</div>
|
||||
<div className="text-xs font-bold text-amber-600">30 Dec 2026</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase tracking-tighter">Background Check</div>
|
||||
<div className="flex items-center space-x-1.5 mt-1">
|
||||
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-600" />
|
||||
<span className="text-[10px] font-bold text-slate-700">No Criminal Record Found</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</EmployerLayout>
|
||||
);
|
||||
}
|
||||
90
resources/js/Pages/Mobile/EmployerAnnouncements.jsx
Normal file
90
resources/js/Pages/Mobile/EmployerAnnouncements.jsx
Normal file
@ -0,0 +1,90 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import EmployerMobileLayout from './Layouts/EmployerMobileLayout';
|
||||
import {
|
||||
Megaphone,
|
||||
Calendar,
|
||||
ArrowRight,
|
||||
Search,
|
||||
Plus,
|
||||
X
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function EmployerAnnouncements() {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const announcements = [
|
||||
{ id: 1, title: 'Ramadan Working Hours Update', date: 'May 12, 2026', content: 'Please note the revised working hours for all domestic staff during the upcoming Holy Month of Ramadan...', priority: 'High' },
|
||||
{ id: 2, title: 'New Visa Regulations 2026', date: 'May 08, 2026', content: 'The Ministry of Human Resources has announced new updates regarding domestic worker visa renewals...', priority: 'Normal' },
|
||||
{ id: 3, title: 'Marketplace Service Maintenance', date: 'May 05, 2026', content: 'Our platform will undergo scheduled maintenance on Sunday from 2:00 AM to 4:00 AM...', priority: 'Low' },
|
||||
];
|
||||
|
||||
return (
|
||||
<EmployerMobileLayout>
|
||||
<Head title="Announcements" />
|
||||
|
||||
<div className="p-6 space-y-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Broadcasts</h1>
|
||||
<p className="text-sm text-slate-500 font-medium">Important updates & announcements</p>
|
||||
</div>
|
||||
<button className="w-12 h-12 bg-[#141B34] text-white rounded-2xl flex items-center justify-center shadow-lg active:scale-95 transition-all">
|
||||
<Plus className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative group">
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300 group-focus-within:text-[#141B34] transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search announcements..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-12 pr-4 py-4 bg-white border border-slate-200 rounded-2xl text-sm focus:ring-4 focus:ring-slate-100 focus:border-slate-300 outline-none transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Announcement List */}
|
||||
<div className="space-y-4 pb-10">
|
||||
{announcements.map((item) => (
|
||||
<div key={item.id} className="bg-white p-6 rounded-3xl border border-slate-100 shadow-sm space-y-4 active:scale-[0.98] transition-all group">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-blue-50 flex items-center justify-center text-blue-600">
|
||||
<Megaphone className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<h4 className="font-bold text-slate-900 text-sm leading-tight">{item.title}</h4>
|
||||
<div className="flex items-center space-x-1 text-[10px] font-bold text-slate-400 uppercase tracking-widest">
|
||||
<Calendar className="w-3 h-3" />
|
||||
<span>{item.date}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-500 leading-relaxed font-medium line-clamp-2">
|
||||
{item.content}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-between pt-2 border-t border-slate-50">
|
||||
<div className={`px-2.5 py-1 rounded-lg text-[9px] font-bold uppercase tracking-widest ${
|
||||
item.priority === 'High' ? 'bg-rose-50 text-rose-500 border border-rose-100' : 'bg-slate-50 text-slate-400 border border-slate-100'
|
||||
}`}>
|
||||
{item.priority} Priority
|
||||
</div>
|
||||
<button className="text-xs font-bold text-[#141B34] flex items-center space-x-1 group-hover:translate-x-1 transition-transform">
|
||||
<span>Read More</span>
|
||||
<ArrowRight className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</EmployerMobileLayout>
|
||||
);
|
||||
}
|
||||
135
resources/js/Pages/Mobile/EmployerCandidates.jsx
Normal file
135
resources/js/Pages/Mobile/EmployerCandidates.jsx
Normal file
@ -0,0 +1,135 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import EmployerMobileLayout from './Layouts/EmployerMobileLayout';
|
||||
import {
|
||||
Users,
|
||||
Clock,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Search,
|
||||
Filter,
|
||||
MessageSquare,
|
||||
MoreHorizontal
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function EmployerCandidates() {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const stats = [
|
||||
{ label: 'Total Candidates', value: 5, icon: Users, color: 'bg-blue-50 text-blue-600' },
|
||||
{ label: 'Reviewing', value: 3, icon: Clock, color: 'bg-orange-50 text-orange-500' },
|
||||
{ label: 'Hired', value: 1, icon: CheckCircle2, color: 'bg-emerald-50 text-emerald-600' },
|
||||
{ label: 'Rejected', value: 1, icon: XCircle, color: 'bg-rose-50 text-rose-500' },
|
||||
];
|
||||
|
||||
const candidates = [
|
||||
{ id: 1, name: 'Sarah Connor', country: 'Philippines', role: 'Site Supervisor', salary: '4,500 AED', status: 'Reviewing' },
|
||||
{ id: 2, name: 'Maria Garcia', country: 'Kenya', role: 'Housemaid', salary: '2,500 AED', status: 'Hired' },
|
||||
{ id: 3, name: 'Elena Gilbert', country: 'Indonesia', role: 'Nanny', salary: '3,000 AED', status: 'Rejected' },
|
||||
];
|
||||
|
||||
const getStatusStyles = (status) => {
|
||||
switch (status) {
|
||||
case 'Reviewing': return 'bg-orange-50 text-orange-600 border-orange-100';
|
||||
case 'Hired': return 'bg-emerald-50 text-emerald-600 border-emerald-100';
|
||||
case 'Rejected': return 'bg-rose-50 text-rose-600 border-rose-100';
|
||||
default: return 'bg-slate-50 text-slate-600 border-slate-100';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusDot = (status) => {
|
||||
switch (status) {
|
||||
case 'Reviewing': return 'bg-orange-500';
|
||||
case 'Hired': return 'bg-emerald-500';
|
||||
case 'Rejected': return 'bg-rose-500';
|
||||
default: return 'bg-slate-500';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<EmployerMobileLayout>
|
||||
<Head title="Candidates" />
|
||||
|
||||
<div className="p-6 space-y-8">
|
||||
{/* Page Title */}
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Candidates</h1>
|
||||
<p className="text-sm text-slate-500 font-medium">Manage your workforce recruitment pipeline</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid 2x2 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{stats.map((stat, i) => (
|
||||
<div key={i} className="bg-white p-5 rounded-2xl border border-slate-100 shadow-sm space-y-3">
|
||||
<div className={`w-10 h-10 rounded-xl flex items-center justify-center ${stat.color}`}>
|
||||
<stat.icon className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-xl font-bold">{stat.value}</div>
|
||||
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-widest leading-none">{stat.label}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Search & Filter */}
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="relative flex-1 group">
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300 group-focus-within:text-[#141B34] transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search candidates..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-12 pr-4 py-4 bg-white border border-slate-200 rounded-2xl text-sm focus:ring-4 focus:ring-slate-100 focus:border-slate-300 outline-none transition-all"
|
||||
/>
|
||||
</div>
|
||||
<button className="w-14 h-14 bg-white rounded-2xl flex items-center justify-center text-slate-400 border border-slate-200 active:scale-95 transition-all">
|
||||
<Filter className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Candidate List */}
|
||||
<div className="space-y-4 pb-10">
|
||||
{candidates.map((candidate) => (
|
||||
<div key={candidate.id} className="bg-white p-5 rounded-3xl border border-slate-100 shadow-sm space-y-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-14 h-14 rounded-full bg-slate-100 border border-slate-200 flex items-center justify-center text-[#141B34] font-bold text-xl">
|
||||
{candidate.name.charAt(0)}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h4 className="font-bold text-slate-900 text-base">{candidate.name}</h4>
|
||||
<div className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">{candidate.country}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<button className="w-9 h-9 rounded-xl bg-slate-50 flex items-center justify-center text-slate-400 border border-slate-100">
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
</button>
|
||||
<button className="w-9 h-9 rounded-xl bg-slate-50 flex items-center justify-center text-slate-400 border border-slate-100">
|
||||
<MoreHorizontal className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-2">
|
||||
<span className="px-3 py-1.5 bg-slate-50 text-slate-600 text-[10px] font-bold uppercase tracking-widest rounded-lg border border-slate-100">
|
||||
{candidate.role}
|
||||
</span>
|
||||
<div className="text-xs font-bold text-slate-900 ml-1">{candidate.salary}</div>
|
||||
</div>
|
||||
|
||||
<div className={`flex items-center space-x-2 px-3 py-1.5 rounded-full border text-[10px] font-bold uppercase tracking-widest ${getStatusStyles(candidate.status)}`}>
|
||||
<div className={`w-1.5 h-1.5 rounded-full ${getStatusDot(candidate.status)}`} />
|
||||
<span>{candidate.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</EmployerMobileLayout>
|
||||
);
|
||||
}
|
||||
147
resources/js/Pages/Mobile/EmployerChatDetail.jsx
Normal file
147
resources/js/Pages/Mobile/EmployerChatDetail.jsx
Normal file
@ -0,0 +1,147 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import {
|
||||
ChevronLeft,
|
||||
MoreVertical,
|
||||
Send,
|
||||
Paperclip,
|
||||
Image as ImageIcon,
|
||||
Smile,
|
||||
CheckCheck,
|
||||
Mic
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function EmployerChatDetail({ id }) {
|
||||
const [message, setMessage] = useState('');
|
||||
const [messages, setMessages] = useState([
|
||||
{ id: 1, sender: 'them', text: 'Hello! Is the site supervisor position still available?', time: '10:30 AM' },
|
||||
{ id: 2, sender: 'me', text: 'Hi Sarah! Yes, it is. We are currently reviewing applications.', time: '10:32 AM' },
|
||||
{ id: 3, sender: 'them', text: 'Great! I have over 8 years of experience in Dubai. Would you like me to send my CV?', time: '10:35 AM' },
|
||||
{ id: 4, sender: 'me', text: 'That would be excellent. Please send it over here.', time: '10:36 AM' },
|
||||
{ id: 5, sender: 'them', text: 'Sure, here it is. I also have pediatric first aid certification.', time: '10:40 AM' },
|
||||
]);
|
||||
|
||||
const messagesEndRef = useRef(null);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
const handleSend = (e) => {
|
||||
if (e) e.preventDefault();
|
||||
if (!message.trim()) return;
|
||||
|
||||
const newMessage = {
|
||||
id: messages.length + 1,
|
||||
sender: 'me',
|
||||
text: message,
|
||||
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
};
|
||||
|
||||
setMessages([...messages, newMessage]);
|
||||
setMessage('');
|
||||
};
|
||||
|
||||
// Mock data for the specific chat
|
||||
const chatInfo = {
|
||||
id: id,
|
||||
name: 'Sarah Connor',
|
||||
initial: 'S',
|
||||
online: true,
|
||||
role: 'Site Supervisor'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8FAFC] flex flex-col max-w-md mx-auto relative font-sans text-slate-900">
|
||||
<Head title={`Chat with ${chatInfo.name}`} />
|
||||
|
||||
{/* Chat Header */}
|
||||
<div className="bg-white px-6 pt-12 pb-6 flex items-center justify-between border-b border-slate-100 sticky top-0 z-50">
|
||||
<div className="flex items-center space-x-4">
|
||||
<button onClick={() => window.history.back()} className="p-2 bg-slate-50 rounded-xl text-slate-400 border border-slate-100">
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="relative">
|
||||
<div className="w-10 h-10 rounded-full bg-[#D8F3FF] flex items-center justify-center text-[#185FA5] font-bold text-sm">
|
||||
{chatInfo.initial}
|
||||
</div>
|
||||
{chatInfo.online && (
|
||||
<div className="absolute bottom-0 right-0 w-3 h-3 bg-emerald-500 border-2 border-white rounded-full shadow-sm" />
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<h4 className="font-bold text-sm leading-none">{chatInfo.name}</h4>
|
||||
<p className="text-[10px] font-bold text-emerald-500 uppercase tracking-widest">Online Now</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button className="p-2.5 text-slate-400 hover:text-[#185FA5] transition-colors">
|
||||
<MoreVertical className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages Area */}
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-6 no-scrollbar pb-32">
|
||||
<div className="flex justify-center">
|
||||
<span className="px-4 py-1.5 bg-slate-100 rounded-full text-[10px] font-bold text-slate-400 uppercase tracking-widest">Today</span>
|
||||
</div>
|
||||
|
||||
{messages.map((msg) => (
|
||||
<div key={msg.id} className={`flex ${msg.sender === 'me' ? 'justify-end' : 'justify-start'}`}>
|
||||
<div className={`max-w-[80%] space-y-1.5`}>
|
||||
<div className={`px-5 py-4 rounded-[28px] text-[13px] font-medium leading-relaxed shadow-sm ${
|
||||
msg.sender === 'me'
|
||||
? 'bg-[#185FA5] text-white rounded-tr-none'
|
||||
: 'bg-white text-slate-600 rounded-tl-none border border-slate-100'
|
||||
}`}>
|
||||
{msg.text}
|
||||
</div>
|
||||
<div className={`flex items-center space-x-1.5 px-2 ${msg.sender === 'me' ? 'justify-end' : 'justify-start'}`}>
|
||||
<span className="text-[9px] font-bold text-slate-300 uppercase tracking-widest">{msg.time}</span>
|
||||
{msg.sender === 'me' && <CheckCheck className="w-3 h-3 text-emerald-500" />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input Area */}
|
||||
<form onSubmit={handleSend} className="fixed bottom-0 left-0 right-0 max-w-md mx-auto p-6 bg-white/80 backdrop-blur-xl border-t border-slate-100">
|
||||
<div className="flex items-center space-x-3 bg-slate-50 border border-slate-100 rounded-[28px] px-4 py-2">
|
||||
<button type="button" className="p-2 text-slate-300 hover:text-[#185FA5] transition-colors">
|
||||
<Paperclip className="w-5 h-5" />
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Type a message..."
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
className="flex-1 bg-transparent border-none text-sm font-medium focus:ring-0 placeholder-slate-300 text-slate-700 h-12"
|
||||
/>
|
||||
<div className="flex items-center space-x-1">
|
||||
<button type="button" className="p-2 text-slate-300">
|
||||
<Smile className="w-5 h-5" />
|
||||
</button>
|
||||
{message.trim() ? (
|
||||
<button type="submit" className="w-10 h-10 bg-[#185FA5] text-white rounded-full flex items-center justify-center shadow-lg shadow-blue-500/30 active:scale-90 transition-transform">
|
||||
<Send className="w-4 h-4 ml-0.5" />
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" className="w-10 h-10 bg-slate-100 text-[#185FA5] rounded-full flex items-center justify-center active:scale-90 transition-transform">
|
||||
<Mic className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
100
resources/js/Pages/Mobile/EmployerForgotPassword.jsx
Normal file
100
resources/js/Pages/Mobile/EmployerForgotPassword.jsx
Normal file
@ -0,0 +1,100 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import {
|
||||
ArrowLeft,
|
||||
Loader2,
|
||||
Mail,
|
||||
ArrowRight,
|
||||
CheckCircle2
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function EmployerForgotPassword() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [isSent, setIsSent] = useState(false);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
setProcessing(true);
|
||||
setTimeout(() => {
|
||||
setIsSent(true);
|
||||
setProcessing(false);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white font-sans text-slate-900 flex flex-col max-w-md mx-auto">
|
||||
<Head title="Forgot Password" />
|
||||
|
||||
<div className="px-6 pt-12 pb-6">
|
||||
<button onClick={() => window.history.back()} className="p-2 -ml-2 text-slate-600">
|
||||
<ArrowLeft className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 px-8 pt-4">
|
||||
{!isSent ? (
|
||||
<div className="space-y-10">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-3xl font-bold tracking-tight">Forgot password</h1>
|
||||
<p className="text-slate-500 text-sm">No worries, we'll send you reset instructions</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-8">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-semibold text-slate-700 ml-1">Email</label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Enter your email"
|
||||
className="w-full pl-12 pr-4 py-4 bg-slate-50 border border-slate-200 rounded-2xl text-sm focus:bg-white focus:ring-2 focus:ring-[#185FA5]/10 focus:border-[#185FA5] outline-none transition-all"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={processing}
|
||||
className="w-full h-16 bg-[#185FA5] text-white rounded-2xl font-bold text-base shadow-lg shadow-blue-500/20 flex items-center justify-center space-x-2 active:scale-[0.98] transition-all"
|
||||
>
|
||||
{processing ? <Loader2 className="w-6 h-6 animate-spin" /> : (
|
||||
<>
|
||||
<span>Reset password</span>
|
||||
<ArrowRight className="w-5 h-5" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-8 text-center py-10">
|
||||
<div className="flex justify-center">
|
||||
<div className="w-20 h-20 bg-blue-50 rounded-full flex items-center justify-center text-[#185FA5]">
|
||||
<CheckCircle2 className="w-10 h-10" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-2xl font-bold tracking-tight">Check your email</h2>
|
||||
<p className="text-sm text-slate-500 leading-relaxed">
|
||||
We've sent a password reset link to <br />
|
||||
<span className="font-bold text-slate-900">{email}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="pt-4">
|
||||
<Link
|
||||
href="/mobile/employer/login"
|
||||
className="font-bold text-[#185FA5] hover:text-[#144f8a] underline underline-offset-4 text-sm"
|
||||
>
|
||||
Back to login
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
89
resources/js/Pages/Mobile/EmployerHome.jsx
Normal file
89
resources/js/Pages/Mobile/EmployerHome.jsx
Normal file
@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import EmployerMobileLayout from './Layouts/EmployerMobileLayout';
|
||||
import {
|
||||
Bookmark,
|
||||
MessageSquare,
|
||||
Calendar,
|
||||
CreditCard,
|
||||
ArrowRight,
|
||||
TrendingUp,
|
||||
Users
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function EmployerHome() {
|
||||
const stats = [
|
||||
{ label: 'Shortlisted', value: 4, icon: Bookmark, color: 'bg-blue-50 text-blue-600' },
|
||||
{ label: 'Messages Sent', value: 18, icon: MessageSquare, color: 'bg-emerald-50 text-emerald-600' },
|
||||
{ label: 'Days Remaining', value: 24, icon: Calendar, color: 'bg-amber-50 text-amber-500' },
|
||||
{ label: 'Total Hires', value: 12, icon: Users, color: 'bg-indigo-50 text-indigo-600' },
|
||||
];
|
||||
|
||||
return (
|
||||
<EmployerMobileLayout>
|
||||
<Head title="Dashboard" />
|
||||
|
||||
<div className="p-6 space-y-8">
|
||||
{/* Welcome Section */}
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-bold tracking-tight text-slate-900">Dashboard</h1>
|
||||
<p className="text-sm text-slate-500 font-medium">Welcome back, John Doe</p>
|
||||
</div>
|
||||
|
||||
{/* Plan Status Card */}
|
||||
<div className="bg-[#141B34] rounded-[32px] p-8 text-white relative overflow-hidden shadow-2xl">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-white/10 rounded-full -mr-16 -mt-16 blur-2xl" />
|
||||
<div className="space-y-6 relative z-10">
|
||||
<div className="inline-flex items-center space-x-2 bg-white/10 px-3 py-1.5 rounded-full border border-white/10 text-[10px] font-bold uppercase tracking-widest">
|
||||
<div className="w-1.5 h-1.5 bg-emerald-400 rounded-full" />
|
||||
<span>Premium Employer Pass</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-3xl font-bold">24 Days</h2>
|
||||
<p className="text-slate-400 text-xs font-medium uppercase tracking-widest">Remaining in your plan</p>
|
||||
</div>
|
||||
<Link href="/mobile/employer/subscription" className="w-full h-14 bg-white text-[#141B34] rounded-2xl font-bold flex items-center justify-center space-x-2 shadow-xl active:scale-[0.98] transition-all text-xs uppercase tracking-widest">
|
||||
<span>Upgrade Plan</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Stacked */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-[10px] font-bold text-slate-400 uppercase tracking-widest ml-1">Key Statistics</h3>
|
||||
<div className="space-y-3">
|
||||
{stats.map((stat, i) => (
|
||||
<div key={i} className="bg-white p-5 rounded-2xl border border-slate-100 shadow-sm flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${stat.color}`}>
|
||||
<stat.icon className="w-6 h-6" />
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{stat.label}</div>
|
||||
<div className="text-xl font-bold text-slate-900">{stat.value}</div>
|
||||
</div>
|
||||
</div>
|
||||
<TrendingUp className="w-5 h-5 text-emerald-500 opacity-20" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Placeholder for Analytics Chart (Full Width) */}
|
||||
<div className="space-y-4 pb-10">
|
||||
<h3 className="text-[10px] font-bold text-slate-400 uppercase tracking-widest ml-1">Recruitment Analytics</h3>
|
||||
<div className="bg-white p-6 rounded-3xl border border-slate-100 shadow-sm h-64 flex flex-col items-center justify-center text-center space-y-4">
|
||||
<div className="w-16 h-16 bg-slate-50 rounded-full flex items-center justify-center text-slate-200">
|
||||
<TrendingUp className="w-8 h-8" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-bold text-slate-900">Activity Chart</p>
|
||||
<p className="text-xs text-slate-400 font-medium">Visualizing your hiring trends over 30 days</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</EmployerMobileLayout>
|
||||
);
|
||||
}
|
||||
111
resources/js/Pages/Mobile/EmployerLogin.jsx
Normal file
111
resources/js/Pages/Mobile/EmployerLogin.jsx
Normal file
@ -0,0 +1,111 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useForm, Head, Link } from '@inertiajs/react';
|
||||
import {
|
||||
ArrowLeft,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Loader2,
|
||||
Mail,
|
||||
Lock
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function EmployerLogin() {
|
||||
const { data, setData, post, processing, errors } = useForm({
|
||||
email: '',
|
||||
password: '',
|
||||
remember: false,
|
||||
source: 'mobile',
|
||||
});
|
||||
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
post('/employer/login');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white font-sans text-slate-900 flex flex-col max-w-md mx-auto">
|
||||
<Head title="Employer Login" />
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-6 pt-12 pb-6 flex items-center">
|
||||
<button onClick={() => window.history.back()} className="p-2 -ml-2 text-slate-600 hover:text-slate-900">
|
||||
<ArrowLeft className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 px-8 pt-4 space-y-10">
|
||||
{/* Title */}
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-3xl font-bold tracking-tight">Welcome back</h1>
|
||||
<p className="text-slate-500 text-sm">Please enter your details to sign in</p>
|
||||
</div>
|
||||
|
||||
{/* Login Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-semibold text-slate-700 ml-1">Email</label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
|
||||
<input
|
||||
type="email"
|
||||
value={data.email}
|
||||
onChange={(e) => setData('email', e.target.value)}
|
||||
placeholder="Enter your email"
|
||||
className="w-full pl-12 pr-4 py-4 bg-slate-50 border border-slate-200 rounded-2xl text-sm focus:bg-white focus:ring-2 focus:ring-[#185FA5]/10 focus:border-[#185FA5] transition-all outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{errors.email && <p className="text-xs text-red-500 mt-1 ml-1">{errors.email}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between ml-1">
|
||||
<label className="text-sm font-semibold text-slate-700">Password</label>
|
||||
<Link href="/mobile/employer/forgot-password" size="sm" className="text-sm font-medium text-[#185FA5] hover:text-[#144f8a]">Forgot?</Link>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={data.password}
|
||||
onChange={(e) => setData('password', e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className="w-full pl-12 pr-12 py-4 bg-slate-50 border border-slate-200 rounded-2xl text-sm focus:bg-white focus:ring-2 focus:ring-[#185FA5]/10 focus:border-[#185FA5] transition-all outline-none"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600"
|
||||
>
|
||||
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={processing}
|
||||
className="w-full h-16 bg-[#185FA5] text-white rounded-2xl font-bold text-base shadow-lg shadow-blue-500/20 flex items-center justify-center space-x-2 active:scale-[0.98] transition-all disabled:opacity-70 mt-8"
|
||||
>
|
||||
{processing ? <Loader2 className="w-6 h-6 animate-spin" /> : <span>Sign in</span>}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="text-center pt-4 pb-10">
|
||||
<p className="text-sm text-slate-500">
|
||||
Don't have an account?{' '}
|
||||
<Link href="/mobile/employer/register" className="font-bold text-[#185FA5] hover:text-[#144f8a] underline underline-offset-4">
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
79
resources/js/Pages/Mobile/EmployerMessages.jsx
Normal file
79
resources/js/Pages/Mobile/EmployerMessages.jsx
Normal file
@ -0,0 +1,79 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import EmployerMobileLayout from './Layouts/EmployerMobileLayout';
|
||||
import { Search, MoreHorizontal, CheckCheck } from 'lucide-react';
|
||||
|
||||
export default function EmployerMessages() {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const chats = [
|
||||
{ id: 1, name: 'Sarah Connor', preview: 'Is the site supervisor position still available?', time: '2m ago', unread: 2, online: true },
|
||||
{ id: 2, name: 'Marketplace Support', preview: 'Your account verification is complete.', time: '1h ago', unread: 0, online: true },
|
||||
{ id: 3, name: 'Maria Garcia', preview: 'I can start from next Monday. Looking forward to it!', time: '3h ago', unread: 0, online: false },
|
||||
{ id: 4, name: 'Elena Gilbert', preview: 'Thank you for the opportunity.', time: 'Yesterday', unread: 0, online: false },
|
||||
{ id: 5, name: 'John Smith', preview: 'Attached are my certificates for review.', time: 'Yesterday', unread: 0, online: false },
|
||||
];
|
||||
|
||||
return (
|
||||
<EmployerMobileLayout>
|
||||
<Head title="Messages" />
|
||||
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Search Bar */}
|
||||
<div className="p-6 pb-2">
|
||||
<div className="relative group">
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300 group-focus-within:text-[#141B34] transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search conversations..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-12 pr-4 py-4 bg-white border border-slate-200 rounded-2xl text-sm focus:ring-4 focus:ring-slate-100 focus:border-slate-300 outline-none transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chat List */}
|
||||
<div className="flex-1 overflow-y-auto no-scrollbar pb-10">
|
||||
<div className="divide-y divide-slate-100">
|
||||
{chats.map((chat) => (
|
||||
<Link
|
||||
key={chat.id}
|
||||
href={`/mobile/employer/chat/${chat.id}`}
|
||||
className="flex items-center p-6 bg-white active:bg-slate-50 transition-all space-x-4"
|
||||
>
|
||||
<div className="relative shrink-0">
|
||||
<div className="w-14 h-14 rounded-full bg-slate-100 border border-slate-200 flex items-center justify-center text-[#141B34] font-bold text-xl">
|
||||
{chat.name.charAt(0)}
|
||||
</div>
|
||||
{chat.online && (
|
||||
<div className="absolute bottom-0.5 right-0.5 w-3.5 h-3.5 bg-emerald-500 border-2 border-white rounded-full" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0 space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="font-bold text-slate-900 text-[15px] truncate">{chat.name}</h4>
|
||||
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{chat.time}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className={`text-xs truncate max-w-[200px] ${chat.unread > 0 ? 'text-slate-900 font-bold' : 'text-slate-500 font-medium'}`}>
|
||||
{chat.preview}
|
||||
</p>
|
||||
{chat.unread > 0 ? (
|
||||
<div className="w-5 h-5 bg-[#185FA5] text-white text-[10px] font-bold rounded-full flex items-center justify-center border border-white">
|
||||
{chat.unread}
|
||||
</div>
|
||||
) : (
|
||||
<CheckCheck className="w-4 h-4 text-slate-300" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</EmployerMobileLayout>
|
||||
);
|
||||
}
|
||||
96
resources/js/Pages/Mobile/EmployerOTP.jsx
Normal file
96
resources/js/Pages/Mobile/EmployerOTP.jsx
Normal file
@ -0,0 +1,96 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import {
|
||||
ArrowLeft,
|
||||
Loader2,
|
||||
Smartphone
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function EmployerOTP() {
|
||||
const [otp, setOtp] = useState(['', '', '', '']);
|
||||
const [timer, setTimer] = useState(59);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (timer > 0) setTimer(timer - 1);
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [timer]);
|
||||
|
||||
const handleChange = (index, value) => {
|
||||
if (value.length <= 1 && /^\d*$/.test(value)) {
|
||||
const newOtp = [...otp];
|
||||
newOtp[index] = value;
|
||||
setOtp(newOtp);
|
||||
|
||||
if (value && index < 3) {
|
||||
document.getElementById(`otp-${index + 1}`).focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
setProcessing(true);
|
||||
setTimeout(() => {
|
||||
window.location.href = '/mobile/employer/home';
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white font-sans text-slate-900 flex flex-col max-w-md mx-auto">
|
||||
<Head title="OTP Verification" />
|
||||
|
||||
<div className="px-6 pt-12 pb-6">
|
||||
<button onClick={() => window.history.back()} className="p-2 -ml-2 text-slate-600">
|
||||
<ArrowLeft className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 px-8 pt-4 space-y-10">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-3xl font-bold tracking-tight">Verify email</h1>
|
||||
<p className="text-slate-500 text-sm">Enter the code sent to your email address</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-10">
|
||||
<div className="flex justify-between space-x-4">
|
||||
{otp.map((digit, i) => (
|
||||
<input
|
||||
key={i}
|
||||
id={`otp-${i}`}
|
||||
type="text"
|
||||
value={digit}
|
||||
onChange={(e) => handleChange(i, e.target.value)}
|
||||
className="w-16 h-18 bg-slate-50 border border-slate-200 rounded-2xl text-center text-2xl font-bold text-slate-900 focus:bg-white focus:ring-2 focus:ring-[#185FA5]/10 focus:border-[#185FA5] outline-none transition-all"
|
||||
maxLength={1}
|
||||
required
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={processing || otp.join('').length < 4}
|
||||
className="w-full h-16 bg-[#185FA5] text-white rounded-2xl font-bold text-base shadow-lg shadow-blue-500/20 flex items-center justify-center active:scale-[0.98] transition-all disabled:opacity-50"
|
||||
>
|
||||
{processing ? <Loader2 className="w-6 h-6 animate-spin" /> : <span>Verify</span>}
|
||||
</button>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-slate-500">
|
||||
Didn't receive code? {timer > 0 ? (
|
||||
<span className="text-[#185FA5] font-medium ml-1">00:{timer < 10 ? `0${timer}` : timer}</span>
|
||||
) : (
|
||||
<button type="button" onClick={() => setTimer(59)} className="text-[#185FA5] font-bold hover:underline ml-1">Resend now</button>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
135
resources/js/Pages/Mobile/EmployerProfile.jsx
Normal file
135
resources/js/Pages/Mobile/EmployerProfile.jsx
Normal file
@ -0,0 +1,135 @@
|
||||
import React from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import EmployerMobileLayout from './Layouts/EmployerMobileLayout';
|
||||
import {
|
||||
User,
|
||||
Settings,
|
||||
CreditCard,
|
||||
Shield,
|
||||
Bell,
|
||||
HelpCircle,
|
||||
LogOut,
|
||||
ChevronRight,
|
||||
Camera,
|
||||
CheckCircle2,
|
||||
Briefcase,
|
||||
Building2,
|
||||
Smartphone,
|
||||
Globe
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function EmployerProfile() {
|
||||
const sections = [
|
||||
{
|
||||
title: 'Account Settings',
|
||||
items: [
|
||||
{ label: 'Personal Information', icon: User, href: '/mobile/employer/profile/edit', color: 'text-blue-500 bg-blue-50' },
|
||||
{ label: 'Subscription Plan', icon: CreditCard, href: '/mobile/employer/subscription', badge: 'Premium', color: 'text-indigo-500 bg-indigo-50' },
|
||||
{ label: 'Security & Password', icon: Shield, href: '/mobile/employer/profile/security', color: 'text-emerald-500 bg-emerald-50' },
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Preferences',
|
||||
items: [
|
||||
{ label: 'Notification Settings', icon: Bell, href: '/mobile/employer/profile/notifications', color: 'text-orange-500 bg-orange-50' },
|
||||
{ label: 'Language', icon: Globe, href: '#', detail: 'English (US)', color: 'text-purple-500 bg-purple-50' },
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Support',
|
||||
items: [
|
||||
{ label: 'Help & Support', icon: HelpCircle, href: '/mobile/employer/support', color: 'text-slate-500 bg-slate-50' },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<EmployerMobileLayout>
|
||||
<Head title="My Profile" />
|
||||
|
||||
<div className="flex flex-col h-full bg-[#F8F9FA]">
|
||||
{/* Profile Hero Card */}
|
||||
<div className="bg-white px-6 pt-10 pb-8 flex flex-col items-center text-center space-y-5 border-b border-slate-100 relative shadow-sm">
|
||||
<div className="absolute top-0 left-0 w-full h-32 bg-[#141B34] -z-10" />
|
||||
|
||||
<div className="relative">
|
||||
<div className="w-32 h-32 rounded-full bg-slate-100 border-4 border-white flex items-center justify-center text-[#141B34] font-black text-4xl shadow-xl overflow-hidden">
|
||||
JD
|
||||
</div>
|
||||
<button className="absolute bottom-1 right-1 w-10 h-10 bg-[#141B34] text-white rounded-full flex items-center justify-center border-4 border-white shadow-lg active:scale-95 transition-all">
|
||||
<Camera className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-center space-x-1.5">
|
||||
<h2 className="text-2xl font-black text-slate-900 uppercase tracking-tight">John Doe</h2>
|
||||
<CheckCircle2 className="w-5 h-5 text-blue-500 fill-blue-50" />
|
||||
</div>
|
||||
<div className="flex items-center justify-center space-x-2 text-slate-400 font-bold text-xs uppercase tracking-widest">
|
||||
<Building2 className="w-3.5 h-3.5" />
|
||||
<span>Al Mansoor Household</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3 w-full max-w-[300px] pt-2">
|
||||
<div className="flex-1 bg-slate-50 p-3 rounded-2xl border border-slate-100">
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Plan</div>
|
||||
<div className="text-[11px] font-black text-[#141B34] uppercase tracking-widest">Premium</div>
|
||||
</div>
|
||||
<div className="flex-1 bg-slate-50 p-3 rounded-2xl border border-slate-100">
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Verified</div>
|
||||
<div className="text-[11px] font-black text-emerald-500 uppercase tracking-widest">Yes</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Settings Menu Sections */}
|
||||
<div className="p-6 space-y-8 pb-32">
|
||||
{sections.map((section, idx) => (
|
||||
<div key={idx} className="space-y-4">
|
||||
<h3 className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em] ml-2">{section.title}</h3>
|
||||
<div className="bg-white rounded-[32px] border border-slate-100 shadow-sm overflow-hidden">
|
||||
{section.items.map((item, i) => (
|
||||
<Link
|
||||
key={i}
|
||||
href={item.href}
|
||||
className={`flex items-center justify-between p-5 hover:bg-slate-50 transition-all ${i !== section.items.length - 1 ? 'border-b border-slate-50' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className={`w-11 h-11 rounded-2xl flex items-center justify-center ${item.color} shadow-sm`}>
|
||||
<item.icon className="w-5 h-5" />
|
||||
</div>
|
||||
<span className="text-[13px] font-bold text-slate-900 uppercase tracking-tight">{item.label}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
{item.badge && (
|
||||
<span className="px-3 py-1 bg-blue-50 text-blue-600 text-[9px] font-black uppercase tracking-widest rounded-lg border border-blue-100">
|
||||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
{item.detail && (
|
||||
<span className="text-[11px] font-bold text-slate-400">{item.detail}</span>
|
||||
)}
|
||||
<ChevronRight className="w-4 h-4 text-slate-300" />
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button className="w-full p-6 bg-white text-rose-500 rounded-[32px] font-black text-xs uppercase tracking-[0.2em] flex items-center justify-center space-x-3 active:scale-[0.98] transition-all border border-rose-100 shadow-sm mt-4">
|
||||
<LogOut className="w-5 h-5" />
|
||||
<span>Sign Out Account</span>
|
||||
</button>
|
||||
|
||||
<div className="text-center pt-4">
|
||||
<p className="text-[9px] font-black text-slate-300 uppercase tracking-[0.3em]">Version 2.4.1 (Stable Build)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</EmployerMobileLayout>
|
||||
);
|
||||
}
|
||||
376
resources/js/Pages/Mobile/EmployerRegister.jsx
Normal file
376
resources/js/Pages/Mobile/EmployerRegister.jsx
Normal file
@ -0,0 +1,376 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useForm, Head, Link } from '@inertiajs/react';
|
||||
import {
|
||||
CheckCircle,
|
||||
Camera,
|
||||
Eye,
|
||||
EyeOff,
|
||||
X,
|
||||
Loader2,
|
||||
FileText,
|
||||
AlertCircle,
|
||||
CreditCard,
|
||||
ShieldCheck,
|
||||
Lock,
|
||||
User,
|
||||
Mail,
|
||||
Phone as PhoneIcon,
|
||||
ArrowRight,
|
||||
ArrowLeft,
|
||||
Check,
|
||||
Globe,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Zap,
|
||||
Shield,
|
||||
Trophy
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function EmployerRegister() {
|
||||
const [step, setStep] = useState(1);
|
||||
const { data, setData, post, processing, errors } = useForm({
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
emirates_id_front: null,
|
||||
emirates_id_back: null,
|
||||
selected_plan: 'premium',
|
||||
card_number: '',
|
||||
expiry: '',
|
||||
cvc: '',
|
||||
source: 'mobile',
|
||||
});
|
||||
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [frontFileName, setFrontFileName] = useState(null);
|
||||
const [backFileName, setBackFileName] = useState(null);
|
||||
|
||||
const plans = [
|
||||
{ id: 'basic', name: 'Basic Search', price: '99', icon: Zap, color: 'text-blue-500 bg-blue-50', features: ['Browse 500+ workers', '10 Shortlists'] },
|
||||
{ id: 'premium', name: 'Premium Pass', price: '199', icon: Shield, color: 'text-[#185FA5] bg-blue-50', popular: true, features: ['Unlimited lists', 'Direct Messaging'] },
|
||||
{ id: 'vip', name: 'VIP Concierge', price: '499', icon: Trophy, color: 'text-amber-500 bg-amber-50', features: ['Assigned Manager', 'Full Warranty'] },
|
||||
];
|
||||
|
||||
const nextStep = () => setStep(prev => prev + 1);
|
||||
const prevStep = () => setStep(prev => prev - 1);
|
||||
|
||||
const handleFileChange = (e, field) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
setData(field, file);
|
||||
const sizeMb = (file.size / (1024 * 1024)).toFixed(1);
|
||||
if (field === 'emirates_id_front') {
|
||||
setFrontFileName(`${file.name} (${sizeMb}MB)`);
|
||||
} else {
|
||||
setBackFileName(`${file.name} (${sizeMb}MB)`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const removeFile = (field) => {
|
||||
setData(field, null);
|
||||
if (field === 'emirates_id_front') {
|
||||
setFrontFileName(null);
|
||||
} else {
|
||||
setBackFileName(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
post('/employer/register');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white font-sans text-slate-900 flex flex-col max-w-md mx-auto relative overflow-hidden">
|
||||
<Head title="Enrollment" />
|
||||
|
||||
{/* Minimal Header */}
|
||||
<div className="px-6 pt-12 pb-4 flex items-center justify-between sticky top-0 bg-white/80 backdrop-blur-xl z-50">
|
||||
<button onClick={() => step > 1 ? prevStep() : window.history.back()} className="p-3 bg-slate-50 rounded-2xl text-slate-400 border border-slate-100 active:scale-95 transition-all">
|
||||
<ChevronLeft className="w-6 h-6" />
|
||||
</button>
|
||||
<div className="flex items-center space-x-1.5 px-4 py-2 bg-slate-50 rounded-full border border-slate-100">
|
||||
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full animate-pulse" />
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-slate-400">Step {step} / 4</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 px-8 pt-8 space-y-8 overflow-y-auto no-scrollbar pb-32">
|
||||
{/* Header Text */}
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-3xl font-black tracking-tight text-slate-900 leading-none">
|
||||
{step === 1 && "Create Account"}
|
||||
{step === 2 && "Identity Vetting"}
|
||||
{step === 3 && "Choose Plan"}
|
||||
{step === 4 && "Checkout"}
|
||||
</h1>
|
||||
<p className="text-sm font-medium text-slate-400">
|
||||
{step === 1 && "Start your professional hiring journey."}
|
||||
{step === 2 && "Verified ID is mandatory for compliance."}
|
||||
{step === 3 && "Select a package that fits your needs."}
|
||||
{step === 4 && "Secure payment via encrypted gateway."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Step 1: Account Info */}
|
||||
{step === 1 && (
|
||||
<div className="space-y-5 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[11px] font-bold text-slate-400 uppercase tracking-widest ml-2">Full Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={data.name}
|
||||
onChange={(e) => setData('name', e.target.value)}
|
||||
placeholder="Enter your full name"
|
||||
className="w-full px-6 py-4 bg-slate-50 border border-slate-100 rounded-[24px] text-sm font-medium focus:bg-white focus:border-[#185FA5] focus:ring-4 focus:ring-blue-500/10 transition-all outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[11px] font-bold text-slate-400 uppercase tracking-widest ml-2">Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
value={data.email}
|
||||
onChange={(e) => setData('email', e.target.value)}
|
||||
placeholder="your@email.com"
|
||||
className="w-full px-6 py-4 bg-slate-50 border border-slate-100 rounded-[24px] text-sm font-medium focus:bg-white focus:border-[#185FA5] focus:ring-4 focus:ring-blue-500/10 transition-all outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[11px] font-bold text-slate-400 uppercase tracking-widest ml-2">Phone Number</label>
|
||||
<input
|
||||
type="text"
|
||||
value={data.phone}
|
||||
onChange={(e) => setData('phone', e.target.value)}
|
||||
placeholder="+971 50 000 0000"
|
||||
className="w-full px-6 py-4 bg-slate-50 border border-slate-100 rounded-[24px] text-sm font-medium focus:bg-white focus:border-[#185FA5] focus:ring-4 focus:ring-blue-500/10 transition-all outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[11px] font-bold text-slate-400 uppercase tracking-widest ml-2">Password</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={data.password}
|
||||
onChange={(e) => setData('password', e.target.value)}
|
||||
placeholder="Min. 8 characters"
|
||||
className="w-full px-6 py-4 bg-slate-50 border border-slate-100 rounded-[24px] text-sm font-medium focus:bg-white focus:border-[#185FA5] focus:ring-4 focus:ring-blue-500/10 transition-all outline-none"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-6 top-1/2 -translate-y-1/2 text-slate-300 hover:text-slate-500"
|
||||
>
|
||||
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={nextStep}
|
||||
className="w-full py-5 bg-[#185FA5] text-white rounded-[24px] font-bold text-sm uppercase tracking-widest shadow-xl shadow-blue-500/20 flex items-center justify-center space-x-3 active:scale-[0.98] transition-all mt-4"
|
||||
>
|
||||
<span>Continue</span>
|
||||
<ArrowRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: Emirates ID */}
|
||||
{step === 2 && (
|
||||
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="bg-emerald-50 p-6 rounded-[32px] flex items-center space-x-4 border border-emerald-100">
|
||||
<ShieldCheck className="w-8 h-8 text-emerald-500 shrink-0" />
|
||||
<p className="text-[11px] font-bold text-emerald-800 leading-relaxed uppercase tracking-wider">
|
||||
Your ID is encrypted and handled securely as per UAE data protection laws.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{ field: 'emirates_id_front', label: 'EID Front Side', file: frontFileName },
|
||||
{ field: 'emirates_id_back', label: 'EID Back Side', file: backFileName }
|
||||
].map((slot) => (
|
||||
<div key={slot.field} className="space-y-2">
|
||||
<label className="text-[11px] font-bold text-slate-400 uppercase tracking-widest ml-2">{slot.label}</label>
|
||||
{slot.file ? (
|
||||
<div className="relative h-40 bg-slate-50 rounded-[32px] border-2 border-[#185FA5] flex flex-col items-center justify-center p-6 shadow-xl shadow-blue-500/5">
|
||||
<FileText className="w-10 h-10 text-[#185FA5] mb-2" />
|
||||
<span className="text-[10px] font-bold text-slate-900 uppercase truncate max-w-full px-4">{slot.file}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeFile(slot.field)}
|
||||
className="absolute -top-2 -right-2 bg-rose-500 text-white p-3 rounded-full shadow-lg active:scale-90 transition-transform"
|
||||
>
|
||||
<X className="w-4 h-4" strokeWidth={3} />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<label className="relative h-40 bg-slate-50 rounded-[32px] border-2 border-dashed border-slate-200 flex flex-col items-center justify-center p-8 cursor-pointer hover:border-[#185FA5] transition-all group">
|
||||
<div className="w-14 h-14 bg-white rounded-2xl flex items-center justify-center shadow-sm mb-3 group-hover:scale-110 transition-transform">
|
||||
<Camera className="w-6 h-6 text-slate-300 group-hover:text-[#185FA5]" />
|
||||
</div>
|
||||
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest group-hover:text-[#185FA5]">Tap to Upload</span>
|
||||
<input
|
||||
type="file"
|
||||
className="hidden"
|
||||
onChange={(e) => handleFileChange(e, slot.field)}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={nextStep}
|
||||
disabled={!frontFileName || !backFileName}
|
||||
className="w-full py-5 bg-[#185FA5] text-white rounded-[24px] font-bold text-sm uppercase tracking-widest shadow-xl shadow-blue-500/20 flex items-center justify-center space-x-3 active:scale-[0.98] transition-all disabled:opacity-50 mt-4"
|
||||
>
|
||||
<span>Verify Identity</span>
|
||||
<ShieldCheck className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Plans */}
|
||||
{step === 3 && (
|
||||
<div className="space-y-4 animate-in fade-in slide-in-from-bottom-4 duration-500 pb-10">
|
||||
{plans.map((p) => (
|
||||
<button
|
||||
key={p.id}
|
||||
type="button"
|
||||
onClick={() => setData('selected_plan', p.id)}
|
||||
className={`w-full p-6 rounded-[32px] border-2 text-left transition-all relative overflow-hidden flex items-center justify-between ${
|
||||
data.selected_plan === p.id
|
||||
? 'bg-blue-50/50 border-[#185FA5] shadow-lg shadow-blue-500/5'
|
||||
: 'bg-white border-slate-50 hover:border-slate-100'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className={`w-12 h-12 rounded-2xl flex items-center justify-center ${p.color}`}>
|
||||
<p.icon className="w-6 h-6" />
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h4 className="text-sm font-bold text-slate-900 uppercase tracking-tight">{p.name}</h4>
|
||||
{p.popular && <span className="bg-emerald-500 text-white px-2 py-0.5 rounded-full text-[8px] font-bold uppercase tracking-widest">Best</span>}
|
||||
</div>
|
||||
<div className="text-[10px] font-medium text-slate-400">{p.features[0]}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-xl font-black text-[#185FA5]">{p.price} <span className="text-[10px] font-bold">AED</span></div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={nextStep}
|
||||
className="w-full py-5 bg-[#185FA5] text-white rounded-[24px] font-bold text-sm uppercase tracking-widest shadow-xl shadow-blue-500/20 flex items-center justify-center space-x-3 active:scale-[0.98] transition-all mt-4"
|
||||
>
|
||||
<span>Continue to Payment</span>
|
||||
<ArrowRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 4: Payment */}
|
||||
{step === 4 && (
|
||||
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
{/* Card Visual */}
|
||||
<div className="bg-[#141B34] rounded-[32px] p-8 text-white relative overflow-hidden shadow-2xl">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-[#185FA5]/20 rounded-full blur-3xl -mr-16 -mt-16" />
|
||||
<div className="flex justify-between items-start mb-10 relative z-10">
|
||||
<div className="w-12 h-8 bg-amber-400 rounded-lg" />
|
||||
<CreditCard className="w-8 h-8 text-white/20" />
|
||||
</div>
|
||||
<div className="space-y-4 relative z-10">
|
||||
<div className="text-lg font-bold tracking-[0.2em] h-8 flex items-center">
|
||||
{data.card_number || '•••• •••• •••• ••••'}
|
||||
</div>
|
||||
<div className="flex justify-between border-t border-white/10 pt-4">
|
||||
<div className="space-y-0.5 text-left">
|
||||
<div className="text-[9px] font-bold uppercase opacity-40 tracking-widest">Card Holder</div>
|
||||
<div className="text-[11px] font-bold uppercase tracking-widest truncate max-w-[150px]">{data.name || 'Your Name'}</div>
|
||||
</div>
|
||||
<div className="text-right space-y-0.5">
|
||||
<div className="text-[9px] font-bold uppercase opacity-40 tracking-widest">Expiry</div>
|
||||
<div className="text-[11px] font-bold uppercase tracking-widest">{data.expiry || 'MM/YY'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<input
|
||||
type="text"
|
||||
value={data.card_number}
|
||||
onChange={(e) => setData('card_number', e.target.value.replace(/\W/gi, '').replace(/(.{4})/g, '$1 ').trim())}
|
||||
placeholder="Card Number"
|
||||
className="w-full px-6 py-4 bg-slate-50 border border-slate-100 rounded-[24px] text-sm font-medium focus:bg-white focus:border-[#185FA5] transition-all outline-none"
|
||||
required
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<input
|
||||
type="text"
|
||||
value={data.expiry}
|
||||
onChange={(e) => setData('expiry', e.target.value.replace(/^(\d{2})/, '$1/'))}
|
||||
placeholder="MM/YY"
|
||||
className="w-full px-6 py-4 bg-slate-50 border border-slate-100 rounded-[24px] text-sm font-medium focus:bg-white focus:border-[#185FA5] transition-all outline-none text-center"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={data.cvc}
|
||||
onChange={(e) => setData('cvc', e.target.value)}
|
||||
placeholder="CVC"
|
||||
className="w-full px-6 py-4 bg-slate-50 border border-slate-100 rounded-[24px] text-sm font-medium focus:bg-white focus:border-[#185FA5] transition-all outline-none text-center"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-slate-50 rounded-[32px] flex items-center justify-between border border-slate-100">
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Total to Pay</div>
|
||||
<div className="text-xl font-black text-slate-900">{plans.find(p => p.id === data.selected_plan)?.price} AED</div>
|
||||
</div>
|
||||
<div className="p-3 bg-emerald-50 rounded-2xl">
|
||||
<Lock className="w-6 h-6 text-emerald-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={processing}
|
||||
className="w-full py-5 bg-emerald-500 text-white rounded-[24px] font-bold text-sm uppercase tracking-widest shadow-xl shadow-emerald-500/20 flex items-center justify-center space-x-3 active:scale-[0.98] transition-all disabled:opacity-70 mt-4"
|
||||
>
|
||||
{processing ? <Loader2 className="w-6 h-6 animate-spin" /> : (
|
||||
<>
|
||||
<span>Confirm & Pay</span>
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
|
||||
<div className="text-center pt-8 pb-10">
|
||||
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest">
|
||||
Already joined? <Link href="/mobile/employer/login" className="text-[#185FA5]">Sign In</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
109
resources/js/Pages/Mobile/EmployerShortlist.jsx
Normal file
109
resources/js/Pages/Mobile/EmployerShortlist.jsx
Normal file
@ -0,0 +1,109 @@
|
||||
import React from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import EmployerMobileLayout from './Layouts/EmployerMobileLayout';
|
||||
import {
|
||||
Bookmark,
|
||||
MessageSquare,
|
||||
X,
|
||||
Briefcase,
|
||||
DollarSign,
|
||||
Star,
|
||||
Globe2,
|
||||
ArrowRight
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function EmployerShortlist() {
|
||||
const savedWorkers = [
|
||||
{ id: 1, name: 'Maria Santos', role: 'Housemaid', rating: '4.9', salary: '1,800 AED', nationality: 'Philippines', img: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Maria', exp: '8 Years' },
|
||||
{ id: 2, name: 'Elena Gilbert', role: 'Nanny', rating: '4.8', salary: '2,500 AED', nationality: 'Kenya', img: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Elena', exp: '5 Years' },
|
||||
];
|
||||
|
||||
return (
|
||||
<EmployerMobileLayout>
|
||||
<Head title="Shortlist" />
|
||||
|
||||
<div className="p-6 space-y-8">
|
||||
{/* Header */}
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Shortlist</h1>
|
||||
<p className="text-sm text-slate-500 font-medium">Quick access to your saved candidates</p>
|
||||
</div>
|
||||
|
||||
{savedWorkers.length > 0 ? (
|
||||
<div className="space-y-4 pb-10">
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{savedWorkers.length} Saved Profiles</span>
|
||||
<button className="text-[10px] font-bold text-rose-500 uppercase tracking-widest">Clear All</button>
|
||||
</div>
|
||||
|
||||
{savedWorkers.map((worker) => (
|
||||
<div key={worker.id} className="bg-white p-5 rounded-3xl border border-slate-100 shadow-sm space-y-5">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-16 h-16 rounded-2xl bg-slate-50 overflow-hidden border border-slate-200">
|
||||
<img src={worker.img} alt="worker" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h4 className="font-bold text-slate-900 text-base">{worker.name}</h4>
|
||||
<div className="flex items-center space-x-2 text-[11px] font-semibold text-slate-400 uppercase tracking-wider">
|
||||
<Globe2 className="w-3 h-3" />
|
||||
<span>{worker.nationality}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button className="w-9 h-9 rounded-xl bg-slate-50 flex items-center justify-center text-slate-300 hover:text-rose-500 transition-colors border border-slate-100">
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="flex items-center space-x-2 text-[11px] font-bold text-slate-600 bg-slate-50 px-3 py-2.5 rounded-xl border border-slate-100">
|
||||
<Briefcase className="w-3.5 h-3.5 text-slate-400" />
|
||||
<span>{worker.exp}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 text-[11px] font-bold text-slate-600 bg-slate-50 px-3 py-2.5 rounded-xl border border-slate-100">
|
||||
<DollarSign className="w-3.5 h-3.5 text-emerald-500" />
|
||||
<span>{worker.salary}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3 pt-2">
|
||||
<Link
|
||||
href={`/mobile/employer/workers/${worker.id}`}
|
||||
className="flex-1 py-4 bg-[#141B34] text-white rounded-2xl font-bold text-sm shadow-xl shadow-slate-900/10 active:scale-95 transition-all text-center"
|
||||
>
|
||||
View Profile
|
||||
</Link>
|
||||
<div className="flex items-center space-x-1 px-4 py-4 bg-amber-50 rounded-2xl border border-amber-100">
|
||||
<Star className="w-4 h-4 text-amber-500 fill-amber-500" />
|
||||
<span className="text-sm font-bold text-amber-700">{worker.rating}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="pt-4">
|
||||
<button className="w-full h-16 bg-white border-2 border-slate-100 text-[#141B34] rounded-2xl font-bold text-sm flex items-center justify-center space-x-2 shadow-sm active:scale-95 transition-all">
|
||||
<MessageSquare className="w-5 h-5" />
|
||||
<span>Message All</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col items-center justify-center space-y-6 pt-20">
|
||||
<div className="w-24 h-24 bg-white border border-slate-100 rounded-[40px] flex items-center justify-center shadow-sm">
|
||||
<Bookmark className="w-10 h-10 text-slate-100" />
|
||||
</div>
|
||||
<div className="text-center space-y-2 px-10">
|
||||
<h3 className="text-lg font-bold tracking-tight">Your Shortlist is Empty</h3>
|
||||
<p className="text-sm text-slate-400 font-medium">Save profiles to compare and hire faster.</p>
|
||||
</div>
|
||||
<Link href="/mobile/employer/workers" className="px-10 py-4 bg-[#141B34] text-white rounded-2xl font-bold text-sm shadow-lg active:scale-95 transition-all">
|
||||
Browse Workers
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</EmployerMobileLayout>
|
||||
);
|
||||
}
|
||||
44
resources/js/Pages/Mobile/EmployerSplash.jsx
Normal file
44
resources/js/Pages/Mobile/EmployerSplash.jsx
Normal file
@ -0,0 +1,44 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Head, router } from '@inertiajs/react';
|
||||
import { ShieldCheck } from 'lucide-react';
|
||||
|
||||
export default function EmployerSplash() {
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
router.get('/mobile/employer/welcome');
|
||||
}, 2000);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#D8F3FF] flex flex-col items-center justify-center p-8 max-w-md mx-auto relative overflow-hidden">
|
||||
<Head title="Welcome to Marketplace" />
|
||||
|
||||
{/* Animated Background Elements */}
|
||||
<div className="absolute top-[-10%] right-[-10%] w-64 h-64 bg-white/40 rounded-full blur-3xl animate-pulse" />
|
||||
<div className="absolute bottom-[-10%] left-[-10%] w-80 h-80 bg-[#185FA5]/10 rounded-full blur-3xl animate-bounce duration-[3000ms]" />
|
||||
|
||||
<div className="relative flex flex-col items-center space-y-8 animate-in fade-in zoom-in duration-1000">
|
||||
<div className="w-24 h-24 bg-gradient-to-br from-[#185FA5] to-[#2573c0] rounded-[32px] flex items-center justify-center shadow-[0_20px_40px_rgba(24,95,165,0.3)] border-4 border-white/20">
|
||||
<ShieldCheck className="w-12 h-12 text-white" strokeWidth={2.5} />
|
||||
</div>
|
||||
|
||||
<div className="text-center space-y-2">
|
||||
<h1 className="text-4xl font-black text-[#141B34] tracking-tighter uppercase leading-none">
|
||||
Job<span className="text-[#185FA5]">Sift</span>
|
||||
</h1>
|
||||
<p className="text-[10px] font-black text-[#185FA5]/60 uppercase tracking-[0.4em] ml-1">Employer Edition</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-12 flex flex-col items-center space-y-4">
|
||||
<div className="flex space-x-1.5">
|
||||
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full animate-bounce" />
|
||||
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full animate-bounce [animation-delay:0.2s]" />
|
||||
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full animate-bounce [animation-delay:0.4s]" />
|
||||
</div>
|
||||
<span className="text-[10px] font-black text-[#3B46D1]/40 uppercase tracking-widest">Version 2.0.26</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
97
resources/js/Pages/Mobile/EmployerSubscription.jsx
Normal file
97
resources/js/Pages/Mobile/EmployerSubscription.jsx
Normal file
@ -0,0 +1,97 @@
|
||||
import React from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import EmployerMobileLayout from './Layouts/EmployerMobileLayout';
|
||||
import { Check, ShieldCheck, Zap, Star, Trophy } from 'lucide-react';
|
||||
|
||||
export default function EmployerSubscription() {
|
||||
const plans = [
|
||||
{
|
||||
id: 'basic',
|
||||
name: 'Basic Search',
|
||||
price: '99',
|
||||
icon: Zap,
|
||||
color: 'bg-slate-50 text-slate-400',
|
||||
features: ['Browse 500+ workers', 'Shortlist up to 10', 'Email support']
|
||||
},
|
||||
{
|
||||
id: 'premium',
|
||||
name: 'Premium Pass',
|
||||
price: '199',
|
||||
icon: Star,
|
||||
color: 'bg-blue-50 text-blue-600',
|
||||
popular: true,
|
||||
features: ['Unlimited shortlisting', 'Direct messaging', 'Priority profile view', '24/7 Support']
|
||||
},
|
||||
{
|
||||
id: 'vip',
|
||||
name: 'VIP Concierge',
|
||||
price: '499',
|
||||
icon: Trophy,
|
||||
color: 'bg-amber-50 text-amber-500',
|
||||
features: ['Assigned manager', 'Medical guarantee', 'Replacement warranty', 'Legal assistance']
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EmployerMobileLayout>
|
||||
<Head title="Subscription" />
|
||||
|
||||
<div className="p-6 space-y-8">
|
||||
{/* Header */}
|
||||
<div className="text-center space-y-2 py-4">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Choose Your Plan</h1>
|
||||
<p className="text-sm text-slate-500 font-medium max-w-[250px] mx-auto">Select a package that fits your hiring needs.</p>
|
||||
</div>
|
||||
|
||||
{/* Plan Cards Stacked */}
|
||||
<div className="space-y-6 pb-20">
|
||||
{plans.map((plan) => (
|
||||
<div key={plan.id} className={`bg-white rounded-[32px] p-8 border shadow-sm relative overflow-hidden transition-all active:scale-[0.98] ${
|
||||
plan.popular ? 'border-[#141B34] border-2 shadow-xl' : 'border-slate-100'
|
||||
}`}>
|
||||
{plan.popular && (
|
||||
<div className="absolute top-0 right-0 bg-[#141B34] text-white px-6 py-1.5 rounded-bl-2xl text-[9px] font-black uppercase tracking-widest">
|
||||
Most Popular
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className={`w-14 h-14 rounded-2xl flex items-center justify-center ${plan.color}`}>
|
||||
<plan.icon className="w-7 h-7" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-lg font-bold">{plan.name}</h3>
|
||||
<div className="flex items-baseline space-x-1">
|
||||
<span className="text-2xl font-black text-slate-900">{plan.price}</span>
|
||||
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">AED / Mo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{plan.features.map((feature, i) => (
|
||||
<div key={i} className="flex items-center space-x-3">
|
||||
<div className="w-5 h-5 rounded-full bg-emerald-50 flex items-center justify-center">
|
||||
<Check className="w-3 h-3 text-emerald-500" strokeWidth={3} />
|
||||
</div>
|
||||
<span className="text-xs text-slate-600 font-medium">{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button className={`w-full py-4 rounded-2xl font-bold text-sm uppercase tracking-widest transition-all ${
|
||||
plan.popular
|
||||
? 'bg-[#141B34] text-white shadow-lg'
|
||||
: 'bg-slate-50 text-slate-900 hover:bg-slate-100'
|
||||
}`}>
|
||||
Select {plan.name}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</EmployerMobileLayout>
|
||||
);
|
||||
}
|
||||
83
resources/js/Pages/Mobile/EmployerWelcome.jsx
Normal file
83
resources/js/Pages/Mobile/EmployerWelcome.jsx
Normal file
@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import {
|
||||
Briefcase,
|
||||
ArrowRight,
|
||||
Sparkles,
|
||||
ShieldCheck,
|
||||
Users
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function EmployerWelcome() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#D8F3FF] flex flex-col max-w-md mx-auto relative overflow-hidden">
|
||||
<Head title="Welcome" />
|
||||
|
||||
{/* Artistic Header Background */}
|
||||
<div className="h-[45vh] relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-[#185FA5] to-[#2573c0] rounded-b-[60px] shadow-2xl" />
|
||||
<div className="absolute top-[10%] left-[10%] w-32 h-32 bg-white/10 rounded-full blur-2xl" />
|
||||
<div className="absolute bottom-[20%] right-[-10%] w-64 h-64 bg-emerald-400/20 rounded-full blur-3xl" />
|
||||
|
||||
<div className="relative h-full flex flex-col items-center justify-center p-8 space-y-6 text-center animate-in slide-in-from-top duration-700">
|
||||
<div className="w-20 h-20 bg-white/20 backdrop-blur-xl rounded-[28px] border border-white/30 flex items-center justify-center shadow-2xl">
|
||||
<ShieldCheck className="w-10 h-10 text-white" strokeWidth={2.5} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-3xl font-black text-white tracking-tighter uppercase leading-none">
|
||||
Find Top <br /> Talent Fast
|
||||
</h1>
|
||||
<p className="text-blue-100/70 text-[11px] font-bold uppercase tracking-[0.2em]">The Future of Recruitment</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 px-8 pt-10 space-y-10 animate-in slide-in-from-bottom duration-700">
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start space-x-5">
|
||||
<div className="w-12 h-12 bg-white rounded-2xl flex items-center justify-center shadow-lg shadow-blue-500/5 shrink-0">
|
||||
<Users className="w-6 h-6 text-[#185FA5]" strokeWidth={2.5} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h4 className="font-black text-[#141B34] text-sm uppercase tracking-tight">Verified Workers</h4>
|
||||
<p className="text-[11px] text-slate-400 font-bold uppercase tracking-tight leading-relaxed">Access 10,000+ pre-vetted domestic and commercial workers.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-5">
|
||||
<div className="w-12 h-12 bg-white rounded-2xl flex items-center justify-center shadow-lg shadow-blue-500/5 shrink-0">
|
||||
<Sparkles className="w-6 h-6 text-[#22D39B]" strokeWidth={2.5} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h4 className="font-black text-[#141B34] text-sm uppercase tracking-tight">AI Matching</h4>
|
||||
<p className="text-[11px] text-slate-400 font-bold uppercase tracking-tight leading-relaxed">Our smart engine matches workers based on your specific needs.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 pt-4">
|
||||
<Link
|
||||
href="/mobile/employer/login"
|
||||
className="w-full h-18 bg-[#185FA5] text-white rounded-[28px] font-black text-sm uppercase tracking-[0.2em] shadow-[0_15px_35px_rgba(24,95,165,0.3)] flex items-center justify-center space-x-3 active:scale-95 transition-all"
|
||||
>
|
||||
<span>Login to Portal</span>
|
||||
<ArrowRight className="w-5 h-5" strokeWidth={3} />
|
||||
</Link>
|
||||
<Link
|
||||
href="/mobile/employer/register"
|
||||
className="w-full h-18 bg-white text-[#185FA5] rounded-[28px] font-black text-sm uppercase tracking-[0.2em] shadow-lg flex items-center justify-center active:scale-95 transition-all border-2 border-white"
|
||||
>
|
||||
<span>Join as Employer</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-8 pb-10 text-center">
|
||||
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest leading-loose">
|
||||
By continuing you agree to our <br />
|
||||
<span className="text-[#185FA5] underline underline-offset-4">Terms of Service</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
319
resources/js/Pages/Mobile/EmployerWorkers.jsx
Normal file
319
resources/js/Pages/Mobile/EmployerWorkers.jsx
Normal file
@ -0,0 +1,319 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import EmployerMobileLayout from './Layouts/EmployerMobileLayout';
|
||||
import {
|
||||
Search,
|
||||
Filter,
|
||||
Globe,
|
||||
Briefcase,
|
||||
DollarSign,
|
||||
Star,
|
||||
Heart,
|
||||
MapPin,
|
||||
ArrowRight,
|
||||
ChevronLeft,
|
||||
Bookmark,
|
||||
CheckCircle2,
|
||||
Calendar,
|
||||
Sparkles,
|
||||
X,
|
||||
RotateCcw
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function EmployerWorkers() {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
const [activeCat, setActiveCat] = useState('All');
|
||||
|
||||
const [filters, setFilters] = useState({
|
||||
nationality: 'All Nationalities',
|
||||
availability: 'All Availabilities',
|
||||
experience: 'All Experience',
|
||||
religion: 'All Religions',
|
||||
maxSalary: 3000
|
||||
});
|
||||
|
||||
const categories = ['All', 'Housemaid', 'Nanny', 'Cook', 'Driver', 'Gardener'];
|
||||
|
||||
const filterOptions = {
|
||||
nationalities: ['All Nationalities', 'Philippines', 'India', 'Sri Lanka', 'Kenya', 'Ethiopia', 'Indonesia'],
|
||||
availabilities: ['All Availabilities', 'Available Now', 'Available in 1 week', 'Available in 1 month'],
|
||||
experienceLevels: ['All Experience', '0-2 Years', '2-5 Years', '5-10 Years', '10+ Years'],
|
||||
religions: ['All Religions', 'Christianity', 'Islam', 'Hinduism', 'Other']
|
||||
};
|
||||
|
||||
const workers = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Maria Santos',
|
||||
role: 'Childcare',
|
||||
salary: '1800 AED / mo',
|
||||
nationality: 'Philippines',
|
||||
initial: 'M',
|
||||
bio: '"Experienced nanny with 6 years working with expatriate families in Dubai. Certified in pediatric first aid."',
|
||||
exp: '5+ Years',
|
||||
avail: 'Immediate',
|
||||
religion: 'Christian',
|
||||
age: '32 yrs',
|
||||
skills: ['Childcare', 'Cooking', 'Housekeeping', 'Ironing']
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Elena Gilbert',
|
||||
role: 'Housemaid',
|
||||
salary: '2500 AED / mo',
|
||||
nationality: 'Kenya',
|
||||
initial: 'E',
|
||||
bio: '"Professional housekeeper with expertise in deep cleaning and organizing. Fluent in English and Swahili."',
|
||||
exp: '3+ Years',
|
||||
avail: 'Immediate',
|
||||
religion: 'Christian',
|
||||
age: '28 yrs',
|
||||
skills: ['Cleaning', 'Ironing', 'Bilingual']
|
||||
}
|
||||
];
|
||||
|
||||
const resetFilters = () => {
|
||||
setFilters({
|
||||
nationality: 'All Nationalities',
|
||||
availability: 'All Availabilities',
|
||||
experience: 'All Experience',
|
||||
religion: 'All Religions',
|
||||
maxSalary: 3000
|
||||
});
|
||||
setSearchQuery('');
|
||||
};
|
||||
|
||||
return (
|
||||
<EmployerMobileLayout>
|
||||
<Head title="Find Workers" />
|
||||
|
||||
<div className="flex flex-col h-full bg-[#F8FAFC]">
|
||||
{/* Header */}
|
||||
<div className="pt-12 pb-6 px-6 space-y-6 bg-white sticky top-0 z-40 shadow-sm">
|
||||
<div className="flex items-center space-x-4">
|
||||
<button onClick={() => window.history.back()} className="p-2 bg-slate-50 rounded-xl text-slate-400 border border-slate-100">
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<h1 className="text-2xl font-bold text-slate-900 tracking-tight">Find Workers</h1>
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex-1 relative group">
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-300 group-focus-within:text-[#185FA5] transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name or skill..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-12 pr-4 py-4 bg-slate-50 border border-slate-100 rounded-2xl text-sm focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowFilters(true)}
|
||||
className={`w-14 h-14 rounded-2xl flex items-center justify-center border transition-all ${
|
||||
showFilters ? 'bg-[#185FA5] text-white border-[#185FA5]' : 'bg-white text-slate-400 border-slate-100'
|
||||
}`}
|
||||
>
|
||||
<Filter className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Category Tabs */}
|
||||
<div className="flex space-x-2 overflow-x-auto no-scrollbar pb-1">
|
||||
{categories.map((cat) => (
|
||||
<button
|
||||
key={cat}
|
||||
onClick={() => setActiveCat(cat)}
|
||||
className={`shrink-0 px-5 py-2.5 rounded-full text-[10px] font-bold uppercase tracking-widest transition-all ${
|
||||
activeCat === cat
|
||||
? 'bg-[#185FA5] text-white shadow-lg'
|
||||
: 'bg-white text-slate-400 border border-slate-100 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
{cat}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Worker Listing */}
|
||||
<div className="px-6 py-8 space-y-8 pb-32">
|
||||
{workers.map((worker) => (
|
||||
<div key={worker.id} className="bg-white rounded-[32px] border border-slate-100 shadow-sm overflow-hidden flex flex-col">
|
||||
{/* Card Header Section */}
|
||||
<div className="p-6 bg-[#F8FAFC] flex items-center justify-between border-b border-slate-50">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-16 h-16 rounded-full bg-[#D8F3FF] flex items-center justify-center text-[#185FA5] font-black text-2xl shadow-inner">
|
||||
{worker.initial}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center space-x-1.5">
|
||||
<h4 className="font-bold text-slate-900 text-lg tracking-tight">{worker.name}</h4>
|
||||
<CheckCircle2 className="w-4 h-4 text-emerald-500" />
|
||||
</div>
|
||||
<div className="flex items-center space-x-1.5 text-slate-400 font-semibold text-xs tracking-wide">
|
||||
<Globe className="w-3.5 h-3.5" />
|
||||
<span>{worker.nationality}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button className="w-10 h-10 rounded-full bg-[#EBF5FF] flex items-center justify-center text-[#185FA5]">
|
||||
<Bookmark className="w-5 h-5 fill-current" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Card Body Section */}
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="px-4 py-2 bg-[#F0F7FF] text-[#185FA5] rounded-xl text-xs font-bold border border-[#E1EFFF]">
|
||||
{worker.role}
|
||||
</div>
|
||||
<div className="flex items-center space-x-1.5">
|
||||
<DollarSign className="w-4 h-4 text-emerald-500" />
|
||||
<span className="text-sm font-bold text-slate-900">{worker.salary}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-[13px] text-slate-500 font-medium leading-relaxed italic">
|
||||
{worker.bio}
|
||||
</p>
|
||||
|
||||
{/* Stats Grid 2x2 */}
|
||||
<div className="bg-[#F8FAFC] p-4 rounded-2xl grid grid-cols-2 gap-4 border border-slate-50">
|
||||
<div className="flex items-center space-x-2.5">
|
||||
<Briefcase className="w-4 h-4 text-slate-400" />
|
||||
<div className="text-[11px] font-bold text-slate-600">Exp: {worker.exp}</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2.5">
|
||||
<Calendar className="w-4 h-4 text-slate-400" />
|
||||
<div className="text-[11px] font-bold text-slate-600">Avail: {worker.avail}</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2.5">
|
||||
<Heart className="w-4 h-4 text-slate-400" />
|
||||
<div className="text-[11px] font-bold text-slate-600">{worker.religion}</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2.5">
|
||||
<Sparkles className="w-4 h-4 text-slate-400" />
|
||||
<div className="text-[11px] font-bold text-slate-600">Age: {worker.age}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Skills Tags */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{worker.skills.map((skill) => (
|
||||
<span key={skill} className="px-3 py-1.5 bg-slate-50 text-slate-500 text-[10px] font-bold rounded-lg border border-slate-100">
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href={`/mobile/employer/workers/${worker.id}`}
|
||||
className="w-full py-4 bg-[#185FA5] text-white rounded-2xl font-bold text-sm tracking-tight active:scale-[0.98] transition-all flex items-center justify-center shadow-lg shadow-blue-500/20"
|
||||
>
|
||||
View Full Profile
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Filter Drawer - Simple UI Style */}
|
||||
{showFilters && (
|
||||
<div className="fixed inset-0 z-[100] flex items-end justify-center px-4 sm:px-0">
|
||||
<div className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm" onClick={() => setShowFilters(false)} />
|
||||
<div className="relative w-full max-w-md bg-white rounded-t-[40px] shadow-2xl p-8 pb-12 animate-in slide-in-from-bottom duration-300 overflow-y-auto max-h-[85vh] no-scrollbar">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div className="flex items-center space-x-3">
|
||||
<button onClick={resetFilters} className="p-2 text-slate-400 hover:text-[#185FA5] transition-colors">
|
||||
<RotateCcw className="w-5 h-5" />
|
||||
</button>
|
||||
<h2 className="text-xl font-bold">Filters</h2>
|
||||
</div>
|
||||
<button onClick={() => setShowFilters(false)} className="w-10 h-10 bg-slate-100 rounded-full flex items-center justify-center text-slate-500 hover:text-rose-500 transition-all">
|
||||
<X className="w-5 h-5" strokeWidth={3} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{/* Nationality */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-[11px] font-bold text-slate-400 uppercase tracking-widest ml-1">Nationality</label>
|
||||
<select
|
||||
value={filters.nationality}
|
||||
onChange={(e) => setFilters({...filters, nationality: e.target.value})}
|
||||
className="w-full px-5 py-4 bg-slate-50 border border-slate-100 rounded-2xl text-sm font-bold appearance-none focus:bg-white focus:border-[#185FA5] outline-none transition-all"
|
||||
>
|
||||
{filterOptions.nationalities.map(n => <option key={n} value={n}>{n}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Availability */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-[11px] font-bold text-slate-400 uppercase tracking-widest ml-1">Availability</label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{filterOptions.availabilities.map(av => (
|
||||
<button
|
||||
key={av}
|
||||
onClick={() => setFilters({...filters, availability: av})}
|
||||
className={`px-3 py-3 rounded-xl text-[10px] font-bold text-center border transition-all ${
|
||||
filters.availability === av
|
||||
? 'bg-[#185FA5] border-[#185FA5] text-white shadow-lg shadow-blue-500/20'
|
||||
: 'bg-white border-slate-100 text-slate-400'
|
||||
}`}
|
||||
>
|
||||
{av === 'All Availabilities' ? 'Any' : av.replace('Available ', '')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Experience */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-[11px] font-bold text-slate-400 uppercase tracking-widest ml-1">Experience</label>
|
||||
<select
|
||||
value={filters.experience}
|
||||
onChange={(e) => setFilters({...filters, experience: e.target.value})}
|
||||
className="w-full px-5 py-4 bg-slate-50 border border-slate-100 rounded-2xl text-sm font-bold appearance-none focus:bg-white focus:border-[#185FA5] outline-none transition-all"
|
||||
>
|
||||
{filterOptions.experienceLevels.map(ex => <option key={ex} value={ex}>{ex}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Salary Slider */}
|
||||
<div className="space-y-4 pt-2">
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<label className="text-[11px] font-bold text-slate-400 uppercase tracking-widest">Max Salary</label>
|
||||
<span className="text-sm font-bold text-[#185FA5]">{filters.maxSalary} AED</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="1200"
|
||||
max="3000"
|
||||
step="100"
|
||||
value={filters.maxSalary}
|
||||
onChange={(e) => setFilters({...filters, maxSalary: Number(e.target.value)})}
|
||||
className="w-full h-2 bg-slate-100 rounded-full appearance-none cursor-pointer accent-[#185FA5]"
|
||||
/>
|
||||
<div className="flex justify-between text-[10px] font-bold text-slate-300 uppercase px-1">
|
||||
<span>1200 AED</span>
|
||||
<span>3000 AED</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setShowFilters(false)}
|
||||
className="w-full py-5 bg-[#185FA5] text-white rounded-3xl font-bold text-base shadow-xl shadow-blue-500/30 active:scale-[0.98] transition-all mt-4"
|
||||
>
|
||||
Apply Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</EmployerMobileLayout>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user