2026-10-09 12:16:48 +05:30

596 lines
23 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useState, useMemo } from 'react'
import { InboxItem, Page } from '../types'
import { getInboxItems, respondToDelegation } from '../api'
import { colors } from '../constants'
import Pagination, { PAGE_SIZE } from '../components/Pagination'
interface InboxPageProps {
onNavigate: (page: Page, name?: string) => void
onRefresh?: () => void
}
const typeToPage: Record<string, Page> = {
process: 'processes-form',
order: 'orders-form',
incident: 'incidents-form',
idea: 'ideas-form',
}
const typeColor: Record<string, string> = {
process: '#3b82f6',
order: '#8b5cf6',
incident: '#ef4444',
idea: '#10b981',
delegation: '#f59e0b',
}
const typeGradient: Record<string, string> = {
process: 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)',
order: 'linear-gradient(135deg, #8b5cf6 0%, #6d28d9 100%)',
incident: 'linear-gradient(135deg, #ef4444 0%, #b91c1c 100%)',
idea: 'linear-gradient(135deg, #10b981 0%, #047857 100%)',
delegation: 'linear-gradient(135deg, #f59e0b 0%, #b45309 100%)',
}
const typeIcon: Record<string, string> = {
process: '⚙️',
order: '📦',
incident: '🚨',
idea: '💡',
delegation: '🤝',
}
export default function InboxPage({ onNavigate, onRefresh }: InboxPageProps) {
const [items, setItems] = useState<InboxItem[]>([])
const [loading, setLoading] = useState(true)
const [busyName, setBusyName] = useState('')
const [errorName, setErrorName] = useState('')
const [search, setSearch] = useState('')
const [filterType, setFilterType] = useState<string>('all')
const [page, setPage] = useState(1)
const load = () => {
setLoading(true)
getInboxItems().then(data => {
setItems(data || [])
setLoading(false)
}).catch(() => setLoading(false))
}
useEffect(() => { load() }, [])
const counts = useMemo(() => {
const res: Record<string, number> = { all: items.length, action: 0, info: 0 }
for (const item of items) {
if (item.requires_action) res.action = (res.action || 0) + 1
else res.info = (res.info || 0) + 1
res[item.type] = (res[item.type] || 0) + 1
}
return res
}, [items])
const filteredItems = useMemo(() => {
return items.filter(item => {
if (filterType === 'action' && !item.requires_action) return false
if (filterType === 'info' && item.requires_action) return false
if (filterType !== 'all' && filterType !== 'action' && filterType !== 'info' && item.type !== filterType) return false
if (search.trim()) {
const q = search.toLowerCase()
const match = [item.number, item.title, item.action, item.organization, item.type].filter(Boolean).join(' ').toLowerCase()
if (!match.includes(q)) return false
}
return true
})
}, [items, filterType, search])
useEffect(() => { setPage(1) }, [filterType, search])
const pageItems = filteredItems.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)
const respond = async (name: string, response: 'Accepted' | 'Rejected') => {
setBusyName(name)
setErrorName('')
try {
await respondToDelegation(name, response)
await load()
onRefresh?.()
} catch {
setErrorName(name)
} finally {
setBusyName('')
}
}
return (
<div style={{ maxWidth: 1100, margin: '0 auto', display: 'flex', flexDirection: 'column', gap: 20 }}>
{/* Top Banner & Stats */}
<div style={{
background: 'linear-gradient(135deg, #1e1b4b 0%, #312e81 50%, #1e293b 100%)',
borderRadius: 16,
padding: '24px 28px',
color: '#ffffff',
boxShadow: '0 10px 25px -5px rgba(30, 27, 75, 0.25), 0 8px 10px -6px rgba(30, 27, 75, 0.2)',
display: 'flex',
flexDirection: 'column',
gap: 18,
position: 'relative',
overflow: 'hidden'
}}>
{/* Glow decoration */}
<div style={{
position: 'absolute',
top: -40,
right: -40,
width: 180,
height: 180,
borderRadius: '50%',
background: 'radial-gradient(circle, rgba(99, 102, 241, 0.35) 0%, transparent 70%)',
pointerEvents: 'none'
}} />
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 14 }}>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ fontSize: 24 }}>📬</span>
<h2 style={{ fontSize: 20, fontWeight: 800, color: '#ffffff', margin: 0, letterSpacing: '-0.3px' }}>
Unified Activity Inbox
</h2>
</div>
<p style={{ margin: '4px 0 0', fontSize: 13, color: '#c7d2fe', opacity: 0.9 }}>
Real-time tasks, approvals, delegations, and workflow actions requiring your attention.
</p>
</div>
<button
type="button"
onClick={load}
disabled={loading}
style={{
background: 'rgba(255, 255, 255, 0.15)',
backdropFilter: 'blur(8px)',
border: '1px solid rgba(255, 255, 255, 0.25)',
color: 'white',
borderRadius: 10,
padding: '8px 16px',
fontSize: 13,
fontWeight: 600,
cursor: loading ? 'wait' : 'pointer',
display: 'flex',
alignItems: 'center',
gap: 8,
transition: 'all 0.15s ease'
}}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = 'rgba(255, 255, 255, 0.25)' }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = 'rgba(255, 255, 255, 0.15)' }}
>
<span style={{ transform: loading ? 'rotate(180deg)' : 'none', transition: 'transform 0.5s', display: 'inline-block' }}>🔄</span>
{loading ? 'Refreshing…' : 'Refresh'}
</button>
</div>
{/* Quick stat counters */}
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
<div style={{
background: 'rgba(255, 255, 255, 0.1)',
backdropFilter: 'blur(6px)',
borderRadius: 10,
padding: '8px 14px',
display: 'flex',
alignItems: 'center',
gap: 10,
border: '1px solid rgba(255, 255, 255, 0.15)'
}}>
<span style={{ fontSize: 12, color: '#e0e7ff', fontWeight: 500 }}>Total Items</span>
<span style={{ background: 'white', color: '#1e1b4b', fontWeight: 800, fontSize: 12, padding: '2px 8px', borderRadius: 999 }}>
{counts.all || 0}
</span>
</div>
<div style={{
background: 'rgba(239, 68, 68, 0.2)',
backdropFilter: 'blur(6px)',
borderRadius: 10,
padding: '8px 14px',
display: 'flex',
alignItems: 'center',
gap: 10,
border: '1px solid rgba(239, 68, 68, 0.35)'
}}>
<span style={{ fontSize: 12, color: '#fecaca', fontWeight: 600 }}>Action Required</span>
<span style={{ background: '#ef4444', color: 'white', fontWeight: 800, fontSize: 12, padding: '2px 8px', borderRadius: 999 }}>
{counts.action || 0}
</span>
</div>
<div style={{
background: 'rgba(59, 130, 246, 0.2)',
backdropFilter: 'blur(6px)',
borderRadius: 10,
padding: '8px 14px',
display: 'flex',
alignItems: 'center',
gap: 10,
border: '1px solid rgba(59, 130, 246, 0.35)'
}}>
<span style={{ fontSize: 12, color: '#bfdbfe', fontWeight: 500 }}>Informational</span>
<span style={{ background: '#3b82f6', color: 'white', fontWeight: 800, fontSize: 12, padding: '2px 8px', borderRadius: 999 }}>
{counts.info || 0}
</span>
</div>
</div>
</div>
{/* Filter Tabs and Search Bar */}
<div style={{
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
justifyContent: 'space-between',
gap: 14,
background: 'white',
padding: '12px 16px',
borderRadius: 12,
border: '1px solid #e2e8f0',
boxShadow: '0 1px 3px rgba(0,0,0,0.04)'
}} className="form-card">
{/* Pills */}
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' }}>
{[
{ id: 'all', label: 'All', icon: '📋', count: counts.all },
{ id: 'action', label: 'Needs Action', icon: '🚨', count: counts.action, badgeColor: '#ef4444' },
{ id: 'info', label: 'Info', icon: 'ℹ️', count: counts.info },
{ id: 'incident', label: 'Incidents', icon: '⚠️', count: counts.incident },
{ id: 'idea', label: 'Ideas', icon: '💡', count: counts.idea },
{ id: 'order', label: 'Orders', icon: '📦', count: counts.order },
{ id: 'process', label: 'Processes', icon: '⚙️', count: counts.process },
{ id: 'delegation', label: 'Delegations', icon: '🤝', count: counts.delegation },
].map(tab => {
const active = filterType === tab.id
if (tab.id !== 'all' && tab.id !== 'action' && tab.id !== 'info' && (!tab.count || tab.count === 0)) return null
return (
<button
key={tab.id}
type="button"
onClick={() => setFilterType(tab.id)}
style={{
display: 'flex',
alignItems: 'center',
gap: 6,
padding: '6px 12px',
borderRadius: 999,
border: active ? '1px solid #4f46e5' : '1px solid #e2e8f0',
background: active ? '#4f46e5' : '#f8fafc',
color: active ? '#ffffff' : '#475569',
fontSize: 12,
fontWeight: 600,
cursor: 'pointer',
transition: 'all 0.15s ease'
}}
>
<span>{tab.icon}</span>
<span>{tab.label}</span>
{tab.count != null && tab.count > 0 && (
<span style={{
fontSize: 10,
fontWeight: 700,
padding: '1px 6px',
borderRadius: 999,
background: active ? 'rgba(255,255,255,0.25)' : (tab.badgeColor || '#e2e8f0'),
color: active ? '#ffffff' : (tab.badgeColor ? 'white' : '#475569')
}}>
{tab.count}
</span>
)}
</button>
)
})}
</div>
{/* Search */}
<div style={{ minWidth: 200, flex: '1 1 200px', maxWidth: 300 }}>
<input
type="text"
placeholder="Search items, titles, IDs…"
value={search}
onChange={e => setSearch(e.target.value)}
style={{
width: '100%',
padding: '7px 12px',
borderRadius: 8,
border: '1px solid #cbd5e1',
fontSize: 12,
boxSizing: 'border-box',
background: '#f8fafc',
color: '#334155'
}}
/>
</div>
</div>
{/* Cards List */}
{loading ? (
<div style={{ textAlign: 'center', padding: '60px 20px', color: '#94a3b8', fontSize: 14 }}>
<div style={{ fontSize: 28, marginBottom: 8, animation: 'spin 1s linear infinite' }}>⏳</div>
Loading inbox updates…
</div>
) : filteredItems.length === 0 ? (
<div style={{
background: 'white',
borderRadius: 16,
padding: '60px 24px',
textAlign: 'center',
border: '1px solid #e2e8f0',
boxShadow: '0 1px 3px rgba(0,0,0,0.03)'
}} className="form-card">
<div style={{ fontSize: 44, marginBottom: 12 }}>✨</div>
<h3 style={{ fontSize: 16, fontWeight: 700, color: '#1e293b', margin: 0 }}>
{search || filterType !== 'all' ? 'No matching inbox items' : 'You are all caught up!'}
</h3>
<p style={{ fontSize: 13, color: '#64748b', marginTop: 6, maxWidth: 420, margin: '6px auto 0' }}>
{search || filterType !== 'all'
? 'Try changing your filter pills or searching for a different keyword.'
: 'There are no active pending approvals or tasks requiring your action right now.'}
</p>
{(search || filterType !== 'all') && (
<button
type="button"
onClick={() => { setSearch(''); setFilterType('all') }}
style={{
marginTop: 16,
padding: '7px 16px',
borderRadius: 8,
background: '#4f46e5',
color: 'white',
border: 'none',
fontWeight: 600,
fontSize: 12,
cursor: 'pointer'
}}
>
Reset Filters
</button>
)}
</div>
) : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(330px, 1fr))', gap: 14 }}>
{pageItems.map((item, idx) => {
const tint = typeColor[item.type] || colors.gray
const gradient = typeGradient[item.type] || 'linear-gradient(135deg, #64748b 0%, #475569 100%)'
const clickable = item.type !== 'delegation'
return (
<div
key={item.name || idx}
className="inbox-card form-card"
onClick={clickable ? () => onNavigate(typeToPage[item.type] as Page, item.name) : undefined}
style={{
background: 'white',
borderRadius: 14,
padding: '16px 18px',
border: '1px solid #e2e8f0',
boxShadow: '0 1px 3px rgba(0,0,0,0.04)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
gap: 14,
cursor: clickable ? 'pointer' : 'default',
position: 'relative',
overflow: 'hidden',
transition: 'transform 0.15s ease, box-shadow 0.15s ease'
}}
onMouseEnter={e => {
if (clickable) {
(e.currentTarget as HTMLElement).style.transform = 'translateY(-2px)'
;(e.currentTarget as HTMLElement).style.boxShadow = '0 10px 20px -5px rgba(0,0,0,0.08)'
}
}}
onMouseLeave={e => {
if (clickable) {
(e.currentTarget as HTMLElement).style.transform = 'none'
;(e.currentTarget as HTMLElement).style.boxShadow = '0 1px 3px rgba(0,0,0,0.04)'
}
}}
>
{/* Accent line */}
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 4, background: gradient }} />
<div>
{/* Card Header row */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginBottom: 10 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{
width: 32,
height: 32,
borderRadius: 8,
background: gradient,
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 15,
flexShrink: 0,
boxShadow: `0 3px 8px -1px ${tint}66`
}}>
{typeIcon[item.type] || '📌'}
</div>
<span style={{ fontSize: 12, fontWeight: 700, color: tint, letterSpacing: '0.3px' }}>
{item.number || item.name}
</span>
</div>
{item.requires_action ? (
<span style={{
fontSize: 10,
fontWeight: 800,
color: '#ef4444',
background: '#fef2f2',
border: '1px solid #fecaca',
padding: '2px 8px',
borderRadius: 999,
textTransform: 'uppercase',
letterSpacing: '0.4px',
display: 'flex',
alignItems: 'center',
gap: 4
}}>
<span style={{ width: 6, height: 6, borderRadius: '50%', background: '#ef4444' }} />
Action
</span>
) : (
<span style={{
fontSize: 10,
fontWeight: 700,
color: '#64748b',
background: '#f1f5f9',
padding: '2px 8px',
borderRadius: 999,
textTransform: 'uppercase',
letterSpacing: '0.3px'
}}>
{item.type}
</span>
)}
</div>
{/* Card Title */}
<div className="inbox-card-title" style={{
fontSize: 14,
fontWeight: 700,
color: '#1e293b',
lineHeight: 1.35,
marginBottom: 6,
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical'
}}>
{item.title || item.action || 'Untitled Item'}
</div>
{/* Action description / remarks */}
<div style={{ fontSize: 12, color: '#475569', lineHeight: 1.4, marginBottom: 8 }}>
{item.action}
</div>
{/* Meta tag chips */}
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' }}>
{item.organization && (
<span style={{
fontSize: 11,
color: '#64748b',
background: '#f8fafc',
border: '1px solid #e2e8f0',
padding: '2px 7px',
borderRadius: 6,
display: 'flex',
alignItems: 'center',
gap: 4
}}>
🏢 {item.organization}
</span>
)}
{item.date && (
<span style={{
fontSize: 11,
color: '#64748b',
background: '#f8fafc',
border: '1px solid #e2e8f0',
padding: '2px 7px',
borderRadius: 6
}}>
📅 {item.date}
</span>
)}
</div>
{errorName === item.name && (
<div style={{ marginTop: 8, fontSize: 11, color: '#ef4444', fontWeight: 600 }}>
⚠️ Failed to process response. Please try again.
</div>
)}
</div>
{/* Footer Action */}
<div>
{item.type === 'delegation' ? (
<div style={{ display: 'flex', gap: 8, marginTop: 4 }}>
<button
type="button"
onClick={() => respond(item.name, 'Accepted')}
disabled={busyName === item.name}
style={{
flex: 1,
padding: '7px 0',
borderRadius: 8,
border: 'none',
background: '#16a34a',
color: 'white',
fontWeight: 700,
fontSize: 12,
cursor: busyName === item.name ? 'wait' : 'pointer',
opacity: busyName === item.name ? 0.6 : 1,
transition: 'background 0.15s ease'
}}
>
✓ Accept
</button>
<button
type="button"
onClick={() => respond(item.name, 'Rejected')}
disabled={busyName === item.name}
style={{
flex: 1,
padding: '7px 0',
borderRadius: 8,
border: 'none',
background: '#ef4444',
color: 'white',
fontWeight: 700,
fontSize: 12,
cursor: busyName === item.name ? 'wait' : 'pointer',
opacity: busyName === item.name ? 0.6 : 1,
transition: 'background 0.15s ease'
}}
>
✕ Reject
</button>
</div>
) : (
<button
type="button"
onClick={e => {
e.stopPropagation()
onNavigate(typeToPage[item.type] as Page, item.name)
}}
style={{
width: '100%',
padding: '7px 12px',
borderRadius: 8,
border: item.requires_action ? 'none' : '1px solid #cbd5e1',
background: item.requires_action ? gradient : '#f8fafc',
color: item.requires_action ? '#ffffff' : '#334155',
fontWeight: 700,
fontSize: 12,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 6,
transition: 'all 0.15s ease'
}}
>
<span>{item.requires_action ? 'Review & Act Now' : 'View Details'}</span>
<span style={{ fontSize: 13 }}>→</span>
</button>
)}
</div>
</div>
)
})}
</div>
)}
<Pagination page={page} total={filteredItems.length} onPageChange={setPage} />
</div>
)
}