feat(ST-507): add code-helpers and CodeBadge component

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-04-26 20:36:33 +02:00
parent 33d66bc7c4
commit 8ffbc526d5
2 changed files with 74 additions and 0 deletions

View file

@ -0,0 +1,20 @@
import { cn } from '@/lib/utils'
interface CodeBadgeProps {
code: string | null | undefined
className?: string
}
export function CodeBadge({ code, className }: CodeBadgeProps) {
if (!code) return null
return (
<span
className={cn(
'inline-flex items-center rounded-md border border-border bg-surface-container px-1.5 py-0.5 font-mono text-[11px] leading-none text-muted-foreground',
className,
)}
>
{code}
</span>
)
}

54
lib/code.ts Normal file
View file

@ -0,0 +1,54 @@
import { prisma } from '@/lib/prisma'
const STORY_AUTO_RE = /^ST-(\d+)$/
const PBI_AUTO_RE = /^PBI-(\d+)$/
const VALID_CODE_RE = /^[A-Za-z0-9._-]+$/
export const MAX_CODE_LENGTH = 30
export function isValidCode(code: string): boolean {
return code.length > 0 && code.length <= MAX_CODE_LENGTH && VALID_CODE_RE.test(code)
}
export function normalizeCode(input: string | null | undefined): string | null {
if (input == null) return null
const trimmed = input.trim()
return trimmed === '' ? null : trimmed
}
function nextSequential(existing: (string | null)[], pattern: RegExp): number {
let max = 0
for (const c of existing) {
if (!c) continue
const m = c.match(pattern)
if (m) {
const n = Number.parseInt(m[1], 10)
if (!Number.isNaN(n) && n > max) max = n
}
}
return max + 1
}
export async function generateNextStoryCode(productId: string): Promise<string> {
const stories = await prisma.story.findMany({
where: { product_id: productId },
select: { code: true },
})
const next = nextSequential(stories.map((s) => s.code), STORY_AUTO_RE)
return `ST-${String(next).padStart(3, '0')}`
}
export async function generateNextPbiCode(productId: string): Promise<string> {
const pbis = await prisma.pbi.findMany({
where: { product_id: productId },
select: { code: true },
})
const next = nextSequential(pbis.map((p) => p.code), PBI_AUTO_RE)
return `PBI-${next}`
}
export function deriveTaskCode(storyCode: string | null, indexOneBased: number): string | null {
if (!storyCode) return null
return `${storyCode}.${indexOneBased}`
}