adds sections as list tasks,log expenses. fees, notifications for sub contrcator

This commit is contained in:
Satyam Rastogi
2026-05-19 18:17:36 +05:30
parent c7a9849d1f
commit 2589975074
5 changed files with 2003 additions and 60 deletions
@@ -0,0 +1,197 @@
import React, { useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { SpotlightCard } from '../SpotlightCard';
import {
Bell, ClipboardList, MessageSquare, RefreshCcw, CheckCircle, AlertCircle,
} from 'lucide-react';
// Allowed notification types for the subcontractor dashboard.
// Keep keys in sync with mockStore notification.type values.
const NOTIF_CONFIG = {
task_assigned: {
label: 'New Task Assigned',
tone: 'text-blue-600 bg-blue-100 dark:text-blue-400 dark:bg-blue-500/15',
icon: ClipboardList,
side: 'border-l-blue-500',
},
message_received: {
label: 'Message Received',
tone: 'text-emerald-600 bg-emerald-100 dark:text-emerald-400 dark:bg-emerald-500/15',
icon: MessageSquare,
side: 'border-l-emerald-500',
},
task_updated: {
label: 'Task Updated',
tone: 'text-amber-600 bg-amber-100 dark:text-amber-400 dark:bg-amber-500/15',
icon: RefreshCcw,
side: 'border-l-amber-500',
},
status_changed: {
label: 'Status Changed',
tone: 'text-purple-600 bg-purple-100 dark:text-purple-400 dark:bg-purple-500/15',
icon: CheckCircle,
side: 'border-l-purple-500',
},
};
const ALLOWED_TYPES = Object.keys(NOTIF_CONFIG);
const formatDateTime = (iso) => {
if (!iso) return '—';
try {
const d = new Date(iso);
const dateStr = d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
const timeStr = d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
return `${dateStr} · ${timeStr}`;
} catch { return iso; }
};
const TABS = [
{ key: 'all', label: 'All' },
{ key: 'task_assigned', label: 'New Task' },
{ key: 'message_received', label: 'Message' },
{ key: 'task_updated', label: 'Updates' },
{ key: 'status_changed', label: 'Status' },
];
/**
* NotificationsPanel — global subcontractor notifications.
*
* Renders a SpotlightCard with tabbed filtering and a list of notification rows.
* Each row navigates to its referenced task (if any) when clicked.
*/
export const NotificationsPanel = ({ notifications, onMarkAllRead, onNotificationClick }) => {
const navigate = useNavigate();
const [tab, setTab] = useState('all');
const allowed = useMemo(
() => notifications.filter(n => ALLOWED_TYPES.includes(n.type)),
[notifications],
);
const filtered = useMemo(() => {
if (tab === 'all') return allowed;
return allowed.filter(n => n.type === tab);
}, [allowed, tab]);
const unreadCount = allowed.filter(n => !n.isRead).length;
const handleClick = (n) => {
if (onNotificationClick) onNotificationClick(n);
if (n.taskId) navigate(`/subcontractor/tasks/${n.taskId}`);
};
return (
<SpotlightCard className="p-5 sm:p-6">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2.5 min-w-0">
<div className="p-1.5 rounded-lg bg-blue-500/10 text-blue-500 shrink-0">
<Bell size={14} />
</div>
<h3 className="text-sm sm:text-base font-bold text-zinc-900 dark:text-white tracking-wide">
Notifications
</h3>
{unreadCount > 0 && (
<span className="text-[10px] font-bold px-2 py-0.5 rounded-full bg-blue-500/10 text-blue-600 dark:text-blue-400 font-mono shrink-0">
{unreadCount} new
</span>
)}
</div>
<button
type="button"
onClick={onMarkAllRead}
disabled={unreadCount === 0}
className="text-[11px] font-bold uppercase tracking-wider text-blue-600 dark:text-blue-400 hover:underline disabled:text-zinc-400 disabled:no-underline transition-colors shrink-0"
>
Mark all read
</button>
</div>
{/* Tab strip — scrollable on narrow screens */}
<div className="mt-4 -mx-1 px-1 overflow-x-auto custom-scrollbar">
<div className="inline-flex rounded-xl bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10 p-1 min-w-full">
{TABS.map(t => {
const count = t.key === 'all' ? allowed.length : allowed.filter(n => n.type === t.key).length;
return (
<TabPill
key={t.key}
active={tab === t.key}
onClick={() => setTab(t.key)}
label={t.label}
count={count}
/>
);
})}
</div>
</div>
<div className="mt-4 space-y-2 max-h-[36rem] overflow-y-auto pr-1 custom-scrollbar">
{filtered.length === 0 ? (
<div className="text-center py-10 text-zinc-500">
<Bell size={20} className="mx-auto mb-2 opacity-50" />
<p className="text-xs">You&apos;re all caught up.</p>
</div>
) : filtered.map(n => (
<NotificationRow key={n.id} notification={n} onClick={() => handleClick(n)} />
))}
</div>
<div className="mt-4 pt-3 border-t border-zinc-200 dark:border-white/10">
<p className="text-[10px] text-zinc-500 flex items-center gap-1.5">
<AlertCircle size={10} /> In-app only · No SMS or email unless specified.
</p>
</div>
</SpotlightCard>
);
};
const TabPill = ({ active, onClick, label, count }) => (
<button
type="button"
onClick={onClick}
className={`flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold whitespace-nowrap transition-colors
${active
? 'bg-white dark:bg-zinc-900 text-zinc-900 dark:text-white shadow-sm'
: 'text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-white'
}`}
>
{label}
{typeof count === 'number' && (
<span className={`text-[10px] font-mono px-1.5 rounded-full ${active ? 'bg-blue-100 text-blue-600 dark:bg-blue-500/20 dark:text-blue-400' : 'bg-zinc-200 dark:bg-white/10 text-zinc-500'}`}>
{count}
</span>
)}
</button>
);
const NotificationRow = ({ notification, onClick }) => {
const cfg = NOTIF_CONFIG[notification.type];
if (!cfg) return null;
const Icon = cfg.icon;
return (
<button
type="button"
onClick={onClick}
className={`w-full flex items-start gap-3 p-3 rounded-xl text-left border-l-2 ${cfg.side}
${notification.isRead
? 'bg-zinc-50 dark:bg-white/[0.02] border-y border-r border-zinc-100 dark:border-white/5'
: 'bg-white dark:bg-white/5 border-y border-r border-zinc-200 dark:border-white/10 shadow-sm'
}
hover:bg-zinc-100 dark:hover:bg-white/10 transition-colors`}
>
<div className={`p-1.5 rounded-lg shrink-0 ${cfg.tone}`}>
<Icon size={14} />
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-xs font-bold text-zinc-900 dark:text-white">{cfg.label}</span>
{!notification.isRead && <span className="w-1.5 h-1.5 rounded-full bg-blue-500" />}
</div>
<p className="text-[11px] text-zinc-600 dark:text-zinc-400 mt-0.5 line-clamp-2">{notification.message}</p>
<p className="text-[10px] text-zinc-500 font-mono mt-1">{formatDateTime(notification.createdAt)}</p>
</div>
</button>
);
};
export default NotificationsPanel;