Skip to content

Commit e4b4d08

Browse files
feat(ContractItemListItem, ContractProgressBar): add contract item display with progress bar and badge
1 parent b59ade5 commit e4b4d08

2 files changed

Lines changed: 113 additions & 0 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { formatCurrency, normalizeString } from '@/lib/utils';
2+
import { Contract } from '@/types/application/contract';
3+
import { ContractType } from '@/types/application/enums';
4+
import { format } from 'date-fns';
5+
import { Badge } from '../ui/badge';
6+
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
7+
import ContractProgressBar from './contract-progress-bar';
8+
9+
const getContractTypeBadgeVariant = (type: ContractType) => {
10+
switch (type) {
11+
case ContractType.FullTime:
12+
return 'default';
13+
case ContractType.PartTime:
14+
return 'secondary';
15+
case ContractType.Contractor:
16+
case ContractType.Intern:
17+
return 'outline';
18+
default:
19+
return 'secondary';
20+
}
21+
};
22+
23+
export default function ContractItemListItem({ contract }: Readonly<{ contract: Contract }>) {
24+
return (
25+
<Card role="listitem">
26+
<CardHeader>
27+
<div className="items-center justify-between gap-2">
28+
<div className="flex flex-wrap items-center gap-3">
29+
<CardTitle className="text-lg">Contract #{contract.id}</CardTitle>
30+
<Badge variant={getContractTypeBadgeVariant(contract.contract_type)} className="select-none">
31+
{normalizeString(contract.contract_type)}
32+
</Badge>
33+
</div>
34+
35+
<div className="text-right">
36+
<p className="text-sm font-medium text-muted-foreground">Rate</p>
37+
<p className="text-lg font-bold">
38+
{formatCurrency(contract.rate)}{' '}
39+
<span className="text-sm text-muted-foreground">/{contract.rate_type}</span>
40+
</p>
41+
</div>
42+
</div>
43+
</CardHeader>
44+
45+
<CardContent className="space-y-4">
46+
<div className="mb-2 flex justify-between text-sm text-muted-foreground">
47+
<span>{format(contract.start_date, 'PPP')}</span>
48+
<span>{format(contract.end_date, 'PPP')}</span>
49+
</div>
50+
<ContractProgressBar startDate={String(contract.start_date)} endDate={String(contract.end_date)} />
51+
</CardContent>
52+
</Card>
53+
);
54+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Progress } from '@/components/ui/progress';
2+
import { formatDistanceToNowStrict } from 'date-fns';
3+
4+
interface ContractProgressBarProps {
5+
startDate: string;
6+
endDate: string;
7+
}
8+
9+
export default function ContractProgressBar({ startDate, endDate }: Readonly<ContractProgressBarProps>) {
10+
const startMs = Date.parse(startDate);
11+
const endMs = Date.parse(endDate);
12+
const now = Date.now();
13+
14+
// Guard: if dates are invalid or duration is zero/negative, show empty progress
15+
if (Number.isNaN(startMs) || Number.isNaN(endMs) || endMs <= startMs) {
16+
return (
17+
<div className="space-y-2">
18+
<Progress value={0} className="transition-all" />
19+
<div className="flex justify-between text-xs text-muted-foreground">
20+
<span>Not Available</span>
21+
<span>Invalid contract dates</span>
22+
</div>
23+
</div>
24+
);
25+
}
26+
27+
const total = endMs - startMs;
28+
const elapsed = Math.max(0, Math.min(now - startMs, total));
29+
const percent = Math.round((elapsed / total) * 100);
30+
31+
const isCompleted = now > endMs;
32+
const isActive = now >= startMs && now <= endMs;
33+
34+
let displayValue: number;
35+
if (isCompleted) displayValue = 100;
36+
else if (isActive) displayValue = percent;
37+
else displayValue = 0;
38+
39+
let statusText: string;
40+
if (isCompleted) statusText = 'Completed';
41+
else if (isActive) statusText = `${percent}% Complete`;
42+
else statusText = 'Not Started';
43+
44+
let detailText: string;
45+
if (isCompleted) detailText = `Ended ${formatDistanceToNowStrict(endMs, { addSuffix: true })}`;
46+
else if (isActive)
47+
detailText = `${percent}% Complete - ${formatDistanceToNowStrict(endMs, { addSuffix: true })} left`;
48+
else detailText = `Not Started - Starts in ${formatDistanceToNowStrict(startMs, { addSuffix: true })}`;
49+
50+
return (
51+
<div className="space-y-2">
52+
<Progress value={displayValue} className="transition-all" />
53+
<div className="flex justify-between text-xs text-muted-foreground">
54+
<span>{statusText}</span>
55+
<span>{detailText}</span>
56+
</div>
57+
</div>
58+
);
59+
}

0 commit comments

Comments
 (0)