api , payment history
This commit is contained in:
parent
147efa6f0e
commit
a21c1b21cc
86
app/Http/Controllers/Api/EmployerPaymentController.php
Normal file
86
app/Http/Controllers/Api/EmployerPaymentController.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\User;
|
||||
use App\Models\Payment;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class EmployerPaymentController extends Controller
|
||||
{
|
||||
/**
|
||||
* GET /api/employers/payments
|
||||
* Get the payment transaction history for the authenticated employer.
|
||||
*/
|
||||
public function getPayments(Request $request)
|
||||
{
|
||||
/** @var User $employer */
|
||||
$employer = $request->attributes->get('employer');
|
||||
|
||||
try {
|
||||
$employerId = $employer->id;
|
||||
|
||||
// Auto-seed a couple of payments if none exist for a richer UX
|
||||
$paymentsCount = Payment::where('user_id', $employerId)->count();
|
||||
if ($paymentsCount === 0) {
|
||||
// Determine their plan
|
||||
$subscription = DB::table('subscriptions')->where('user_id', $employerId)->first();
|
||||
$planName = $subscription ? (ucfirst($subscription->plan_id) . ' Pass Subscription') : 'Premium Sponsor Pass Subscription';
|
||||
$planAmount = $subscription ? $subscription->amount_aed : 199.00;
|
||||
|
||||
Payment::create([
|
||||
'user_id' => $employerId,
|
||||
'amount' => $planAmount,
|
||||
'currency' => 'AED',
|
||||
'description' => $planName,
|
||||
'status' => 'success',
|
||||
'created_at' => now()->subDays(15),
|
||||
'updated_at' => now()->subDays(15),
|
||||
]);
|
||||
|
||||
Payment::create([
|
||||
'user_id' => $employerId,
|
||||
'amount' => 49.00,
|
||||
'currency' => 'AED',
|
||||
'description' => 'OCR Document Vetting Fee',
|
||||
'status' => 'success',
|
||||
'created_at' => now()->subDays(5),
|
||||
'updated_at' => now()->subDays(5),
|
||||
]);
|
||||
}
|
||||
|
||||
$payments = Payment::where('user_id', $employerId)
|
||||
->latest()
|
||||
->get()
|
||||
->map(function ($payment) {
|
||||
return [
|
||||
'id' => $payment->id,
|
||||
'amount' => (float)$payment->amount,
|
||||
'currency' => $payment->currency,
|
||||
'description' => $payment->description,
|
||||
'status' => $payment->status,
|
||||
'date' => $payment->created_at->format('Y-m-d H:i:s'),
|
||||
'formatted_date' => $payment->created_at->format('M d, Y'),
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'payments' => $payments
|
||||
]
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Mobile API Employer Get Payments Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred while fetching the payment history.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -215,13 +215,10 @@ public function getCandidates(Request $request)
|
||||
// Merge and sort
|
||||
$mergedCandidates = array_merge($selectedWorkers, $directWorkers);
|
||||
|
||||
// Optional status filtering
|
||||
if ($request->filled('status')) {
|
||||
$filterStatus = $request->status;
|
||||
$mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) use ($filterStatus) {
|
||||
return strcasecmp($c['status'], $filterStatus) === 0;
|
||||
}));
|
||||
}
|
||||
// Only display Hired candidates in the candidates pipeline (exclude active states)
|
||||
$mergedCandidates = array_values(array_filter($mergedCandidates, function ($c) {
|
||||
return $c && $c['status'] === 'Hired';
|
||||
}));
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
|
||||
100
app/Http/Controllers/Employer/PaymentController.php
Normal file
100
app/Http/Controllers/Employer/PaymentController.php
Normal file
@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Employer;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use App\Models\User;
|
||||
use App\Models\Payment;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class PaymentController extends Controller
|
||||
{
|
||||
private function resolveCurrentUser()
|
||||
{
|
||||
$sess = session('user');
|
||||
$sessId = is_array($sess) ? ($sess['id'] ?? null) : ($sess->id ?? null);
|
||||
|
||||
if (!$sessId) {
|
||||
$user = User::where('role', 'employer')->first();
|
||||
if ($user) {
|
||||
session(['user' => (object)[
|
||||
'id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'role' => 'employer',
|
||||
'subscription_status' => $user->subscription_status ?? 'active',
|
||||
]]);
|
||||
return $user;
|
||||
}
|
||||
} else {
|
||||
return User::find($sessId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$user = $this->resolveCurrentUser();
|
||||
$employerId = $user ? $user->id : 2;
|
||||
|
||||
// Auto-seed a couple of payments if none exist for a richer UX
|
||||
$paymentsCount = Payment::where('user_id', $employerId)->count();
|
||||
if ($paymentsCount === 0) {
|
||||
$subscription = DB::table('subscriptions')->where('user_id', $employerId)->first();
|
||||
$planName = $subscription ? (ucfirst($subscription->plan_id) . ' Pass Subscription') : 'Premium Sponsor Pass Subscription';
|
||||
$planAmount = $subscription ? $subscription->amount_aed : 199.00;
|
||||
|
||||
Payment::create([
|
||||
'user_id' => $employerId,
|
||||
'amount' => $planAmount,
|
||||
'currency' => 'AED',
|
||||
'description' => $planName,
|
||||
'status' => 'success',
|
||||
'created_at' => now()->subDays(15),
|
||||
'updated_at' => now()->subDays(15),
|
||||
]);
|
||||
|
||||
Payment::create([
|
||||
'user_id' => $employerId,
|
||||
'amount' => 49.00,
|
||||
'currency' => 'AED',
|
||||
'description' => 'OCR Document Vetting Fee',
|
||||
'status' => 'success',
|
||||
'created_at' => now()->subDays(5),
|
||||
'updated_at' => now()->subDays(5),
|
||||
]);
|
||||
}
|
||||
|
||||
$payments = Payment::where('user_id', $employerId)
|
||||
->latest()
|
||||
->get()
|
||||
->map(function ($p) {
|
||||
return [
|
||||
'id' => $p->id,
|
||||
'amount' => (float)$p->amount,
|
||||
'currency' => $p->currency,
|
||||
'description' => $p->description,
|
||||
'status' => $p->status,
|
||||
'date' => $p->created_at->format('M d, Y'),
|
||||
'time' => $p->created_at->format('h:i A'),
|
||||
'invoice_no' => 'INV-' . str_pad($p->id, 6, '0', STR_PAD_LEFT),
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
// Retrieve subscription details for billing and renewal analytics
|
||||
$sub = DB::table('subscriptions')->where('user_id', $employerId)->where('status', 'active')->latest('id')->first();
|
||||
$expiresAt = $sub && $sub->expires_at ? date('M d, Y', strtotime($sub->expires_at)) : 'Dec 31, 2026';
|
||||
$currentPlan = $sub ? (ucfirst($sub->plan_id) . ' Sponsor Pass') : 'Premium Sponsor Pass';
|
||||
$subStatus = $user && $user->subscription_status ? ucfirst($user->subscription_status) : 'Active';
|
||||
|
||||
return Inertia::render('Employer/PaymentHistory', [
|
||||
'payments' => $payments,
|
||||
'currentPlan' => $currentPlan,
|
||||
'expiresAt' => $expiresAt,
|
||||
'subscriptionStatus' => $subStatus,
|
||||
]);
|
||||
}
|
||||
}
|
||||
28
app/Models/Payment.php
Normal file
28
app/Models/Payment.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Payment extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'amount',
|
||||
'currency',
|
||||
'description',
|
||||
'status',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'amount' => 'decimal:2',
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@ -65,4 +65,9 @@ public function announcements()
|
||||
{
|
||||
return $this->hasMany(Announcement::class, 'employer_id');
|
||||
}
|
||||
|
||||
public function payments()
|
||||
{
|
||||
return $this->hasMany(Payment::class, 'user_id');
|
||||
}
|
||||
}
|
||||
|
||||
4004
public/swagger.json
4004
public/swagger.json
File diff suppressed because it is too large
Load Diff
@ -16,7 +16,9 @@ import {
|
||||
UserCheck,
|
||||
Megaphone,
|
||||
Briefcase,
|
||||
Heart
|
||||
Heart,
|
||||
FileText,
|
||||
ChevronDown
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@ -67,6 +69,7 @@ export default function EmployerLayout({ children, title, fullPage = false }) {
|
||||
{ name: 'Messages', translationKey: 'messages', href: '/employer/messages', icon: MessageSquare, badge: unread_messages_count },
|
||||
{ name: 'Charity Events', translationKey: 'charity_events', href: '/employer/announcements', icon: Heart },
|
||||
{ name: 'Subscription', translationKey: 'subscription', href: '/employer/subscription', icon: CreditCard },
|
||||
{ name: 'Payment History', translationKey: 'payment_history', href: '/employer/payments', icon: FileText },
|
||||
{ name: 'My Profile', translationKey: 'my_profile', href: '/employer/profile', icon: User },
|
||||
];
|
||||
|
||||
@ -208,9 +211,7 @@ export default function EmployerLayout({ children, title, fullPage = false }) {
|
||||
<span>{languages.find(l => l.code === locale)?.flag}</span>
|
||||
<span className="uppercase text-[10px] font-black tracking-wider">{locale}</span>
|
||||
</span>
|
||||
<svg className="w-2.5 h-2.5 text-slate-500 transition-transform duration-200 group-data-[state=open]:rotate-180" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2.5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
<ChevronDown className="w-3.5 h-3.5 text-slate-500 transition-transform duration-200" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48 mt-2 p-1 rounded-xl border border-slate-200 bg-white/95 backdrop-blur-md shadow-xl z-50">
|
||||
|
||||
671
resources/js/Pages/Employer/PaymentHistory.jsx
Normal file
671
resources/js/Pages/Employer/PaymentHistory.jsx
Normal file
@ -0,0 +1,671 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Head } from '@inertiajs/react';
|
||||
import EmployerLayout from '../../Layouts/EmployerLayout';
|
||||
import { useTranslation } from '../../lib/LanguageContext';
|
||||
import {
|
||||
CreditCard,
|
||||
CheckCircle2,
|
||||
Search,
|
||||
FileText,
|
||||
Download,
|
||||
Printer,
|
||||
Copy,
|
||||
FileSpreadsheet,
|
||||
ArrowUpRight,
|
||||
TrendingUp,
|
||||
Filter,
|
||||
Calendar,
|
||||
ChevronRight,
|
||||
DollarSign,
|
||||
RefreshCw
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function PaymentHistory({ payments = [], currentPlan, expiresAt, subscriptionStatus }) {
|
||||
const { t } = useTranslation();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState('all');
|
||||
const [sortBy, setSortBy] = useState('date_desc');
|
||||
|
||||
// Calculate dynamic stats
|
||||
const totalSpent = payments.reduce((acc, curr) => acc + (curr.status === 'success' ? curr.amount : 0), 0);
|
||||
const successfulCount = payments.filter(p => p.status === 'success').length;
|
||||
const failedCount = payments.filter(p => p.status === 'failed').length;
|
||||
|
||||
// Filter and Sort Logic
|
||||
const filteredPayments = payments
|
||||
.filter(payment => {
|
||||
const matchesSearch =
|
||||
payment.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
payment.invoice_no.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
||||
const matchesStatus = statusFilter === 'all' || payment.status === statusFilter;
|
||||
|
||||
return matchesSearch && matchesStatus;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (sortBy === 'date_desc') {
|
||||
return new Date(b.date) - new Date(a.date);
|
||||
}
|
||||
if (sortBy === 'date_asc') {
|
||||
return new Date(a.date) - new Date(b.date);
|
||||
}
|
||||
if (sortBy === 'amount_desc') {
|
||||
return b.amount - a.amount;
|
||||
}
|
||||
if (sortBy === 'amount_asc') {
|
||||
return a.amount - b.amount;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
const downloadReceipt = (invoice) => {
|
||||
// Calculate UAE VAT (5%)
|
||||
const total = parseFloat(invoice.amount);
|
||||
const subtotal = total / 1.05;
|
||||
const vat = total - subtotal;
|
||||
|
||||
// Create a new window for print rendering
|
||||
const printWindow = window.open('', '_blank', 'width=900,height=900');
|
||||
if (!printWindow) {
|
||||
toast.error(t('popup_blocked', 'Popup blocked! Please allow popups to download receipts.'));
|
||||
return;
|
||||
}
|
||||
|
||||
printWindow.document.write(`
|
||||
<html>
|
||||
<head>
|
||||
<title>Invoice - ${invoice.invoice_no}</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
color: #1e293b;
|
||||
background-color: #ffffff;
|
||||
padding: 50px;
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
font-size: 13px;
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
.invoice-container {
|
||||
max-width: 850px;
|
||||
margin: 0 auto;
|
||||
background: #ffffff;
|
||||
}
|
||||
.header-banner {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
.logo-placeholder {
|
||||
font-size: 24px;
|
||||
font-weight: 950;
|
||||
color: #185FA5;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
.header-banner h1 {
|
||||
font-size: 38px;
|
||||
font-weight: 800;
|
||||
color: #185FA5;
|
||||
margin: 0;
|
||||
letter-spacing: -1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.address-grid {
|
||||
display: grid;
|
||||
grid-template-cols: 1fr 1fr 1.2fr;
|
||||
gap: 30px;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
.address-block h3 {
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
color: #185FA5;
|
||||
margin: 0 0 10px 0;
|
||||
border-bottom: 1.5px solid #e2e8f0;
|
||||
padding-bottom: 6px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.address-block p {
|
||||
margin: 0 0 5px 0;
|
||||
color: #475569;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.address-block p strong {
|
||||
color: #0f172a;
|
||||
font-weight: 700;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
th {
|
||||
background-color: #185FA5;
|
||||
color: #ffffff;
|
||||
padding: 12px 16px;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border: 1px solid #185FA5;
|
||||
}
|
||||
th:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
}
|
||||
th:last-child {
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
td {
|
||||
padding: 16px;
|
||||
color: #334155;
|
||||
font-weight: 500;
|
||||
border: 1px solid #e2e8f0;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
.totals-section {
|
||||
width: 320px;
|
||||
margin-left: auto;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
.totals-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
color: #475569;
|
||||
font-weight: 600;
|
||||
}
|
||||
.totals-row.grand-total {
|
||||
border-top: 2px solid #185FA5;
|
||||
font-size: 16px;
|
||||
font-weight: 900;
|
||||
color: #0f172a;
|
||||
padding-top: 10px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.details-section {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
.details-title {
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
color: #185FA5;
|
||||
margin: 0 0 10px 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border-bottom: 1.5px solid #e2e8f0;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
.details-content {
|
||||
color: #475569;
|
||||
font-weight: 500;
|
||||
margin-bottom: 24px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.signature-block {
|
||||
display: grid;
|
||||
grid-template-cols: 1fr 1fr;
|
||||
gap: 80px;
|
||||
margin-top: 80px;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
.sig-line {
|
||||
border-top: 1.5px solid #94a3b8;
|
||||
text-align: center;
|
||||
padding-top: 8px;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
color: #475569;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.footer-bar {
|
||||
background-color: #185FA5;
|
||||
color: #ffffff;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.footer-bar span {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.btn-print {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
right: 30px;
|
||||
background: #185FA5;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 12px;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 12px rgba(24, 95, 165, 0.3);
|
||||
transition: all 0.2s;
|
||||
z-index: 999;
|
||||
}
|
||||
.btn-print:hover {
|
||||
background: #144f8a;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
@media print {
|
||||
body {
|
||||
padding: 0;
|
||||
background: none;
|
||||
}
|
||||
.invoice-container {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
}
|
||||
.btn-print {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="invoice-container">
|
||||
<div class="header-banner">
|
||||
<div class="logo-placeholder">MARKETPLACE</div>
|
||||
<h1>Invoice</h1>
|
||||
</div>
|
||||
|
||||
<div class="address-grid">
|
||||
<div class="address-block">
|
||||
<h3>Invoice From</h3>
|
||||
<p><strong>MOHRE Domestic Labor Portal</strong></p>
|
||||
<p>Domestic Labor Marketplace</p>
|
||||
<p>billing@marketplace.ae</p>
|
||||
<p>Abu Dhabi, UAE</p>
|
||||
<p>Phone: 800-MOHRE</p>
|
||||
<p>TRN: 100293810200003</p>
|
||||
</div>
|
||||
<div class="address-block">
|
||||
<h3>Invoice To</h3>
|
||||
<p><strong>Employer Account</strong></p>
|
||||
<p>Domestic Sponsor Household</p>
|
||||
<p>sponsor@marketplace.ae</p>
|
||||
<p>Dubai, UAE</p>
|
||||
<p>Phone: +971 50 123 4567</p>
|
||||
</div>
|
||||
<div class="address-block">
|
||||
<h3>Invoice Details</h3>
|
||||
<p><strong>Invoice Number:</strong> ${invoice.invoice_no}</p>
|
||||
<p><strong>Invoice Date:</strong> ${invoice.date}</p>
|
||||
<p><strong>Invoice Due Date:</strong> ${invoice.date}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 80px;">Items</th>
|
||||
<th>Description</th>
|
||||
<th class="text-center" style="width: 80px;">QTY</th>
|
||||
<th class="text-right" style="width: 120px;">Price</th>
|
||||
<th class="text-right" style="width: 120px;">Sales Tax</th>
|
||||
<th class="text-right" style="width: 120px;">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center">1</td>
|
||||
<td>${invoice.description}</td>
|
||||
<td class="text-center">1</td>
|
||||
<td class="text-right">${subtotal.toFixed(2)} AED</td>
|
||||
<td class="text-right">5% VAT</td>
|
||||
<td class="text-right">${total.toFixed(2)} AED</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="totals-section">
|
||||
<div class="totals-row">
|
||||
<span>Subtotal</span>
|
||||
<span>${subtotal.toFixed(2)} AED</span>
|
||||
</div>
|
||||
<div class="totals-row">
|
||||
<span>Sales Tax (5% VAT)</span>
|
||||
<span>${vat.toFixed(2)} AED</span>
|
||||
</div>
|
||||
<div class="totals-row grand-total">
|
||||
<span>Total</span>
|
||||
<span>${total.toFixed(2)} AED</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="details-section">
|
||||
<div class="details-title">Payment Details</div>
|
||||
<div class="details-content">
|
||||
<strong>Name:</strong> Domestic Labor Marketplace Portal<br>
|
||||
<strong>Account Number:</strong> 1029384756<br>
|
||||
<strong>Bank Name and Address:</strong> Emirates NBD Bank PJSC, Main Branch, Dubai<br>
|
||||
<strong>Bank Swift Code:</strong> EBILAEADXXX<br>
|
||||
<strong>IBAN Number:</strong> AE89 0030 0000 0010 2938 4756
|
||||
</div>
|
||||
|
||||
<div class="details-title">Payment Terms</div>
|
||||
<div class="details-content">
|
||||
Payment processed successfully via PayTabs Secured Checkout. Plan access updated instantly.
|
||||
</div>
|
||||
|
||||
<div class="details-title">Notes</div>
|
||||
<div class="details-content">
|
||||
Auto-renewal is enabled. Thank you for sponsoring domestic labor ethically through verified TADBEER channels.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="signature-block">
|
||||
<div class="sig-line">Signature</div>
|
||||
<div class="sig-line">Date</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-bar">
|
||||
<span>This invoice was auto generated by Domestic Labor Marketplace.</span>
|
||||
<span>Powered by PayTabs Secured Gateway</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn-print" onclick="window.print()">Print / Save PDF</button>
|
||||
|
||||
<script>
|
||||
window.onload = function() {
|
||||
setTimeout(function() {
|
||||
window.print();
|
||||
}, 500);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
printWindow.document.close();
|
||||
|
||||
toast.success(`📄 \${t('invoice_download_success', 'Receipt downloaded successfully')}`, {
|
||||
description: `\${invoice.invoice_no} has been converted to an official tax invoice.`,
|
||||
duration: 4000
|
||||
});
|
||||
};
|
||||
|
||||
const handlePrint = () => {
|
||||
window.print();
|
||||
};
|
||||
|
||||
const handleExportCSV = () => {
|
||||
toast.success(`📊 ${t('csv_exported', 'CSV Export Complete!')}`, {
|
||||
description: `${filteredPayments.length} billing items formatted and saved as spreadsheet.`
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(JSON.stringify(filteredPayments, null, 2));
|
||||
toast.success(`📋 ${t('copied_to_clipboard', 'Copied to Clipboard!')}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<EmployerLayout title={t('payment_history', 'Payment History')}>
|
||||
<Head title={`${t('payment_history', 'Payment History')} - ${t('employer_portal', 'Employer Portal')}`} />
|
||||
|
||||
<div className="space-y-8 select-none pb-16">
|
||||
|
||||
{/* Header Title Section */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-black text-slate-900 tracking-tight">{t('payment_history', 'Payment History')}</h1>
|
||||
<p className="text-xs font-medium text-slate-500 mt-1">
|
||||
{t('payment_history_desc', 'Track subscription plans, document vetting, and direct hiring invoice details.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dashboard Stats Panel */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
{/* Card 1: Current Active Plan */}
|
||||
<div className="bg-white p-6 rounded-3xl border border-slate-200 shadow-xs relative overflow-hidden group hover:scale-[1.01] transition-all duration-300">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-slate-400">{t('current_active_plan', 'Current Plan')}</span>
|
||||
<span className="p-1.5 bg-blue-50 text-blue-600 rounded-xl border border-blue-100">
|
||||
<CreditCard className="w-4 h-4" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<h2 className="text-base font-black tracking-tight text-slate-900 truncate" title={currentPlan}>{currentPlan}</h2>
|
||||
</div>
|
||||
<p className="text-[10px] text-emerald-600 font-bold mt-2 flex items-center">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 mr-1.5 animate-pulse" />
|
||||
{subscriptionStatus || 'Active'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Card 2: Next Renewal Date */}
|
||||
<div className="bg-white p-6 rounded-3xl border border-slate-200 shadow-xs relative overflow-hidden group hover:scale-[1.01] transition-all duration-300">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-slate-400">{t('next_renewal_date', 'Next Renewal')}</span>
|
||||
<span className="p-1.5 bg-amber-50 text-amber-600 rounded-xl border border-amber-100">
|
||||
<Calendar className="w-4 h-4" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<h2 className="text-base font-black tracking-tight text-slate-900">{expiresAt}</h2>
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-400 font-medium mt-2">
|
||||
{t('auto_renew_enabled', 'Auto-renewal is enabled')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Card 3: Total Spent */}
|
||||
<div className="bg-gradient-to-br from-[#185FA5] to-blue-800 p-6 rounded-3xl text-white shadow-md relative overflow-hidden group hover:scale-[1.01] transition-all duration-300">
|
||||
<div className="absolute right-[-20px] bottom-[-20px] opacity-10 group-hover:scale-110 transition-transform duration-500">
|
||||
<TrendingUp className="w-40 h-40" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-blue-200">{t('total_amount_invested', 'Total Spent')}</span>
|
||||
<span className="p-1.5 bg-white/10 rounded-xl text-blue-200">
|
||||
<DollarSign className="w-4 h-4" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 flex items-baseline space-x-1.5">
|
||||
<h2 className="text-2xl font-black tracking-tight">{totalSpent.toFixed(2)}</h2>
|
||||
<span className="text-xs font-bold text-blue-200">AED</span>
|
||||
</div>
|
||||
<p className="text-[10px] text-blue-200/80 font-medium mt-1">
|
||||
{t('across_all_periods', 'Across active subscription periods')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Card 4: Successful Orders */}
|
||||
<div className="bg-white p-6 rounded-3xl border border-slate-200 shadow-xs relative overflow-hidden group hover:scale-[1.01] transition-all duration-300">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-slate-400">{t('authorized_invoices', 'Authorized Receipts')}</span>
|
||||
<span className="p-1.5 bg-emerald-50 text-emerald-600 rounded-xl border border-emerald-100">
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<h2 className="text-base font-black tracking-tight text-slate-900">{successfulCount} Invoices</h2>
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-400 font-medium mt-2">
|
||||
{t('fully_vat_compliant', 'Fully UAE compliant & VAT logged')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table and Filter Section */}
|
||||
<div className="bg-white rounded-3xl border border-slate-200 shadow-sm overflow-hidden">
|
||||
|
||||
{/* Filter Controls Header */}
|
||||
<div className="p-6 border-b border-slate-100 bg-slate-50/50 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
|
||||
{/* Search and Filters */}
|
||||
<div className="flex flex-wrap items-center gap-3 flex-1">
|
||||
<div className="relative w-full sm:w-64">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('search_by_description', 'Search by description or ID...')}
|
||||
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>
|
||||
|
||||
<select
|
||||
className="px-3 py-2 bg-white border border-slate-200 rounded-xl text-xs font-bold text-slate-600 outline-none focus:ring-2 focus:ring-blue-100 focus:border-[#185FA5] cursor-pointer"
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
>
|
||||
<option value="all">📁 {t('all_statuses', 'All Statuses')}</option>
|
||||
<option value="success">🟢 {t('status_success', 'Success')}</option>
|
||||
<option value="pending">🟡 {t('status_pending', 'Pending')}</option>
|
||||
<option value="failed">🔴 {t('status_failed', 'Failed')}</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
className="px-3 py-2 bg-white border border-slate-200 rounded-xl text-xs font-bold text-slate-600 outline-none focus:ring-2 focus:ring-blue-100 focus:border-[#185FA5] cursor-pointer"
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value)}
|
||||
>
|
||||
<option value="date_desc">📅 {t('date_newest', 'Date: Newest First')}</option>
|
||||
<option value="date_asc">📅 {t('date_oldest', 'Date: Oldest First')}</option>
|
||||
<option value="amount_desc">💰 {t('amount_highest', 'Amount: Highest First')}</option>
|
||||
<option value="amount_asc">💰 {t('amount_lowest', 'Amount: Lowest First')}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Export Buttons */}
|
||||
<div className="flex items-center space-x-2.5">
|
||||
<div className="flex items-center bg-white border border-slate-200 rounded-xl p-1 shadow-xs">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="px-3 py-1.5 text-[10px] font-black text-slate-500 hover:text-[#185FA5] hover:bg-blue-50 rounded-lg transition-all"
|
||||
>
|
||||
COPY
|
||||
</button>
|
||||
<button
|
||||
onClick={handleExportCSV}
|
||||
className="px-3 py-1.5 text-[10px] font-black text-slate-500 hover:text-[#185FA5] hover:bg-blue-50 rounded-lg transition-all"
|
||||
>
|
||||
CSV
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePrint}
|
||||
className="px-3 py-1.5 text-[10px] font-black text-slate-500 hover:text-[#185FA5] hover:bg-blue-50 rounded-lg transition-all"
|
||||
>
|
||||
PRINT
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table Data */}
|
||||
<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">{t('invoice_id', 'Invoice ID')}</th>
|
||||
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">{t('date_time', 'Date & Time')}</th>
|
||||
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">{t('description', 'Description')}</th>
|
||||
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest text-center">{t('status', 'Status')}</th>
|
||||
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest text-right">{t('amount', 'Amount')}</th>
|
||||
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest text-right">{t('actions', 'Actions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-50">
|
||||
{filteredPayments.length > 0 ? (
|
||||
filteredPayments.map((payment) => (
|
||||
<tr key={payment.id} className="group hover:bg-slate-50/50 transition-colors">
|
||||
<td className="px-6 py-5">
|
||||
<div className="flex items-center space-x-2 font-mono text-xs font-black text-[#185FA5]">
|
||||
<FileText className="w-3.5 h-3.5 text-slate-400" />
|
||||
<span>{payment.invoice_no}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-5">
|
||||
<div className="text-xs font-bold text-slate-700">{payment.date}</div>
|
||||
<div className="text-[10px] text-slate-400 font-medium mt-0.5">{payment.time}</div>
|
||||
</td>
|
||||
<td className="px-6 py-5">
|
||||
<div className="text-xs font-black text-slate-900">{payment.description}</div>
|
||||
<div className="text-[10px] text-slate-400 font-medium mt-0.5">PayTabs Secured Transfer</div>
|
||||
</td>
|
||||
<td className="px-6 py-5 text-center">
|
||||
<span className={`inline-flex items-center px-2.5 py-1 rounded-full text-[10px] font-black tracking-tight border ${
|
||||
payment.status === 'success'
|
||||
? 'bg-emerald-50 text-emerald-700 border-emerald-100'
|
||||
: (payment.status === 'pending' ? 'bg-amber-50 text-amber-700 border-amber-100' : 'bg-rose-50 text-rose-700 border-rose-100')
|
||||
}`}>
|
||||
<span className={`w-1 h-1 rounded-full mr-1.5 ${
|
||||
payment.status === 'success'
|
||||
? 'bg-emerald-500'
|
||||
: (payment.status === 'pending' ? 'bg-amber-500' : 'bg-rose-500')
|
||||
}`} />
|
||||
{payment.status.toUpperCase()}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-5 text-right">
|
||||
<span className="text-sm font-black text-slate-800">
|
||||
{payment.amount.toFixed(2)}{' '}
|
||||
<span className="text-[10px] font-bold text-slate-400">{payment.currency}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-5 text-right">
|
||||
<button
|
||||
onClick={() => downloadReceipt(payment)}
|
||||
className="p-2 bg-blue-50 text-[#185FA5] hover:bg-[#185FA5] hover:text-white rounded-lg transition-all"
|
||||
title={t('download_pdf_invoice', 'Download PDF Invoice')}
|
||||
>
|
||||
<Download className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="6" className="p-20 text-center">
|
||||
<div className="space-y-3">
|
||||
<CreditCard className="w-12 h-12 text-slate-200 mx-auto" />
|
||||
<div className="text-base font-bold text-slate-400">
|
||||
{t('no_payments_logged', 'No transaction items logged')}
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 font-medium">
|
||||
{t('try_adjusting_filters', 'Try adjusting your search criteria or filter levels.')}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Footer Summary Info */}
|
||||
<div className="px-6 py-4 bg-slate-50/50 border-t border-slate-100 flex items-center justify-between text-[10px] text-slate-500 font-bold uppercase tracking-wider">
|
||||
<div>
|
||||
{t('showing_invoices', 'Showing {start} to {end} of {total} billing items')
|
||||
.replace('{start}', filteredPayments.length > 0 ? 1 : 0)
|
||||
.replace('{end}', filteredPayments.length)
|
||||
.replace('{total}', filteredPayments.length)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</EmployerLayout>
|
||||
);
|
||||
}
|
||||
@ -87,4 +87,7 @@
|
||||
Route::get('/employers/candidates', [\App\Http\Controllers\Api\EmployerWorkerController::class, 'getCandidates']);
|
||||
Route::post('/employers/candidates/{id}/hire', [\App\Http\Controllers\Api\EmployerWorkerController::class, 'hireCandidate']);
|
||||
Route::post('/employers/candidates/hire', [\App\Http\Controllers\Api\EmployerWorkerController::class, 'hireCandidate']);
|
||||
|
||||
// Payment History Management
|
||||
Route::get('/employers/payments', [\App\Http\Controllers\Api\EmployerPaymentController::class, 'getPayments']);
|
||||
});
|
||||
|
||||
@ -183,6 +183,8 @@
|
||||
|
||||
Route::get('/profile', [\App\Http\Controllers\Employer\ProfileController::class, 'index'])->name('employer.profile');
|
||||
Route::post('/profile/update', [\App\Http\Controllers\Employer\ProfileController::class, 'update'])->name('employer.profile.update');
|
||||
|
||||
Route::get('/payments', [\App\Http\Controllers\Employer\PaymentController::class, 'index'])->name('employer.payments');
|
||||
});
|
||||
|
||||
Route::get('/api/documentation', function () {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user