Files
appcakes-builds/src/pages/Home.tsx
T
2026-06-20 01:45:23 +00:00

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;