Skip to content
4 changes: 4 additions & 0 deletions app/components/AppDataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ interface AppDataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]
data: TData[]
optionsChildren?: React.ReactNode
/** Content rendered between the toolbar and the table. */
children?: React.ReactNode
/** Stable key for each row. When provided, row order is frozen until the user re-sorts via column header click. */
getRowId?: (row: TData) => string
}
Expand Down Expand Up @@ -57,6 +59,7 @@ export function AppDataTable<TData, TValue>({
columns,
data,
optionsChildren,
children,
getRowId,
}: AppDataTableProps<TData, TValue>) {
const [sorting, setSorting] = React.useState<SortingState>([])
Expand Down Expand Up @@ -118,6 +121,7 @@ export function AppDataTable<TData, TValue>({
{optionsChildren}
</AppDataTableViewOptions>
</HStack>
{children}
<div className="rounded-md border">
<Table>
<TableHeader>
Expand Down
2 changes: 1 addition & 1 deletion app/components/AppDataTableViewOption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function AppDataTableViewOptions<TData>({
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<Button variant="ghost" size="sm">
<Settings2Icon className="h-4 w-4" />
Options
</Button>
Expand Down
41 changes: 19 additions & 22 deletions app/components/week-calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,6 @@ const WeeklyCalendar = ({
navigateTo(dayjs(weekInterval.start).add(7, 'day').toDate())
}

const handleToday = () => {
navigateTo(new Date())
}

const handleDateSelect = (date: Date | undefined) => {
if (date) {
navigateTo(date)
Expand All @@ -156,9 +152,6 @@ const WeeklyCalendar = ({
const formatWeekLabel = () => {
const start = dayjs(weekInterval.start)
const end = dayjs(weekInterval.end)
if (start.year() !== end.year()) {
return `${start.format('YYYY/M/D')} – ${end.format('YYYY/M/D')}`
}
return `${start.format('M/D')} – ${end.format('M/D')}`
}

Expand All @@ -181,7 +174,7 @@ const WeeklyCalendar = ({
<div className="flex items-center gap-1">
<Button
type="button"
variant="outline"
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={handlePrevWeek}
Expand Down Expand Up @@ -211,33 +204,37 @@ const WeeklyCalendar = ({
weekStartsOn={startDay}
locale={ja}
components={calendarComponents}
className="rounded-lg border p-3"
className="p-1"
/>
{!isCurrentWeek && (
<div className="border-t p-1">
<Button
type="button"
variant="ghost"
size="sm"
className="w-full text-xs"
onClick={() => {
navigateTo(new Date())
setOpen(false)
}}
>
今週に戻る
</Button>
</div>
)}
</PopoverContent>
</Popover>

<Button
type="button"
variant="outline"
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={handleNextWeek}
aria-label="Next week"
>
<ChevronRightIcon className="h-4 w-4" />
</Button>

{!isCurrentWeek && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={handleToday}
className="text-muted-foreground text-xs"
>
Today
</Button>
)}
</div>
)
}
Expand Down
12 changes: 12 additions & 0 deletions app/libs/stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Calculate the median of a numeric array.
* Returns null for empty arrays.
*/
export function median(values: number[]): number | null {
if (values.length === 0) return null
const sorted = [...values].sort((a, b) => a - b)
const mid = Math.floor(sorted.length / 2)
return sorted.length % 2 === 1
? sorted[mid]
: (sorted[mid - 1] + sorted[mid]) / 2
}
12 changes: 1 addition & 11 deletions app/routes/$orgSlug/analysis/reviews/+functions/aggregate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,7 @@
* clientLoader で呼び出し、集計済みデータをチャートコンポーネントに渡す。
*/
import { getPRComplexity, type PRSizeLabel } from '~/app/libs/pr-classify'

// --- 共通ユーティリティ ---

function median(values: number[]): number | null {
if (values.length === 0) return null
const sorted = [...values].sort((a, b) => a - b)
const mid = Math.floor(sorted.length / 2)
return sorted.length % 2 !== 0
? sorted[mid]
: (sorted[mid - 1] + sorted[mid]) / 2
}
import { median } from '~/app/libs/stats'

function formatHours(h: number): string {
if (h < 1) return `${(h * 60).toFixed(0)}m`
Expand Down
28 changes: 28 additions & 0 deletions app/routes/$orgSlug/throughput/+components/diff-badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { cn } from '~/app/libs/utils'

export function DiffBadge({
value,
prevValue,
format = (v) => `${v >= 0 ? '+' : ''}${v.toFixed(1)}`,
invertColor = false,
}: {
value: number
prevValue: number | null
format?: (diff: number) => string
invertColor?: boolean
}) {
if (prevValue === null) return null
const diff = value - prevValue
if (diff === 0) return null
const isPositive = invertColor ? diff < 0 : diff > 0
return (
<span
className={cn(
'text-xs font-medium',
isPositive ? 'text-emerald-600' : 'text-red-500',
)}
>
{format(diff)}
</span>
)
}
19 changes: 19 additions & 0 deletions app/routes/$orgSlug/throughput/+components/stat-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type React from 'react'

export function StatCard({
value,
label,
children,
}: {
value: string | number
label: string
children?: React.ReactNode
}) {
return (
<div className="rounded-lg border p-4 text-center">
<div className="text-3xl font-bold">{value}</div>
<div className="text-muted-foreground text-sm">{label}</div>
{children}
</div>
)
}
16 changes: 16 additions & 0 deletions app/routes/$orgSlug/throughput/+functions/calc-stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { median } from '~/app/libs/stats'

export function calcStats<T extends { achievement: boolean }>(
prs: T[],
getTime: (pr: T) => number | null,
) {
const achievementCount = prs.filter((pr) => pr.achievement).length
const achievementRate =
prs.length > 0 ? (achievementCount / prs.length) * 100 : 0
const times = prs.map(getTime).filter((v): v is number => v !== null)
return {
count: prs.length,
achievementRate,
median: median(times),
}
}

This file was deleted.

Loading