mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-22 14:51:45 +00:00
47671d52e0
FE6: split the oversized page and panel components into thin layout shells plus colocated use<Component> hooks, .constants.ts, .helpers.ts (with tests) and presentational sub-components, following the established 'logic in a hook, render in slices' pattern. Behaviour, markup, classes and effect order are unchanged. Largest reductions: PackingListPanel 1598->42, FileManager 1055->36, AdminPage 1525->167, BudgetPanel 1266->146, JourneyDetailPage 2822->547, PlacesSidebar 945->66, CollabChat 861->106, CollabNotes 1417->532. DayPlanSidebar's drag-and-drop render body was left intact (ref-identity sensitive) and only its toolbar/modals/constants were extracted.
65 lines
2.5 KiB
TypeScript
65 lines
2.5 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { budgetApi } from '../../api/client'
|
|
import type { BudgetItem } from '../../types'
|
|
import { fmtNum, colorForUserId, widgetTheme } from './BudgetPanel.helpers'
|
|
import RingAvatar from './BudgetPanelRingAvatar'
|
|
|
|
interface PerPersonSummaryEntry {
|
|
user_id: number
|
|
username: string
|
|
avatar_url: string | null
|
|
total_assigned: number
|
|
}
|
|
|
|
interface PerPersonInlineProps {
|
|
tripId: number
|
|
budgetItems: BudgetItem[]
|
|
currency: string
|
|
locale: string
|
|
}
|
|
|
|
export default function PerPersonInline({ tripId, budgetItems, currency, locale, grandTotal, theme }: PerPersonInlineProps & { grandTotal: number; theme: ReturnType<typeof widgetTheme> }) {
|
|
const [data, setData] = useState<PerPersonSummaryEntry[] | null>(null)
|
|
const fmt = (v: number) => fmtNum(v, locale, currency)
|
|
|
|
useEffect(() => {
|
|
budgetApi.perPersonSummary(tripId).then(d => setData(d.summary)).catch(() => {})
|
|
}, [tripId, budgetItems])
|
|
|
|
if (!data || data.length === 0) return null
|
|
|
|
const people = data.map(p => ({ ...p, color: colorForUserId(p.user_id) }))
|
|
|
|
return (
|
|
<>
|
|
{grandTotal > 0 && (
|
|
<div style={{ display: 'flex', height: 6, borderRadius: 999, overflow: 'hidden', marginTop: 8, marginBottom: 4, gap: 3 }}>
|
|
{people.map(p => (
|
|
<div key={p.user_id} style={{
|
|
height: '100%', borderRadius: 999,
|
|
flex: Math.max(p.total_assigned || 0, 0.01),
|
|
background: p.color.gradient,
|
|
}} />
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<div style={{ marginTop: 14, paddingTop: 14, borderTop: `1px solid ${theme.divider}`, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
{people.map(p => {
|
|
const percent = grandTotal > 0 ? Math.round((p.total_assigned / grandTotal) * 100) : 0
|
|
return (
|
|
<div key={p.user_id} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '6px 0' }}>
|
|
<RingAvatar userId={p.user_id} username={p.username} avatarUrl={p.avatar_url} size={34} innerBg={theme.centerBg} textColor={theme.text} />
|
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
<div style={{ fontSize: 13.5, fontWeight: 500, letterSpacing: '-0.01em', color: theme.text }}>{p.username}</div>
|
|
<div style={{ fontSize: 11, color: theme.faint, marginTop: 1 }}>{percent}%</div>
|
|
</div>
|
|
<div style={{ fontSize: 13.5, fontWeight: 600, color: theme.text, letterSpacing: '-0.01em' }}>{fmt(p.total_assigned)}</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</>
|
|
)
|
|
}
|