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 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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue