471 lines
13 KiB
TypeScript
471 lines
13 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { useHistory } from 'react-router-dom';
|
|
import {
|
|
IonPage,
|
|
IonContent,
|
|
IonSpinner,
|
|
useIonViewWillEnter,
|
|
IonIcon,
|
|
} from '@ionic/react';
|
|
import { documentTextOutline, addOutline, searchOutline } from 'ionicons/icons';
|
|
import { supabase } from '../supabase';
|
|
import { useProfile } from '../contexts/ProfileContext';
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
import DashboardMetricCard from '../components/DashboardMetricCard';
|
|
import RecentActivityCard from '../components/RecentActivityCard';
|
|
import { Database } from '../database.types';
|
|
|
|
type RfqRow = Database['public']['Tables']['rfqs']['Row'];
|
|
|
|
const Home: React.FC = () => {
|
|
const history = useHistory();
|
|
const { user } = useAuth();
|
|
const { profile } = useProfile();
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState('');
|
|
|
|
const [rfqSummary, setRfqSummary] = useState({
|
|
open: 0,
|
|
quoted: 0,
|
|
closed: 0,
|
|
});
|
|
const [supplierSummary, setSupplierSummary] = useState({
|
|
assignedOpen: 0,
|
|
myQuotes: 0,
|
|
accepted: 0,
|
|
});
|
|
const [recentItems, setRecentItems] = useState<any[]>([]);
|
|
|
|
useIonViewWillEnter(() => {
|
|
loadDashboardData();
|
|
});
|
|
|
|
const loadDashboardData = async () => {
|
|
if (!user || !profile) return;
|
|
setLoading(true);
|
|
setError('');
|
|
|
|
try {
|
|
if (profile.role === 'buyer') {
|
|
// Aggregate counts
|
|
const { data: allRfqs, error: countErr } = await supabase
|
|
.from('rfqs')
|
|
.select('id, status')
|
|
.eq('buyer_id', user.id);
|
|
|
|
if (countErr) throw countErr;
|
|
|
|
let open = 0,
|
|
quoted = 0,
|
|
closed = 0;
|
|
allRfqs?.forEach((r) => {
|
|
if (r.status === 'open') open++;
|
|
else if (r.status === 'quoted') quoted++;
|
|
else if (r.status === 'closed') closed++;
|
|
});
|
|
setRfqSummary({ open, quoted, closed });
|
|
|
|
// Recent items
|
|
const { data: recent, error: recentErr } = await supabase
|
|
.from('rfqs')
|
|
.select('*')
|
|
.eq('buyer_id', user.id)
|
|
.order('created_at', { ascending: false })
|
|
.limit(5);
|
|
if (recentErr) throw recentErr;
|
|
setRecentItems(recent || []);
|
|
} else {
|
|
// Supplier
|
|
const { data: allRecipients, error: recErr } = await supabase
|
|
.from('rfq_recipients')
|
|
.select('id, rfqs!inner(id, status)')
|
|
.eq('supplier_id', user.id);
|
|
if (recErr) throw recErr;
|
|
|
|
let assignedOpen = 0;
|
|
allRecipients?.forEach((r: any) => {
|
|
if (r.rfqs && r.rfqs.status === 'open') assignedOpen++;
|
|
});
|
|
|
|
const { data: myQuotes, error: qErr } = await supabase
|
|
.from('quotations')
|
|
.select('id, status')
|
|
.eq('supplier_id', user.id);
|
|
if (qErr) throw qErr;
|
|
|
|
let accepted = 0;
|
|
myQuotes?.forEach((q) => {
|
|
if (q.status === 'accepted') accepted++;
|
|
});
|
|
setSupplierSummary({
|
|
assignedOpen,
|
|
myQuotes: myQuotes?.length || 0,
|
|
accepted,
|
|
});
|
|
|
|
const { data: recent, error: recentErr } = await supabase
|
|
.from('rfq_recipients')
|
|
.select('id, rfqs!inner(*)')
|
|
.eq('supplier_id', user.id)
|
|
.order('created_at', { ascending: false })
|
|
.limit(5);
|
|
if (recentErr) throw recentErr;
|
|
|
|
// Map joined data
|
|
const mapped = (recent || []).map((r: any) => r.rfqs);
|
|
setRecentItems(mapped);
|
|
}
|
|
} catch (err: any) {
|
|
console.error(err);
|
|
setError('Failed to load dashboard data.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handlePrimaryAction = () => {
|
|
if (profile?.role === 'buyer') {
|
|
history.push('/rfq/new');
|
|
} else {
|
|
history.push('/requests');
|
|
}
|
|
};
|
|
|
|
const handleRecentItemTap = (item: RfqRow) => {
|
|
history.push(`/rfq/${item.id}`);
|
|
};
|
|
|
|
if (!profile) return null;
|
|
|
|
return (
|
|
<IonPage style={{ background: 'var(--color-bg)' }}>
|
|
<IonContent
|
|
style={{
|
|
'--background': 'var(--color-bg-gradient)',
|
|
'--padding-start': '0px',
|
|
'--padding-end': '0px',
|
|
'--padding-top': '0px',
|
|
'--padding-bottom': 'calc(96px + var(--ion-safe-area-bottom))',
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
padding: 'calc(16px + var(--ion-safe-area-top, 0px)) 20px 16px',
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
}}
|
|
>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '3px' }}>
|
|
<div
|
|
style={{
|
|
fontSize: '13px',
|
|
fontWeight: '700',
|
|
letterSpacing: '0.08em',
|
|
textTransform: 'uppercase',
|
|
color: 'var(--color-brand)',
|
|
}}
|
|
>
|
|
QuoteLink
|
|
</div>
|
|
<div
|
|
style={{
|
|
fontSize: '13px',
|
|
fontWeight: '400',
|
|
color: 'var(--color-text-tertiary)',
|
|
}}
|
|
>
|
|
Procurement made simpler
|
|
</div>
|
|
</div>
|
|
<div
|
|
onClick={() => history.push('/profile')}
|
|
style={{
|
|
width: '40px',
|
|
height: '40px',
|
|
borderRadius: '999px',
|
|
background: 'var(--color-brand-medium)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
cursor: 'pointer',
|
|
color: 'var(--color-brand)',
|
|
fontSize: '16px',
|
|
fontWeight: '700',
|
|
textTransform: 'uppercase',
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
{profile.full_name.charAt(0)}
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
style={{
|
|
margin: '0 20px 18px',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '5px',
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
fontSize: '26px',
|
|
fontWeight: '400',
|
|
color: 'var(--color-text-primary)',
|
|
lineHeight: '1.08',
|
|
}}
|
|
>
|
|
Good to see you,{' '}
|
|
<span style={{ fontWeight: '700' }}>
|
|
{profile.full_name.split(' ')[0]}
|
|
</span>
|
|
</div>
|
|
<div
|
|
style={{
|
|
fontSize: '14px',
|
|
fontWeight: '400',
|
|
color: 'var(--color-text-secondary)',
|
|
}}
|
|
>
|
|
{profile.role === 'buyer'
|
|
? 'Track requests, review supplier pricing, and keep decisions moving.'
|
|
: 'Review assigned RFQs, send pricing fast, and stay on top of wins.'}
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<div
|
|
style={{
|
|
margin: '0 20px 12px',
|
|
background: 'var(--color-danger-soft)',
|
|
borderRadius: '12px',
|
|
padding: '12px 14px',
|
|
fontSize: '13px',
|
|
fontWeight: '500',
|
|
color: 'var(--color-danger-text)',
|
|
}}
|
|
>
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div
|
|
style={{
|
|
background: 'var(--color-brand-strong)',
|
|
borderRadius: '24px',
|
|
padding: '22px',
|
|
margin: '0 20px 12px',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '14px',
|
|
cursor: 'pointer',
|
|
}}
|
|
onClick={handlePrimaryAction}
|
|
>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'flex-start',
|
|
}}
|
|
>
|
|
<div
|
|
style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}
|
|
>
|
|
<span
|
|
style={{
|
|
fontSize: '12px',
|
|
fontWeight: '600',
|
|
color: 'rgba(255,255,255,0.72)',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: '0.05em',
|
|
}}
|
|
>
|
|
{profile.role === 'buyer' ? 'Active Requests' : 'Assigned RFQs'}
|
|
</span>
|
|
<span
|
|
style={{
|
|
fontSize: '34px',
|
|
fontWeight: '400',
|
|
color: '#ffffff',
|
|
}}
|
|
>
|
|
{profile.role === 'buyer'
|
|
? rfqSummary.open + rfqSummary.quoted
|
|
: supplierSummary.assignedOpen}
|
|
</span>
|
|
</div>
|
|
<div
|
|
style={{
|
|
width: '40px',
|
|
height: '40px',
|
|
borderRadius: '14px',
|
|
background: 'rgba(255,255,255,0.15)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
}}
|
|
>
|
|
<IonIcon
|
|
icon={profile.role === 'buyer' ? addOutline : searchOutline}
|
|
style={{ color: '#ffffff', fontSize: '24px' }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div
|
|
style={{
|
|
fontSize: '14px',
|
|
fontWeight: '600',
|
|
color: 'var(--color-accent)',
|
|
}}
|
|
>
|
|
{profile.role === 'buyer'
|
|
? 'Create new request →'
|
|
: 'Browse requests →'}
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: '12px', margin: '0 20px 24px' }}>
|
|
{profile.role === 'buyer' ? (
|
|
<>
|
|
<DashboardMetricCard
|
|
label="Received Quotes"
|
|
value={rfqSummary.quoted.toString()}
|
|
hint="Awaiting decision"
|
|
icon={documentTextOutline}
|
|
/>
|
|
<DashboardMetricCard
|
|
label="Completed"
|
|
value={rfqSummary.closed.toString()}
|
|
hint="Past requests"
|
|
icon={documentTextOutline}
|
|
/>
|
|
</>
|
|
) : (
|
|
<>
|
|
<DashboardMetricCard
|
|
label="My Quotes"
|
|
value={supplierSummary.myQuotes.toString()}
|
|
hint="Submitted"
|
|
icon={documentTextOutline}
|
|
/>
|
|
<DashboardMetricCard
|
|
label="Accepted"
|
|
value={supplierSummary.accepted.toString()}
|
|
hint="Won bids"
|
|
icon={documentTextOutline}
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<div
|
|
style={{
|
|
margin: '16px 20px 8px',
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'flex-end',
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
fontSize: '15px',
|
|
fontWeight: '700',
|
|
color: 'var(--color-text-primary)',
|
|
}}
|
|
>
|
|
Recent Activity
|
|
</div>
|
|
<div
|
|
onClick={() => history.push('/requests')}
|
|
style={{
|
|
fontSize: '13px',
|
|
fontWeight: '600',
|
|
color: 'var(--color-brand)',
|
|
cursor: 'pointer',
|
|
}}
|
|
>
|
|
See all
|
|
</div>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div
|
|
style={{
|
|
margin: '0 20px',
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
padding: '20px',
|
|
}}
|
|
>
|
|
<IonSpinner
|
|
name="crescent"
|
|
style={{ color: 'var(--color-brand)' }}
|
|
/>
|
|
</div>
|
|
) : recentItems.length > 0 ? (
|
|
recentItems.map((item) => (
|
|
<RecentActivityCard
|
|
key={item.id}
|
|
title={item.title}
|
|
subtitle={new Date(item.created_at).toLocaleDateString()}
|
|
meta={item.status}
|
|
onClick={() => handleRecentItemTap(item)}
|
|
/>
|
|
))
|
|
) : (
|
|
<div
|
|
style={{
|
|
margin: '0 20px',
|
|
background: 'var(--color-surface)',
|
|
borderRadius: '24px',
|
|
padding: '28px',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
textAlign: 'center',
|
|
gap: '12px',
|
|
}}
|
|
>
|
|
<IonIcon
|
|
icon={documentTextOutline}
|
|
style={{ fontSize: '48px', color: 'var(--color-text-tertiary)' }}
|
|
/>
|
|
<div
|
|
style={{
|
|
fontSize: '14px',
|
|
fontWeight: '400',
|
|
color: 'var(--color-text-secondary)',
|
|
}}
|
|
>
|
|
No recent activity yet.
|
|
</div>
|
|
{profile.role === 'buyer' && (
|
|
<button
|
|
onClick={handlePrimaryAction}
|
|
style={{
|
|
background: 'var(--color-brand)',
|
|
color: '#ffffff',
|
|
borderRadius: '999px',
|
|
padding: '10px 20px',
|
|
fontSize: '14px',
|
|
fontWeight: '600',
|
|
border: 'none',
|
|
cursor: 'pointer',
|
|
marginTop: '8px',
|
|
}}
|
|
>
|
|
Create Request
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</IonContent>
|
|
</IonPage>
|
|
);
|
|
};
|
|
|
|
export default Home;
|