Files
DeskcommCRM/components/admin/tenants/TenantActions.tsx
T
Rafael MelgaçoandClaude Fable 5 73a23750b2 fix(admin): criação de tenant violava check constraint de status (#28)
POST /api/v1/admin/tenants inseria status 'onboarding', valor que a check
constraint de organizations.status (active|suspended|redacted|archived)
nunca aceitou — a rota 500ava em qualquer criação. O marcador real de
onboarding no produto é onboarded_at null (mesmo modelo do signup
self-service e do gate de /onboarding), então:

- insert passa a usar status 'active' (onboarded_at null = em onboarding)
- filtro GET status=onboarding vira estado derivado: active + onboarded_at
  is null (o dropdown "Onboarding" do admin, que nunca casava nada, agora
  funciona)
- badge da listagem deriva "Onboarding" de active + onboarded_at null
- tipos de linha (useTenantDetail, TenantActions) deixam de aceitar um
  status que o banco não produz; o tipo de FILTRO mantém 'onboarding'

Sem migration: nenhuma linha com status inválido pode existir (a
constraint sempre bloqueou o INSERT).


Claude-Session: https://claude.ai/code/session_01454SGvQ7wBAvah8ZPnnWXv

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 20:21:00 +00:00

96 lines
2.7 KiB
TypeScript

"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { SuspendDialog } from "./SuspendDialog";
import { ReactivateDialog } from "./ReactivateDialog";
import { ImpersonateButton } from "@/components/admin/ImpersonateButton";
// ---------------------------------------------------------------------------
// Props
// ---------------------------------------------------------------------------
interface TenantActionsProps {
organizationId: string;
status: "active" | "suspended" | "redacted";
displayName: string;
}
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
export function TenantActions({
organizationId,
status,
displayName,
}: TenantActionsProps) {
const [suspendOpen, setSuspendOpen] = useState(false);
const [reactivateOpen, setReactivateOpen] = useState(false);
const canSuspend = status === "active";
const isSuspended = status === "suspended";
const isRedacted = status === "redacted";
return (
<>
<div className="rounded-lg border bg-card p-5 space-y-4">
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">
Ações
</h2>
{/* Impersonate (S-11.07) */}
<ImpersonateButton
organizationId={organizationId}
displayName={displayName}
disabled={isRedacted}
disabledReason={
isRedacted ? "Tenant redigido — ação não disponível" : undefined
}
/>
{/* Suspend */}
{canSuspend && (
<Button
className="w-full"
variant="destructive"
onClick={() => setSuspendOpen(true)}
aria-label="Suspender tenant"
>
Suspender tenant
</Button>
)}
{/* Reactivate */}
{isSuspended && (
<Button
className="w-full"
variant="outline"
onClick={() => setReactivateOpen(true)}
aria-label="Reativar tenant"
>
Reativar tenant
</Button>
)}
{isRedacted && (
<p className="text-xs text-muted-foreground text-center py-2">
Tenant redigido — ações de gestão não disponíveis.
</p>
)}
</div>
<SuspendDialog
open={suspendOpen}
onClose={() => setSuspendOpen(false)}
organizationId={organizationId}
/>
<ReactivateDialog
open={reactivateOpen}
onClose={() => setReactivateOpen(false)}
organizationId={organizationId}
/>
</>
);
}