54 lines
1.6 KiB
TypeScript
54 lines
1.6 KiB
TypeScript
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}`
|
|
}
|