feat(web): banner y disabled de secciones de medio inactivo — issue #16
This commit is contained in:
@@ -18,9 +18,10 @@ interface DeactivateSeccionModalProps {
|
|||||||
seccionId: number
|
seccionId: number
|
||||||
seccionNombre: string
|
seccionNombre: string
|
||||||
activo: boolean
|
activo: boolean
|
||||||
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DeactivateSeccionModal({ seccionId, seccionNombre, activo }: DeactivateSeccionModalProps) {
|
export function DeactivateSeccionModal({ seccionId, seccionNombre, activo, disabled = false }: DeactivateSeccionModalProps) {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const { mutate: deactivate, isPending: deactivating } = useDeactivateSeccion()
|
const { mutate: deactivate, isPending: deactivating } = useDeactivateSeccion()
|
||||||
const { mutate: reactivate, isPending: reactivating } = useReactivateSeccion()
|
const { mutate: reactivate, isPending: reactivating } = useReactivateSeccion()
|
||||||
@@ -38,7 +39,7 @@ export function DeactivateSeccionModal({ seccionId, seccionNombre, activo }: Dea
|
|||||||
return (
|
return (
|
||||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button variant="outline" size="sm">
|
<Button variant="outline" size="sm" disabled={disabled}>
|
||||||
{activo ? 'Desactivar' : 'Reactivar'}
|
{activo ? 'Desactivar' : 'Reactivar'}
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { AlertTriangle } from 'lucide-react'
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||||
|
|
||||||
|
interface MedioInactivoBannerProps {
|
||||||
|
medioNombre: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MedioInactivoBanner({ medioNombre }: MedioInactivoBannerProps) {
|
||||||
|
return (
|
||||||
|
<Alert variant="destructive" className="mb-4">
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<AlertTitle>Medio desactivado</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
El medio "{medioNombre}" está desactivado. Las operaciones de edición, activación y
|
||||||
|
desactivación de sus secciones están bloqueadas hasta que se reactive el medio.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -11,9 +11,10 @@ import { DeactivateSeccionModal } from './DeactivateSeccionModal'
|
|||||||
|
|
||||||
interface SeccionesTableProps {
|
interface SeccionesTableProps {
|
||||||
rows: SeccionListItem[]
|
rows: SeccionListItem[]
|
||||||
|
medioInactivo?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SeccionesTable({ rows }: SeccionesTableProps) {
|
export function SeccionesTable({ rows, medioInactivo = false }: SeccionesTableProps) {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const columns = useMemo<ColumnDef<SeccionListItem>[]>(
|
const columns = useMemo<ColumnDef<SeccionListItem>[]>(
|
||||||
@@ -73,6 +74,7 @@ export function SeccionesTable({ rows }: SeccionesTableProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
disabled={medioInactivo}
|
||||||
onClick={() => navigate(`/admin/secciones/${row.original.id}/edit`)}
|
onClick={() => navigate(`/admin/secciones/${row.original.id}/edit`)}
|
||||||
>
|
>
|
||||||
Editar
|
Editar
|
||||||
@@ -81,6 +83,7 @@ export function SeccionesTable({ rows }: SeccionesTableProps) {
|
|||||||
seccionId={row.original.id}
|
seccionId={row.original.id}
|
||||||
seccionNombre={row.original.nombre}
|
seccionNombre={row.original.nombre}
|
||||||
activo={row.original.activo}
|
activo={row.original.activo}
|
||||||
|
disabled={medioInactivo}
|
||||||
/>
|
/>
|
||||||
</CanPerform>
|
</CanPerform>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,7 +91,7 @@ export function SeccionesTable({ rows }: SeccionesTableProps) {
|
|||||||
meta: { priority: 'high' },
|
meta: { priority: 'high' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[navigate],
|
[navigate, medioInactivo],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { CanPerform } from '@/components/auth/CanPerform'
|
import { CanPerform } from '@/components/auth/CanPerform'
|
||||||
import { useSeccion } from '../hooks/useSeccion'
|
import { useSeccion } from '../hooks/useSeccion'
|
||||||
import { DeactivateSeccionModal } from '../components/DeactivateSeccionModal'
|
import { DeactivateSeccionModal } from '../components/DeactivateSeccionModal'
|
||||||
|
import { MedioInactivoBanner } from '../components/MedioInactivoBanner'
|
||||||
import { tipoSeccionLabel } from '../tipoSeccion'
|
import { tipoSeccionLabel } from '../tipoSeccion'
|
||||||
|
import { useMedio } from '../../medios/hooks/useMedio'
|
||||||
|
|
||||||
function formatDate(iso: string | null): string {
|
function formatDate(iso: string | null): string {
|
||||||
if (!iso) return '—'
|
if (!iso) return '—'
|
||||||
@@ -21,6 +23,8 @@ export function SeccionDetailPage() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const { data: seccion, isLoading } = useSeccion(seccionId)
|
const { data: seccion, isLoading } = useSeccion(seccionId)
|
||||||
|
const { data: medio } = useMedio(seccion?.medioId ?? 0)
|
||||||
|
const medioInactivo = medio?.activo === false
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -79,10 +83,15 @@ export function SeccionDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{medioInactivo && medio && (
|
||||||
|
<MedioInactivoBanner medioNombre={medio.nombre} />
|
||||||
|
)}
|
||||||
|
|
||||||
<CanPerform permission="administracion:secciones:gestionar">
|
<CanPerform permission="administracion:secciones:gestionar">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
disabled={medioInactivo}
|
||||||
onClick={() => navigate(`/admin/secciones/${seccionId}/edit`)}
|
onClick={() => navigate(`/admin/secciones/${seccionId}/edit`)}
|
||||||
>
|
>
|
||||||
Editar
|
Editar
|
||||||
@@ -91,6 +100,7 @@ export function SeccionDetailPage() {
|
|||||||
seccionId={seccionId}
|
seccionId={seccionId}
|
||||||
seccionNombre={seccion.nombre}
|
seccionNombre={seccion.nombre}
|
||||||
activo={seccion.activo}
|
activo={seccion.activo}
|
||||||
|
disabled={medioInactivo}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CanPerform>
|
</CanPerform>
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import { Skeleton } from '@/components/ui/skeleton'
|
|||||||
import { CanPerform } from '@/components/auth/CanPerform'
|
import { CanPerform } from '@/components/auth/CanPerform'
|
||||||
import { SeccionesTable } from '../components/SeccionesTable'
|
import { SeccionesTable } from '../components/SeccionesTable'
|
||||||
import { SeccionesFilters } from '../components/SeccionesFilters'
|
import { SeccionesFilters } from '../components/SeccionesFilters'
|
||||||
|
import { MedioInactivoBanner } from '../components/MedioInactivoBanner'
|
||||||
import { useSeccionesList } from '../hooks/useSeccionesList'
|
import { useSeccionesList } from '../hooks/useSeccionesList'
|
||||||
|
import { useMedio } from '../../medios/hooks/useMedio'
|
||||||
import type { TipoSeccion } from '../types'
|
import type { TipoSeccion } from '../types'
|
||||||
|
|
||||||
export function SeccionesListPage() {
|
export function SeccionesListPage() {
|
||||||
@@ -28,6 +30,10 @@ export function SeccionesListPage() {
|
|||||||
|
|
||||||
const { data, isLoading } = useSeccionesList(query)
|
const { data, isLoading } = useSeccionesList(query)
|
||||||
|
|
||||||
|
// Fetch parent medio only when filtering by a single medioId
|
||||||
|
const { data: filteredMedio } = useMedio(medioId ?? 0)
|
||||||
|
const medioInactivo = medioId !== undefined && filteredMedio?.activo === false
|
||||||
|
|
||||||
const handleMedioIdChange = useCallback((value: number | undefined) => {
|
const handleMedioIdChange = useCallback((value: number | undefined) => {
|
||||||
setMedioId(value)
|
setMedioId(value)
|
||||||
setPage(1)
|
setPage(1)
|
||||||
@@ -57,12 +63,20 @@ export function SeccionesListPage() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="text-xl font-semibold">Secciones</h1>
|
<h1 className="text-xl font-semibold">Secciones</h1>
|
||||||
<CanPerform permission="administracion:secciones:gestionar">
|
<CanPerform permission="administracion:secciones:gestionar">
|
||||||
<Button onClick={() => navigate('/admin/secciones/nuevo')} size="sm">
|
<Button
|
||||||
|
onClick={() => navigate('/admin/secciones/nuevo')}
|
||||||
|
size="sm"
|
||||||
|
disabled={medioInactivo}
|
||||||
|
>
|
||||||
Nueva sección
|
Nueva sección
|
||||||
</Button>
|
</Button>
|
||||||
</CanPerform>
|
</CanPerform>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{medioInactivo && filteredMedio && (
|
||||||
|
<MedioInactivoBanner medioNombre={filteredMedio.nombre} />
|
||||||
|
)}
|
||||||
|
|
||||||
<SeccionesFilters
|
<SeccionesFilters
|
||||||
onMedioIdChange={handleMedioIdChange}
|
onMedioIdChange={handleMedioIdChange}
|
||||||
onTipoChange={handleTipoChange}
|
onTipoChange={handleTipoChange}
|
||||||
@@ -77,7 +91,7 @@ export function SeccionesListPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<SeccionesTable rows={data?.items ?? []} />
|
<SeccionesTable rows={data?.items ?? []} medioInactivo={medioInactivo} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
|
|||||||
Reference in New Issue
Block a user