$ open projects/case-flow
InovaApp2025 | operações e suporte
Case Flow pega conversa solta e vira chamado organizado.
Sistema de chamados com Kanban, controle por setor e chatbot que entende a mensagem do usuário, extrai contexto e cria ticket com categoria e prioridade.
$ cat story.md
História
O Case Flow ataca um problema comum: o usuário explica o problema do jeito dele, mas o time precisa de um ticket organizado para agir. A IA fica nessa passagem. Ela entende a mensagem, sugere estrutura e entrega ao Kanban um chamado mais pronto para triagem.
$ cat architecture.svg
Arquitetura
$ grep selling-points
Por que vale olhar
Automação sem teatro
A IA entra onde ajuda: transformar texto livre em dados que o time consegue usar.
Responsabilidade por setor
Roles e categorias deixam claro quem pode ver, assumir e resolver cada demanda.
Atendimento fechado
Ticket, comentários, status, Kanban e histórico ficam no mesmo fluxo.
$ view code --tabs
Viewer de código
model Ticket {
id String @id @default(cuid())
title String
description String @db.Text
category TicketCategory
status TicketStatus @default(NEW)
priority Priority @default(MEDIUM)
createdBy String
assignedTo String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
creator User @relation("CreatedTickets", fields: [createdBy], references: [id])
assignee User? @relation("AssignedTickets", fields: [assignedTo], references: [id])
comments Comment[]
}
enum UserRole {
ADM
TI
SAC
FINANCEIRO
}
enum TicketStatus {
NEW
IN_PROGRESS
FINISHED
RETURNED
}
export const authOptions: NextAuthOptions = {
providers: [
CredentialsProvider({
name: "credentials",
credentials: { email: { type: "email" }, password: { type: "password" } },
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) return null;
const user = await prisma.user.findUnique({ where: { email: credentials.email } });
if (!user) return null;
const isPasswordValid = await bcrypt.compare(credentials.password, user.password);
if (!isPasswordValid) return null;
return { id: user.id, email: user.email, name: user.name, role: user.role };
}
})
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = (user as any).role;
token.id = user.id;
}
return token;
},
async session({ session, token }) {
if (session.user && token) {
(session.user as any).id = token.id;
(session.user as any).role = token.role;
}
return session;
}
}
}
export async function POST(request: NextRequest) {
try {
const session = await getServerSession(authOptions)
if (!session?.user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 })
}
const { message } = await request.json()
// Detecta se a mensagem do chat é uma intenção de criar um chamado
const isTicketRequest = detectTicketIntent(message)
return Response.json({ isTicketRequest, message })
} catch (error) {
console.error("Intent analysis error:", error)
return Response.json({ error: "Internal server error" }, { status: 500 })
}
}
function detectTicketIntent(message: string): boolean {
const ticketPhrases = ["abrir um chamado", "criar um chamado", "abrir um ticket"]
return ticketPhrases.some((phrase) => message.toLowerCase().includes(phrase))
}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="grid grid-cols-1 lg:grid-cols-4 gap-6"
>
{Object.entries(statusConfig).map(([status, config], index) => {
const statusTickets = getTicketsByStatus(status)
return (
<motion.div
key={status}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1 * index }}
className={`${config.bgColor} ${config.borderColor} border rounded-xl p-4 min-h-[500px]`}
>
<div className="flex items-center gap-3 mb-4">
<div className={`p-2 ${config.color} rounded-lg`}>
<config.icon className="h-5 w-5 text-white" />
</div>
<div className="flex-1">
<h3 className="font-semibold text-foreground">{config.title}</h3>
<p className="text-sm text-muted-foreground">{statusTickets.length} chamados</p>
</div>
</div>
<div className="space-y-3">
{statusTickets.map((ticket) => (
<TicketCard key={ticket.id} ticket={ticket} onUpdate={handleTicketUpdate} />
))}
</div>
</motion.div>
)
})}
</motion.div>
<Card className="h-full hover:shadow-lg transition-all duration-200 border-l-4 border-l-blue-500">
<CardHeader className="pb-3">
<div className="flex items-start justify-between gap-2">
<h3 className="font-medium text-sm line-clamp-2 flex-1">
{ticket.title}
</h3>
<Badge className={cn("text-xs", priorityColors[ticket.priority])}>
{ticket.priority}
</Badge>
</div>
<div className="flex flex-wrap gap-1">
<Badge variant="outline" className={cn("text-xs", categoryColors[ticket.category])}>
{ticket.category}
</Badge>
</div>
</CardHeader>
<CardContent className="pt-0">
<p className="text-xs text-muted-foreground line-clamp-2 mb-3">
{ticket.description}
</p>
<div className="flex items-center justify-between text-xs text-muted-foreground">
<div className="flex items-center gap-1">
<User className="h-3 w-3" />
<span>{ticket.creator.name}</span>
</div>
<div className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
<span>{formatDistanceToNow(new Date(ticket.createdAt), { locale: ptBR })}</span>
</div>
</div>
</CardContent>
</Card>
$ result
Mostra IA no meio da operação: entende a intenção, organiza a demanda e manda o chamado para o fluxo de suporte.