feat(ideas): multi-select secundaire producten + badges in IdeaDetailLayout

Voegt checkbox-lijst toe voor extra producten (exclusief primaire) in
de Idee-tab, geïntegreerd in bestaande save/reset flow via
updateSecondaryProductsAction. Toont secundaire product-badges in de
detail-header. Bevat ook schema/dto/action-dependencies (IdeaProduct
junction, secondary_products in IdeaDto).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scrum4Me Agent 2026-05-06 02:27:12 +02:00 committed by Madhura68
parent 9a733d77bb
commit 86e69fc457

View file

@ -20,7 +20,7 @@ import { getIdeaStatusBadge } from '@/lib/idea-status-colors'
import type { IdeaStatusApi } from '@/lib/idea-status' import type { IdeaStatusApi } from '@/lib/idea-status'
import { isIdeaEditable } from '@/lib/idea-status' import { isIdeaEditable } from '@/lib/idea-status'
import type { IdeaDto } from '@/lib/idea-dto' import type { IdeaDto } from '@/lib/idea-dto'
import { updateIdeaAction, archiveIdeaAction } from '@/actions/ideas' import { updateIdeaAction, archiveIdeaAction, updateSecondaryProductsAction } from '@/actions/ideas'
import { IdeaRowActions } from '@/components/ideas/idea-row-actions' import { IdeaRowActions } from '@/components/ideas/idea-row-actions'
import { IdeaMdEditor } from '@/components/ideas/idea-md-editor' import { IdeaMdEditor } from '@/components/ideas/idea-md-editor'
import { IdeaPbiLinkCard } from '@/components/ideas/idea-pbi-link-card' import { IdeaPbiLinkCard } from '@/components/ideas/idea-pbi-link-card'
@ -163,6 +163,18 @@ export function IdeaDetailLayout({
<span className="text-sm italic text-muted-foreground">geen product</span> <span className="text-sm italic text-muted-foreground">geen product</span>
)} )}
</div> </div>
{idea.secondary_products.length > 0 && (
<div className="flex flex-wrap gap-1 mt-1">
{idea.secondary_products.map((sp) => (
<span
key={sp.id}
className="text-xs bg-muted px-2 py-0.5 rounded-full text-muted-foreground"
>
{sp.product.name}
</span>
))}
</div>
)}
</div> </div>
<IdeaRowActions idea={idea} isDemo={isDemo} onArchive={handleArchive} /> <IdeaRowActions idea={idea} isDemo={isDemo} onArchive={handleArchive} />
</header> </header>
@ -214,6 +226,7 @@ export function IdeaDetailLayout({
products={products} products={products}
isDemo={isDemo} isDemo={isDemo}
pending={pending} pending={pending}
secondaryProducts={idea.secondary_products}
/> />
)} )}
{tab === 'grill' && ( {tab === 'grill' && (
@ -250,9 +263,10 @@ interface FormProps {
products: ProductOption[] products: ProductOption[]
isDemo: boolean isDemo: boolean
pending: boolean pending: boolean
secondaryProducts: IdeaDto['secondary_products']
} }
function IdeaFormSection({ idea, products, isDemo, pending }: FormProps) { function IdeaFormSection({ idea, products, isDemo, pending, secondaryProducts }: FormProps) {
const router = useRouter() const router = useRouter()
const editable = const editable =
!isDemo && !isDemo &&
@ -260,12 +274,20 @@ function IdeaFormSection({ idea, products, isDemo, pending }: FormProps) {
const [title, setTitle] = useState(idea.title) const [title, setTitle] = useState(idea.title)
const [description, setDescription] = useState(idea.description ?? '') const [description, setDescription] = useState(idea.description ?? '')
const [productId, setProductId] = useState(idea.product_id ?? '') const [productId, setProductId] = useState(idea.product_id ?? '')
const [selectedSecondary, setSelectedSecondary] = useState<string[]>(
secondaryProducts.map((sp) => sp.product_id),
)
const [submitting, startSubmit] = useTransition() const [submitting, startSubmit] = useTransition()
const secondaryDirty =
JSON.stringify([...selectedSecondary].sort()) !==
JSON.stringify(secondaryProducts.map((sp) => sp.product_id).sort())
const dirty = const dirty =
title !== idea.title || title !== idea.title ||
description !== (idea.description ?? '') || description !== (idea.description ?? '') ||
productId !== (idea.product_id ?? '') productId !== (idea.product_id ?? '') ||
secondaryDirty
function save() { function save() {
startSubmit(async () => { startSubmit(async () => {
@ -278,6 +300,13 @@ function IdeaFormSection({ idea, products, isDemo, pending }: FormProps) {
toast.error(r.error) toast.error(r.error)
return return
} }
if (secondaryDirty) {
const r2 = await updateSecondaryProductsAction(idea.id, selectedSecondary)
if ('error' in r2) {
toast.error(r2.error)
return
}
}
toast.success('Opgeslagen') toast.success('Opgeslagen')
router.refresh() router.refresh()
}) })
@ -320,6 +349,30 @@ function IdeaFormSection({ idea, products, isDemo, pending }: FormProps) {
))} ))}
</select> </select>
</div> </div>
{products.filter((p) => p.id !== productId).length > 0 && (
<div className="space-y-1">
<label className="text-xs font-medium text-muted-foreground">Extra producten</label>
<div className="space-y-1">
{products
.filter((p) => p.id !== productId)
.map((p) => (
<label key={p.id} className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={selectedSecondary.includes(p.id)}
onChange={(e) =>
setSelectedSecondary((prev) =>
e.target.checked ? [...prev, p.id] : prev.filter((id) => id !== p.id),
)
}
disabled={!editable || pending || submitting}
/>
{p.name}
</label>
))}
</div>
</div>
)}
{!editable && ( {!editable && (
<p className="text-xs text-muted-foreground italic"> <p className="text-xs text-muted-foreground italic">
@ -337,6 +390,7 @@ function IdeaFormSection({ idea, products, isDemo, pending }: FormProps) {
setTitle(idea.title) setTitle(idea.title)
setDescription(idea.description ?? '') setDescription(idea.description ?? '')
setProductId(idea.product_id ?? '') setProductId(idea.product_id ?? '')
setSelectedSecondary(secondaryProducts.map((sp) => sp.product_id))
}} }}
> >
Reset Reset