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:
parent
9a733d77bb
commit
86e69fc457
1 changed files with 57 additions and 3 deletions
|
|
@ -20,7 +20,7 @@ import { getIdeaStatusBadge } from '@/lib/idea-status-colors'
|
|||
import type { IdeaStatusApi } from '@/lib/idea-status'
|
||||
import { isIdeaEditable } from '@/lib/idea-status'
|
||||
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 { IdeaMdEditor } from '@/components/ideas/idea-md-editor'
|
||||
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>
|
||||
)}
|
||||
</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>
|
||||
<IdeaRowActions idea={idea} isDemo={isDemo} onArchive={handleArchive} />
|
||||
</header>
|
||||
|
|
@ -214,6 +226,7 @@ export function IdeaDetailLayout({
|
|||
products={products}
|
||||
isDemo={isDemo}
|
||||
pending={pending}
|
||||
secondaryProducts={idea.secondary_products}
|
||||
/>
|
||||
)}
|
||||
{tab === 'grill' && (
|
||||
|
|
@ -250,9 +263,10 @@ interface FormProps {
|
|||
products: ProductOption[]
|
||||
isDemo: 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 editable =
|
||||
!isDemo &&
|
||||
|
|
@ -260,12 +274,20 @@ function IdeaFormSection({ idea, products, isDemo, pending }: FormProps) {
|
|||
const [title, setTitle] = useState(idea.title)
|
||||
const [description, setDescription] = useState(idea.description ?? '')
|
||||
const [productId, setProductId] = useState(idea.product_id ?? '')
|
||||
const [selectedSecondary, setSelectedSecondary] = useState<string[]>(
|
||||
secondaryProducts.map((sp) => sp.product_id),
|
||||
)
|
||||
const [submitting, startSubmit] = useTransition()
|
||||
|
||||
const secondaryDirty =
|
||||
JSON.stringify([...selectedSecondary].sort()) !==
|
||||
JSON.stringify(secondaryProducts.map((sp) => sp.product_id).sort())
|
||||
|
||||
const dirty =
|
||||
title !== idea.title ||
|
||||
description !== (idea.description ?? '') ||
|
||||
productId !== (idea.product_id ?? '')
|
||||
productId !== (idea.product_id ?? '') ||
|
||||
secondaryDirty
|
||||
|
||||
function save() {
|
||||
startSubmit(async () => {
|
||||
|
|
@ -278,6 +300,13 @@ function IdeaFormSection({ idea, products, isDemo, pending }: FormProps) {
|
|||
toast.error(r.error)
|
||||
return
|
||||
}
|
||||
if (secondaryDirty) {
|
||||
const r2 = await updateSecondaryProductsAction(idea.id, selectedSecondary)
|
||||
if ('error' in r2) {
|
||||
toast.error(r2.error)
|
||||
return
|
||||
}
|
||||
}
|
||||
toast.success('Opgeslagen')
|
||||
router.refresh()
|
||||
})
|
||||
|
|
@ -320,6 +349,30 @@ function IdeaFormSection({ idea, products, isDemo, pending }: FormProps) {
|
|||
))}
|
||||
</select>
|
||||
</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 && (
|
||||
<p className="text-xs text-muted-foreground italic">
|
||||
|
|
@ -337,6 +390,7 @@ function IdeaFormSection({ idea, products, isDemo, pending }: FormProps) {
|
|||
setTitle(idea.title)
|
||||
setDescription(idea.description ?? '')
|
||||
setProductId(idea.product_id ?? '')
|
||||
setSelectedSecondary(secondaryProducts.map((sp) => sp.product_id))
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue