Design principles: Claims First (collapsed others) + Renewal Date (+1 day) everywhere

This commit is contained in:
lorentz 2026-04-10 13:56:46 +00:00
parent 4315f704c7
commit 02d1fcb73d
5 changed files with 65 additions and 38 deletions

View file

@ -5,7 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Building2, Calendar, FileText, CheckSquare, GitBranch, CornerLeftUp } from 'lucide-react'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { formatDate, daysUntil } from '@/lib/utils'
import { formatRenewalDate, daysUntil } from '@/lib/utils'
interface ClientCardProps {
client: {
@ -117,7 +117,7 @@ export function ClientCard({ client }: ClientCardProps) {
<Calendar className="h-4 w-4 text-muted-foreground" />
<span className="text-muted-foreground">Renewal date:</span>
<span className="font-medium" suppressHydrationWarning>
{formatDate(nextPolicy.expirationDate)}
{formatRenewalDate(nextPolicy.expirationDate)}
</span>
{daysToExpiration !== null && daysToExpiration <= 90 && (
<Badge

View file

@ -12,7 +12,7 @@ import {
import { Badge } from '@/components/ui/badge'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { GitBranch, CornerLeftUp } from 'lucide-react'
import { formatDate, daysUntil } from '@/lib/utils'
import { formatRenewalDate, daysUntil } from '@/lib/utils'
interface ClientTableProps {
clients: Array<{
@ -134,7 +134,7 @@ export function ClientTable({ clients }: ClientTableProps) {
<TableCell>
{nextPolicy ? (
<div className="flex items-center gap-2">
<span suppressHydrationWarning>{formatDate(nextPolicy.expirationDate)}</span>
<span suppressHydrationWarning>{formatRenewalDate(nextPolicy.expirationDate)}</span>
{daysToExpiration !== null && daysToExpiration <= 90 && (
<Badge
variant={daysToExpiration <= 30 ? 'destructive' : 'secondary'}

View file

@ -17,7 +17,7 @@ import {
CreditCard,
Shield
} from 'lucide-react'
import { formatDate } from '@/lib/utils'
import { formatDate, formatRenewalDate } from '@/lib/utils'
interface PolicyDetailProps {
policy: any
@ -52,10 +52,10 @@ export function PolicyDetail({ policy }: PolicyDetailProps) {
variant={isExpired ? 'destructive' : daysUntilExpiration <= 30 ? 'destructive' : daysUntilExpiration <= 90 ? 'secondary' : 'default'}
className="text-sm"
>
{isExpired ? 'Expired' : `Expires in ${daysUntilExpiration} days`}
{isExpired ? 'Expired' : `Renews in ${daysUntilExpiration} days`}
</Badge>
<p className="text-sm text-muted-foreground mt-2">
<span suppressHydrationWarning>{formatDate(policy.expirationDate)}</span>
<span suppressHydrationWarning>{formatRenewalDate(policy.expirationDate)}</span>
</p>
</div>
</div>
@ -117,10 +117,10 @@ export function PolicyDetail({ policy }: PolicyDetailProps) {
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Expiration Date</p>
<p className="text-sm text-muted-foreground">Renewal Date</p>
<p className="font-medium">
<span suppressHydrationWarning>
{formatDate(policy.expirationDate)}
{formatRenewalDate(policy.expirationDate)}
</span>
</p>
</div>

View file

@ -61,7 +61,7 @@ export function PolicyChip({ policy, disabled }: PolicyChipProps) {
</div>
<div className="text-xs text-muted-foreground truncate">
{policy.carrierName || 'Unknown carrier'}
{expDisplay && <span suppressHydrationWarning> · Exp {expDisplay}</span>}
{expDisplay && <span suppressHydrationWarning> · Renews {expDisplay}</span>}
</div>
</div>
{!policy.expirationDate && (

View file

@ -1,7 +1,8 @@
'use client'
import { useMemo } from 'react'
import { useState, useMemo } from 'react'
import { SelectContent, SelectGroup, SelectItem, SelectLabel } from '@/components/ui/select'
import { ChevronDown } from 'lucide-react'
export interface SelectableUser {
id: string
@ -24,53 +25,79 @@ function formatDepartment(dept: string): string {
.join(' ')
}
/** Departments shown first in this order; everything else follows alphabetically */
const PRIORITY_DEPARTMENTS = ['claims']
export function UserSelectContent({ users, excludeIds = [], prefixItem }: UserSelectContentProps) {
const grouped = useMemo(() => {
const eligible = users.filter((u) => !excludeIds.includes(u.id))
const [showAll, setShowAll] = useState(false)
const { claimsUsers, otherGroups } = useMemo(() => {
const eligible = users.filter((u) => !excludeIds.includes(u.id))
const sorter = (a: SelectableUser, b: SelectableUser) =>
(a.displayName || a.email).localeCompare(b.displayName || b.email)
const claims = eligible
.filter((u) => u.department?.toLowerCase() === 'claims')
.sort(sorter)
// Group remaining users by department, sorted alphabetically
const map = new Map<string, SelectableUser[]>()
for (const u of eligible) {
if (u.department?.toLowerCase() === 'claims') continue
const key = (u.department || 'other').toLowerCase()
if (!map.has(key)) map.set(key, [])
map.get(key)!.push(u)
}
for (const list of map.values()) list.sort(sorter)
// Sort users within each group
for (const list of map.values()) {
list.sort((a, b) =>
(a.displayName || a.email).localeCompare(b.displayName || b.email)
)
}
const sortedKeys = [...map.keys()].sort((a, b) => a.localeCompare(b))
const groups = sortedKeys.map((key) => ({
key,
label: formatDepartment(key),
users: map.get(key)!,
}))
// Sort department keys: priority first, then alphabetical
const keys = [...map.keys()].sort((a, b) => {
const ai = PRIORITY_DEPARTMENTS.indexOf(a)
const bi = PRIORITY_DEPARTMENTS.indexOf(b)
if (ai !== -1 && bi !== -1) return ai - bi
if (ai !== -1) return -1
if (bi !== -1) return 1
return a.localeCompare(b)
})
return keys.map((key) => ({ key, label: formatDepartment(key), users: map.get(key)! }))
return { claimsUsers: claims, otherGroups: groups }
}, [users, excludeIds])
return (
<SelectContent>
{prefixItem}
{grouped.map((group) => (
<SelectGroup key={group.key}>
<SelectLabel>{group.label}</SelectLabel>
{group.users.map((u) => (
{claimsUsers.length > 0 && (
<SelectGroup>
<SelectLabel>Claims</SelectLabel>
{claimsUsers.map((u) => (
<SelectItem key={u.id} value={u.id}>
{u.displayName || u.email}
</SelectItem>
))}
</SelectGroup>
))}
)}
{otherGroups.length > 0 && (
<SelectGroup>
<SelectLabel
className="flex items-center justify-between cursor-pointer select-none hover:text-foreground"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
setShowAll((v) => !v)
}}
>
<span>All Staff</span>
<ChevronDown
className={`h-3.5 w-3.5 transition-transform ${showAll ? 'rotate-180' : ''}`}
/>
</SelectLabel>
{showAll &&
otherGroups.map((group) => (
<SelectGroup key={group.key} className="pl-2">
<SelectLabel className="text-xs text-muted-foreground">{group.label}</SelectLabel>
{group.users.map((u) => (
<SelectItem key={u.id} value={u.id}>
{u.displayName || u.email}
</SelectItem>
))}
</SelectGroup>
))}
</SelectGroup>
)}
</SelectContent>
)
}