156 lines
8.2 KiB
TypeScript
156 lines
8.2 KiB
TypeScript
import { usePage, Link } from '@inertiajs/react';
|
|
import { PageProps, RetentionReminderItem } from '@/types';
|
|
import { useState } from 'react';
|
|
import { AlertCircle, CheckCircle2, ChevronRight, DollarSign, Send, UploadCloud, X } from 'lucide-react';
|
|
import { Button } from '@/Components/ui/button';
|
|
import { Badge } from '@/Components/ui/badge';
|
|
import RetentionPaymentProofModal from './RetentionPaymentProofModal';
|
|
|
|
const formatCurrency = (v: string | number) =>
|
|
new Intl.NumberFormat('en-PH', { style: 'currency', currency: 'PHP' }).format(Number(v || 0));
|
|
|
|
export default function RetentionStickyToast() {
|
|
const { props } = usePage<PageProps>();
|
|
const reminders = props.retention_reminders;
|
|
const [dismissed, setDismissed] = useState<string[]>([]);
|
|
const [activeProofModalInvoice, setActiveProofModalInvoice] = useState<RetentionReminderItem | null>(null);
|
|
|
|
if (!reminders || reminders.count === 0) return null;
|
|
|
|
const visibleItems = reminders.items.filter((item) => !dismissed.includes(item.id));
|
|
if (visibleItems.length === 0) return null;
|
|
|
|
const currentItem = visibleItems[0];
|
|
const isContractor = currentItem.role_target === 'contractor';
|
|
|
|
return (
|
|
<>
|
|
<div className="fixed bottom-6 right-6 z-50 max-w-md w-full animate-in fade-in slide-in-from-bottom-5 duration-300">
|
|
<div
|
|
className={`rounded-2xl border shadow-2xl p-4 backdrop-blur-md transition-all ${
|
|
isContractor
|
|
? 'bg-amber-50/95 border-amber-300/80 text-amber-950 shadow-amber-500/10'
|
|
: 'bg-indigo-50/95 border-indigo-300/80 text-indigo-950 shadow-indigo-500/10'
|
|
}`}
|
|
>
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="flex items-start gap-3">
|
|
<div
|
|
className={`p-2.5 rounded-xl shrink-0 mt-0.5 ${
|
|
isContractor ? 'bg-amber-200/80 text-amber-800' : 'bg-indigo-200/80 text-indigo-800'
|
|
}`}
|
|
>
|
|
{isContractor ? (
|
|
<Send className="h-5 w-5 animate-pulse" />
|
|
) : (
|
|
<DollarSign className="h-5 w-5 animate-pulse" />
|
|
)}
|
|
</div>
|
|
<div className="space-y-1">
|
|
<div className="flex items-center gap-2">
|
|
<h4 className="font-bold text-xs uppercase tracking-wider">
|
|
{currentItem.title}
|
|
</h4>
|
|
<Badge
|
|
variant="outline"
|
|
className={`text-[10px] px-1.5 py-0 font-bold uppercase ${
|
|
isContractor
|
|
? 'bg-amber-100 text-amber-900 border-amber-300'
|
|
: 'bg-indigo-100 text-indigo-900 border-indigo-300'
|
|
}`}
|
|
>
|
|
{isContractor ? 'Awaiting 10% Proof' : 'Proof Received'}
|
|
</Badge>
|
|
</div>
|
|
<p className="text-xs text-slate-700 leading-relaxed font-medium">
|
|
{currentItem.message}
|
|
</p>
|
|
<div className="flex items-center gap-2 pt-1 text-[11px] font-semibold text-slate-600">
|
|
<span>Amount:</span>
|
|
<span className="font-bold text-slate-900 font-mono">
|
|
{formatCurrency(currentItem.amount)}
|
|
</span>
|
|
{currentItem.retention_amount && (
|
|
<span className={`font-mono font-bold ${isContractor ? 'text-rose-700' : 'text-emerald-700'}`}>
|
|
(10% Ret: {isContractor ? '-' : '+'}{formatCurrency(currentItem.retention_amount)})
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="icon-xs"
|
|
onClick={() => setDismissed((prev) => [...prev, currentItem.id])}
|
|
className="text-slate-400 hover:text-slate-700 shrink-0 -mt-1 -mr-1"
|
|
title="Dismiss reminder"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Action Bar */}
|
|
<div className="mt-3.5 pt-3 border-t border-slate-200/80 flex items-center justify-between gap-2">
|
|
{visibleItems.length > 1 ? (
|
|
<span className="text-[10px] text-slate-500 font-medium">
|
|
+{visibleItems.length - 1} more pending action{visibleItems.length > 2 ? 's' : ''}
|
|
</span>
|
|
) : (
|
|
<span className="text-[10px] text-slate-500 font-medium">
|
|
Project: {currentItem.project_name}
|
|
</span>
|
|
)}
|
|
|
|
<div className="flex items-center gap-2">
|
|
{isContractor ? (
|
|
<Button
|
|
size="sm"
|
|
onClick={() => setActiveProofModalInvoice(currentItem)}
|
|
className="bg-amber-600 hover:bg-amber-700 text-white font-semibold text-xs shadow-xs"
|
|
>
|
|
<UploadCloud className="mr-1.5 h-3.5 w-3.5" /> Send Payment Proof
|
|
</Button>
|
|
) : (
|
|
<Link href={currentItem.view_url}>
|
|
<Button
|
|
size="sm"
|
|
className="bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-xs shadow-xs"
|
|
>
|
|
<CheckCircle2 className="mr-1.5 h-3.5 w-3.5" /> Check & Confirm
|
|
</Button>
|
|
</Link>
|
|
)}
|
|
|
|
<Link href={currentItem.view_url}>
|
|
<Button variant="outline" size="sm" className="text-xs bg-white">
|
|
Details <ChevronRight className="ml-1 h-3 w-3" />
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick-Action Modal for Contractor Admin */}
|
|
{activeProofModalInvoice && (
|
|
<RetentionPaymentProofModal
|
|
isOpen={!!activeProofModalInvoice}
|
|
onClose={() => setActiveProofModalInvoice(null)}
|
|
invoice={{
|
|
ulid: activeProofModalInvoice.ulid,
|
|
invoice_number: activeProofModalInvoice.invoice_number,
|
|
total_amount: activeProofModalInvoice.amount,
|
|
retention_amount: activeProofModalInvoice.retention_amount,
|
|
retention_rate: activeProofModalInvoice.retention_rate || 10,
|
|
project: {
|
|
name: activeProofModalInvoice.project_name,
|
|
code: '',
|
|
},
|
|
}}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|