Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot] 2026-04-18 12:08:37 +00:00
parent 00df1c7db5
commit f7d36d03f8
5 changed files with 329 additions and 1 deletions

View file

@ -0,0 +1,45 @@
"use client";
import { Link } from "@tanstack/react-router";
import { AlertTriangle } from "lucide-react";
import { useCronjobs } from "@/hooks/use-cronjobs";
import { Button } from "@/components/ui/button";
export function AutoPausedBanner() {
const { data: jobs } = useCronjobs();
const autoPaused = (jobs ?? []).filter((j) => j.status === "auto_paused");
if (autoPaused.length === 0) return null;
return (
<div className="rounded-xl border border-destructive/40 bg-destructive/10 p-4 flex items-start gap-3">
<AlertTriangle className="h-5 w-5 text-destructive shrink-0 mt-0.5" />
<div className="flex-1 min-w-0">
<p className="font-semibold text-sm text-destructive">
{autoPaused.length === 1
? "1 automação foi pausada automaticamente"
: `${autoPaused.length} automações foram pausadas automaticamente`}
</p>
<p className="text-xs text-muted-foreground mt-1">
Geralmente isso acontece quando uma integração necessária foi desconectada ou expirou.
Reconecte a integração ou revise a automação para reativá-la.
</p>
<ul className="mt-2 text-xs space-y-0.5">
{autoPaused.slice(0, 3).map((j) => (
<li key={j.id} className="truncate">
<Link to="/painel/cronjobs/$id" params={{ id: j.id }} className="text-primary underline">
{j.name}
</Link>
{j.auto_paused_reason && (
<span className="text-muted-foreground"> {j.auto_paused_reason}</span>
)}
</li>
))}
</ul>
</div>
<Button asChild size="sm" variant="outline" className="shrink-0">
<Link to="/painel/cronjobs">Ver todas</Link>
</Button>
</div>
);
}

View file

@ -0,0 +1,99 @@
"use client";
import { Link } from "@tanstack/react-router";
import { ArrowRight, CalendarClock, Plus, AlertTriangle } from "lucide-react";
import { useCronjobs, useUserJobsLimits } from "@/hooks/use-cronjobs";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
export function CronjobsDashboardWidget() {
const { data: jobs, isLoading } = useCronjobs();
const { data: limits } = useUserJobsLimits();
if (isLoading) return <Skeleton className="h-48 w-full rounded-xl" />;
const list = jobs ?? [];
const activeCount = list.filter((j) => j.status === "active").length;
const autoPausedCount = list.filter((j) => j.status === "auto_paused").length;
const recent = list.slice(0, 3);
const planAllows = (limits?.max_jobs ?? 0) > 0;
return (
<div className="rounded-xl border border-border bg-card p-6 shadow-soft">
<div className="flex items-start justify-between gap-4 mb-4">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center">
<CalendarClock className="h-5 w-5 text-primary" />
</div>
<div>
<h3 className="font-semibold">Suas automações</h3>
<p className="text-xs text-muted-foreground">
{activeCount} ativa{activeCount === 1 ? "" : "s"}
{limits?.max_jobs ? ` de ${limits.max_jobs} disponíveis` : ""}
{autoPausedCount > 0 && (
<>
{" · "}
<span className="text-destructive font-medium">
{autoPausedCount} auto-pausada{autoPausedCount === 1 ? "" : "s"}
</span>
</>
)}
</p>
</div>
</div>
<Button asChild variant="ghost" size="sm" className="text-primary hover:text-primary">
<Link to="/painel/cronjobs">
Ver todas <ArrowRight className="ml-1 h-3.5 w-3.5" />
</Link>
</Button>
</div>
{!planAllows ? (
<div className="rounded-lg border border-dashed border-border p-6 text-center">
<p className="text-sm text-muted-foreground">
Seu plano atual não inclui automações.{" "}
<Link to="/painel/faturamento" className="text-primary underline">
Fazer upgrade
</Link>
</p>
</div>
) : list.length === 0 ? (
<div className="rounded-lg border border-dashed border-border p-6 text-center">
<p className="text-sm text-muted-foreground mb-3">
Crie sua primeira automação descrevendo em português.
</p>
<Button asChild size="sm" className="rounded-lg">
<Link to="/painel/cronjobs/nova">
<Plus className="h-4 w-4 mr-1.5" /> Nova automação
</Link>
</Button>
</div>
) : (
<ul className="space-y-2">
{recent.map((job) => (
<li key={job.id}>
<Link
to="/painel/cronjobs/$id"
params={{ id: job.id }}
className="flex items-center justify-between gap-3 rounded-lg border border-border bg-background/50 px-3 py-2.5 hover:bg-muted/50 transition-colors"
>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">{job.name}</p>
<p className="text-xs text-muted-foreground truncate">{job.human_readable}</p>
</div>
{job.status === "active" && <Badge variant="success" className="shrink-0">Ativa</Badge>}
{job.status === "paused" && <Badge variant="secondary" className="shrink-0">Pausada</Badge>}
{job.status === "auto_paused" && (
<Badge variant="destructive" className="shrink-0 gap-1">
<AlertTriangle className="h-3 w-3" /> Auto
</Badge>
)}
</Link>
</li>
))}
</ul>
)}
</div>
);
}

View file

@ -0,0 +1,96 @@
"use client";
import { Link } from "@tanstack/react-router";
import { ArrowRight, Plug, AlertCircle } from "lucide-react";
import { useIntegrationCards } from "@/hooks/use-integrations";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
export function IntegrationsDashboardWidget() {
const { cards, isLoading, limit } = useIntegrationCards();
if (isLoading) return <Skeleton className="h-48 w-full rounded-xl" />;
const connected = cards.filter((c) => c.kind === "connected");
const errored = cards.filter((c) => c.kind === "error");
const recent = [...errored, ...connected].slice(0, 3);
return (
<div className="rounded-xl border border-border bg-card p-6 shadow-soft">
<div className="flex items-start justify-between gap-4 mb-4">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center">
<Plug className="h-5 w-5 text-primary" />
</div>
<div>
<h3 className="font-semibold">Suas integrações</h3>
<p className="text-xs text-muted-foreground">
{connected.length} conectada{connected.length === 1 ? "" : "s"}
{limit?.max_integrations ? ` de ${limit.max_integrations} disponíveis` : ""}
{errored.length > 0 && (
<>
{" · "}
<span className="text-destructive font-medium">
{errored.length} com erro
</span>
</>
)}
</p>
</div>
</div>
<Button asChild variant="ghost" size="sm" className="text-primary hover:text-primary">
<Link to="/painel/integracoes">
Gerenciar <ArrowRight className="ml-1 h-3.5 w-3.5" />
</Link>
</Button>
</div>
{recent.length === 0 ? (
<div className="rounded-lg border border-dashed border-border p-6 text-center">
<p className="text-sm text-muted-foreground mb-3">
Conecte serviços como Gmail, Notion e Cal.com para ampliar seu Mika.
</p>
<Button asChild size="sm" variant="outline" className="rounded-lg">
<Link to="/painel/integracoes">Explorar integrações</Link>
</Button>
</div>
) : (
<ul className="space-y-2">
{recent.map((card) => {
const mcp = card.mcp;
return (
<li key={mcp.id}>
<Link
to="/painel/integracoes/$slug"
params={{ slug: mcp.slug }}
className="flex items-center justify-between gap-3 rounded-lg border border-border bg-background/50 px-3 py-2.5 hover:bg-muted/50 transition-colors"
>
<div className="flex items-center gap-3 min-w-0 flex-1">
<img src={mcp.icon_url} alt="" className="h-6 w-6 rounded shrink-0" />
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">{mcp.name}</p>
{card.kind === "connected" && card.integration.connected_account_email && (
<p className="text-xs text-muted-foreground truncate">
{card.integration.connected_account_email}
</p>
)}
</div>
</div>
{card.kind === "connected" && (
<Badge variant="success" className="shrink-0">Ativa</Badge>
)}
{card.kind === "error" && (
<Badge variant="destructive" className="shrink-0 gap-1">
<AlertCircle className="h-3 w-3" /> Erro
</Badge>
)}
</Link>
</li>
);
})}
</ul>
)}
</div>
);
}

View file

@ -10,6 +10,9 @@ import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { SubscriptionBanner } from "@/components/mika/SubscriptionBanner";
import { SkillsDashboardWidget } from "@/components/mika/SkillsDashboardWidget";
import { CronjobsDashboardWidget } from "@/components/mika/cronjobs/CronjobsDashboardWidget";
import { IntegrationsDashboardWidget } from "@/components/mika/integrations/IntegrationsDashboardWidget";
import { AutoPausedBanner } from "@/components/mika/cronjobs/AutoPausedBanner";
import { TelegramOnboardingWizard } from "@/components/mika/telegram/TelegramOnboardingWizard";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
@ -76,7 +79,16 @@ function DashboardPage() {
<ProvisioningCard />
)}
{subscription && subscription.status === "active" && <SkillsDashboardWidget />}
{subscription && subscription.status === "active" && (
<>
<AutoPausedBanner />
<div className="grid gap-6 lg:grid-cols-2">
<SkillsDashboardWidget />
<CronjobsDashboardWidget />
</div>
<IntegrationsDashboardWidget />
</>
)}
<TelegramOnboardingWizard open={wizardOpen} onOpenChange={setWizardOpen} />
</div>