feat(frontend): tieneAvisos en RubroTreeNode + disable btn subrubro (CAT-002)
This commit is contained in:
@@ -101,11 +101,14 @@ export function CategoryTreeNode({
|
|||||||
{/* Action buttons — only if canEdit */}
|
{/* Action buttons — only if canEdit */}
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
{/* CAT-002: disabled when leaf-with-avisos; PRD-002 activates the real data path */}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-6 w-6 p-0"
|
className="h-6 w-6 p-0"
|
||||||
aria-label={`Agregar subrubro en ${node.nombre}`}
|
aria-label={`Agregar subrubro en ${node.nombre}`}
|
||||||
|
disabled={node.tieneAvisos === true}
|
||||||
|
title={node.tieneAvisos === true ? 'El rubro contiene avisos asignados. Muévalos antes de agregar sub-rubros.' : undefined}
|
||||||
onClick={() => onAddChild(node.id)}
|
onClick={() => onAddChild(node.id)}
|
||||||
>
|
>
|
||||||
<Plus className="h-3.5 w-3.5" />
|
<Plus className="h-3.5 w-3.5" />
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ export interface RubroTreeNode {
|
|||||||
activo: boolean
|
activo: boolean
|
||||||
parentId: number | null
|
parentId: number | null
|
||||||
tarifarioBaseId: number | null
|
tarifarioBaseId: number | null
|
||||||
|
// CAT-002: additive field — optional for backward-compat (PRD-002 always sends it)
|
||||||
|
tieneAvisos?: boolean
|
||||||
hijos: RubroTreeNode[]
|
hijos: RubroTreeNode[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,62 @@ describe('CategoryTree', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ── CAT-002: tieneAvisos disables "Agregar subrubro" button ──────────────────
|
||||||
|
|
||||||
|
describe('CategoryTreeNode tieneAvisos (CAT-002)', () => {
|
||||||
|
it('BotonAgregarSubrubro_Disabled_CuandoTieneAvisosTrue', () => {
|
||||||
|
const nodeConAvisos: RubroTreeNode = {
|
||||||
|
id: 10,
|
||||||
|
nombre: 'ConAvisos',
|
||||||
|
orden: 1,
|
||||||
|
activo: true,
|
||||||
|
parentId: null,
|
||||||
|
tarifarioBaseId: null,
|
||||||
|
tieneAvisos: true,
|
||||||
|
hijos: [],
|
||||||
|
}
|
||||||
|
render(
|
||||||
|
<CategoryTree
|
||||||
|
nodes={[nodeConAvisos]}
|
||||||
|
onEdit={noop}
|
||||||
|
onDelete={noop}
|
||||||
|
onAddChild={noop}
|
||||||
|
onMove={noop}
|
||||||
|
canEdit={true}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
const addBtn = screen.getByRole('button', { name: /agregar subrubro en conavisos/i })
|
||||||
|
expect(addBtn).toBeDisabled()
|
||||||
|
expect(addBtn).toHaveAttribute('title')
|
||||||
|
expect(addBtn.getAttribute('title')).toMatch(/El rubro contiene avisos/i)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('BotonAgregarSubrubro_Enabled_CuandoTieneAvisosFalse', () => {
|
||||||
|
const nodeSinAvisos: RubroTreeNode = {
|
||||||
|
id: 11,
|
||||||
|
nombre: 'SinAvisos',
|
||||||
|
orden: 1,
|
||||||
|
activo: true,
|
||||||
|
parentId: null,
|
||||||
|
tarifarioBaseId: null,
|
||||||
|
tieneAvisos: false,
|
||||||
|
hijos: [],
|
||||||
|
}
|
||||||
|
render(
|
||||||
|
<CategoryTree
|
||||||
|
nodes={[nodeSinAvisos]}
|
||||||
|
onEdit={noop}
|
||||||
|
onDelete={noop}
|
||||||
|
onAddChild={noop}
|
||||||
|
onMove={noop}
|
||||||
|
canEdit={true}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
const addBtn = screen.getByRole('button', { name: /agregar subrubro en sinavisos/i })
|
||||||
|
expect(addBtn).not.toBeDisabled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('CategoryTreeNode depth guard', () => {
|
describe('CategoryTreeNode depth guard', () => {
|
||||||
it('renders depth warning when depth exceeds 10', () => {
|
it('renders depth warning when depth exceeds 10', () => {
|
||||||
// Build a deeply nested node at depth 11
|
// Build a deeply nested node at depth 11
|
||||||
|
|||||||
@@ -313,6 +313,35 @@ describe('MoveRubroDialog', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('displays backend error inline when 409 rubro_padre_es_hoja_con_avisos (CAT-002)', async () => {
|
||||||
|
mockMutateAsync.mockRejectedValue({
|
||||||
|
response: {
|
||||||
|
status: 409,
|
||||||
|
data: {
|
||||||
|
error: 'rubro_padre_es_hoja_con_avisos',
|
||||||
|
message: 'El rubro padre contiene 3 avisos. Muévalos antes de crear sub-rubros.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
wrap(
|
||||||
|
<MoveRubroDialog
|
||||||
|
open={true}
|
||||||
|
onOpenChange={vi.fn()}
|
||||||
|
rubro={rubroUsados}
|
||||||
|
tree={fullTree}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument())
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: /mover/i }))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText(/el rubro padre contiene 3 avisos/i),
|
||||||
|
).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('displays backend error inline when 422 depth', async () => {
|
it('displays backend error inline when 422 depth', async () => {
|
||||||
mockMutateAsync.mockRejectedValue({
|
mockMutateAsync.mockRejectedValue({
|
||||||
response: {
|
response: {
|
||||||
|
|||||||
Reference in New Issue
Block a user