From 8ffbc526d54e23022177503493374e90b0043880 Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Sun, 26 Apr 2026 20:36:33 +0200 Subject: [PATCH] feat(ST-507): add code-helpers and CodeBadge component Co-Authored-By: Claude Opus 4.7 (1M context) --- components/shared/code-badge.tsx | 20 ++++++++++++ lib/code.ts | 54 ++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 components/shared/code-badge.tsx create mode 100644 lib/code.ts diff --git a/components/shared/code-badge.tsx b/components/shared/code-badge.tsx new file mode 100644 index 0000000..126dbeb --- /dev/null +++ b/components/shared/code-badge.tsx @@ -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 ( + + {code} + + ) +} diff --git a/lib/code.ts b/lib/code.ts new file mode 100644 index 0000000..6f1fb8c --- /dev/null +++ b/lib/code.ts @@ -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 { + 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 { + 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}` +}