transcription-cost-usage-re.../frontend/components/transcription-table.tsx

622 lines
22 KiB
TypeScript
Raw Normal View History

2025-06-09 11:13:05 +00:00
"use client"
2025-06-13 21:14:16 +00:00
import { useState, useEffect } from "react"
2025-06-09 11:13:05 +00:00
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Alert, AlertDescription } from "@/components/ui/alert"
2025-06-13 21:14:16 +00:00
import { Download, Search, Loader2, DollarSign, TrendingUp, Calculator } from "lucide-react"
2025-06-09 11:13:05 +00:00
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000/api/v1"
2025-06-16 19:26:37 +00:00
function formatDateBr(dateStr: string) {
2025-06-13 21:14:16 +00:00
const [year, month, day] = dateStr.split("-");
return `${day}/${month}/${year}`;
}
export function formatDateTime(raw?: string): string {
if (!raw || raw === "-") return "-"
const iso = raw.replace(" ", "T")
const date = new Date(iso)
if (isNaN(date.getTime())) return "-"
return (
date.toLocaleDateString("pt-BR", {
day: "2-digit",
month: "2-digit",
year: "numeric",
}) +
" " +
date.toLocaleTimeString("pt-BR", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
})
)
}
2025-06-09 11:13:05 +00:00
interface ClientTranscriptionData {
uniqueid: string
src: string
dst: string
start_call: string
total_billsec: number
total_min: string
client_total_cost: string
2025-06-09 11:13:05 +00:00
}
interface HitTranscriptionData {
companyId: string // Empresa
uniqueid: string // Identificador da chamada
src: string // Origem
dst: string // Destino
total_billsec: number // Quantidade de segundos
qtd_token_input: number // Quantidade de tokens(input)
qtd_token_output: number // Quantidade de tokens(output)
2025-06-13 21:14:16 +00:00
total_min: number,
custo_hit: string // Custo HIT
client_total_cost: string // Custo Cliente
client_price: string // Preço Cliente por Minuto
start_call: string // Inicio
end_call: string // Fim
2025-06-09 11:13:05 +00:00
}
type TranscriptionData = ClientTranscriptionData | HitTranscriptionData
2025-06-13 21:14:16 +00:00
// Interface para informações de custo
interface CostInfo {
company_id: string
start_date: string
end_date: string
total_cost_hit: number
total_client_cost: number
}
// Interface para cotação do dólar
interface ExchangeRateResponse {
USDBRL: {
code: string
codein: string
name: string
high: string
low: string
varBid: string
pctChange: string
bid: string
ask: string
timestamp: string
create_date: string
}
}
2025-06-09 11:13:05 +00:00
interface PaginationInfo {
total: number
page: number
page_size: number
total_pages: number
}
interface ApiResponse {
success: boolean
data: {
data: TranscriptionData[]
pagination: PaginationInfo
2025-06-13 21:14:16 +00:00
cost?: CostInfo
2025-06-09 11:13:05 +00:00
}
}
export default function TranscriptionTable() {
const [data, setData] = useState<TranscriptionData[]>([])
2025-06-13 21:14:16 +00:00
const [costInfo, setCostInfo] = useState<CostInfo | null>(null)
const [exchangeRate, setExchangeRate] = useState<number | null>(null)
2025-06-09 11:13:05 +00:00
const [isLoading, setIsLoading] = useState(false)
const [isExporting, setIsExporting] = useState(false)
const [error, setError] = useState("")
// Filtros
const [companyId, setCompanyId] = useState("")
const [startDate, setStartDate] = useState("")
const [endDate, setEndDate] = useState("")
const [who, setWho] = useState<"client" | "hit">("client")
2025-06-13 21:14:16 +00:00
// Estados para paginação
2025-06-09 11:13:05 +00:00
const [pagination, setPagination] = useState<PaginationInfo>({
total: 0,
page: 1,
page_size: 20,
total_pages: 0,
})
const [currentPage, setCurrentPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
const getAuthHeaders = () => {
const token = localStorage.getItem("access_token")
return {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
}
}
2025-06-13 21:14:16 +00:00
// Função para buscar a cotação do dólar
const fetchExchangeRate = async () => {
try {
const response = await fetch("https://economia.awesomeapi.com.br/json/last/USD-BRL")
if (response.ok) {
const data: ExchangeRateResponse = await response.json()
const rate = Number.parseFloat(data.USDBRL.ask) // Usando o valor de venda (ask)
setExchangeRate(rate)
return rate
}
} catch (err) {
console.log("Erro ao buscar cotação do dólar:", err)
// Usar uma cotação padrão em caso de erro
setExchangeRate(5.5)
return 5.5
}
return null
}
2025-06-09 11:13:05 +00:00
const fetchData = async (page: number = currentPage) => {
if (!companyId || !startDate || !endDate) {
setError("Preencha todos os campos obrigatórios")
return
}
setIsLoading(true)
setError("")
try {
2025-06-13 21:14:16 +00:00
// Buscar cotação do dólar em paralelo
const ratePromise = fetchExchangeRate()
2025-06-09 11:13:05 +00:00
const params = new URLSearchParams({
companyId,
startDate,
endDate,
who,
page: page.toString(),
page_size: pageSize.toString(),
})
const response = await fetch(`${API_BASE_URL}/usage/data/trascription?${params}`, {
headers: getAuthHeaders(),
})
if (response.ok) {
const result: ApiResponse = await response.json()
if (result.success && result.data) {
setData(result.data.data || [])
setPagination(result.data.pagination)
2025-06-13 21:14:16 +00:00
setCostInfo(result.data.cost || null)
2025-06-09 11:13:05 +00:00
setCurrentPage(result.data.pagination.page)
} else {
setData([])
2025-06-13 21:14:16 +00:00
setCostInfo(null)
2025-06-09 11:13:05 +00:00
setPagination({ total: 0, page: 1, page_size: 20, total_pages: 0 })
}
} else {
const errorData = await response.json()
setError(errorData.message || "Erro ao buscar dados")
}
2025-06-13 21:14:16 +00:00
// Aguardar a cotação do dólar
await ratePromise
2025-06-09 11:13:05 +00:00
} catch (err) {
2025-06-09 13:32:46 +00:00
console.log("====> Erro de conexão com o servidor: ", err)
2025-06-09 11:13:05 +00:00
setError("Erro de conexão com o servidor")
} finally {
setIsLoading(false)
}
}
const exportToExcel = async () => {
if (!companyId || !startDate || !endDate) {
setError("Preencha todos os campos obrigatórios")
return
}
setIsExporting(true)
setError("")
try {
const params = new URLSearchParams({
companyId,
startDate,
endDate,
who,
})
const response = await fetch(`${API_BASE_URL}/usage/export/trascription?${params}`, {
headers: getAuthHeaders(),
})
if (response.ok) {
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = `transcription-report-${who}-${startDate}-${endDate}.xlsx`
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
} else {
const errorData = await response.json()
setError(errorData.message || "Erro ao exportar dados")
}
} catch (err) {
2025-06-09 13:32:46 +00:00
console.log("====> Erro de conexão com o servidor: ", err)
2025-06-09 11:13:05 +00:00
setError("Erro de conexão com o servidor")
} finally {
setIsExporting(false)
}
}
2025-06-13 21:14:16 +00:00
// Funções de navegação de páginas
2025-06-09 11:13:05 +00:00
const goToPage = (page: number) => {
if (page >= 1 && page <= pagination.total_pages) {
setCurrentPage(page)
fetchData(page)
}
}
const goToFirstPage = () => goToPage(1)
const goToPreviousPage = () => goToPage(currentPage - 1)
const goToNextPage = () => goToPage(currentPage + 1)
const goToLastPage = () => goToPage(pagination.total_pages)
2025-06-13 21:14:16 +00:00
// Função para formatar valores monetários
const formatCurrency = (value: number, currency = "USD"): string => {
return new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: currency,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(value)
}
// Função para converter USD para BRL
const convertUsdToBrl = (usdValue: number): number => {
if (!exchangeRate) return usdValue
return usdValue * exchangeRate
}
// Buscar cotação do dólar quando o componente for montado
useEffect(() => {
fetchExchangeRate()
}, [])
2025-06-09 11:13:05 +00:00
return (
<div className="space-y-6">
2025-06-13 21:14:16 +00:00
{/* Card de Informações de Custo */}
{costInfo && exchangeRate && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Custo Total HIT</CardTitle>
<Calculator className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-blue-600">
{formatCurrency(convertUsdToBrl(costInfo.total_cost_hit), "BRL")}
</div>
<p className="text-xs text-muted-foreground">
Empresa {costInfo.company_id} {formatCurrency(costInfo.total_cost_hit, "USD")}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Custo Total Cliente</CardTitle>
<DollarSign className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">
{formatCurrency(costInfo.total_client_cost, "BRL")}
</div>
<p className="text-xs text-muted-foreground">
2025-06-16 19:26:37 +00:00
{formatDateBr(costInfo.start_date)} até {formatDateBr(costInfo.end_date)}
2025-06-13 21:14:16 +00:00
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Margem</CardTitle>
<TrendingUp className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-purple-600">
{formatCurrency(costInfo.total_client_cost - convertUsdToBrl(costInfo.total_cost_hit), "BRL")}
</div>
<p className="text-xs text-muted-foreground">
{(
((costInfo.total_client_cost - convertUsdToBrl(costInfo.total_cost_hit)) /
costInfo.total_client_cost) *
100
).toFixed(1)}
% de margem
</p>
</CardContent>
</Card>
</div>
)}
2025-06-09 11:13:05 +00:00
{/* Filtros */}
<Card>
<CardHeader>
<CardTitle>Filtros de Consulta</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4">
<div className="space-y-2">
<Label htmlFor="companyId">ID da Empresa *</Label>
<Input
id="companyId"
value={companyId}
onChange={(e) => setCompanyId(e.target.value)}
placeholder="123"
/>
</div>
<div className="space-y-2">
<Label htmlFor="startDate">Data Inicial *</Label>
<Input id="startDate" type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} />
</div>
<div className="space-y-2">
<Label htmlFor="endDate">Data Final *</Label>
<Input id="endDate" type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} />
</div>
<div className="space-y-2">
<Label htmlFor="who">Tipo de Consulta *</Label>
<Select value={who} onValueChange={(value: "client" | "hit") => setWho(value)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="client">Cliente</SelectItem>
<SelectItem value="hit">HIT</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="pageSize">Registros por página</Label>
<Select
value={pageSize.toString()}
2025-06-13 21:14:16 +00:00
onValueChange={(value: string) => {
2025-06-09 11:13:05 +00:00
setPageSize(Number(value))
setCurrentPage(1)
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="10">10</SelectItem>
<SelectItem value="20">20</SelectItem>
<SelectItem value="50">50</SelectItem>
<SelectItem value="100">100</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>&nbsp;</Label>
<div className="flex gap-2">
<Button onClick={() => fetchData(1)} disabled={isLoading} className="flex-1">
{isLoading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Search className="mr-2 h-4 w-4" />}
Buscar
</Button>
<Button onClick={exportToExcel} disabled={isExporting || data.length === 0} variant="outline">
{isExporting ? <Loader2 className="h-4 w-4 animate-spin" /> : <Download className="h-4 w-4" />}
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
2025-06-13 21:14:16 +00:00
{/* Indicador de cotação do dólar */}
{exchangeRate && (
<div className="flex justify-end">
<div className="text-xs text-gray-500 bg-gray-50 px-2 py-1 rounded">
Cotação USD/BRL: R$ {exchangeRate.toFixed(4)}
</div>
</div>
)}
2025-06-09 11:13:05 +00:00
{/* Tabela de Dados */}
<Card>
<CardHeader>
<CardTitle>
Resultados da Consulta
{pagination.total > 0 && (
<span className="text-sm font-normal text-gray-500 ml-2">
(Página {currentPage} de {pagination.total_pages} - {pagination.total} registros total)
</span>
)}
</CardTitle>
</CardHeader>
<CardContent>
{data.length > 0 ? (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
{who === "hit" ? (
2025-06-09 11:13:05 +00:00
<>
<TableHead>Empresa</TableHead>
<TableHead>Identificador da chamada</TableHead>
<TableHead>Origem</TableHead>
<TableHead>Destino</TableHead>
2025-06-13 21:14:16 +00:00
<TableHead>Segundos</TableHead>
<TableHead>Minutos</TableHead>
<TableHead>Custo Cliente (R$)</TableHead>
<TableHead>Preço Cliente p/ Minuto (R$)</TableHead>
<TableHead>Custo HIT ($)</TableHead>
<TableHead>Início</TableHead>
<TableHead>Fim</TableHead>
2025-06-13 21:14:16 +00:00
<TableHead>Tokens (Input)</TableHead>
<TableHead>Tokens (Output)</TableHead>
</>
) : (
<>
<TableHead>ID Único</TableHead>
<TableHead>Origem</TableHead>
<TableHead>Destino</TableHead>
<TableHead>Início</TableHead>
<TableHead>Duração (s)</TableHead>
2025-06-13 21:14:16 +00:00
<TableHead>Duração (m)</TableHead>
<TableHead>Custo (R$)</TableHead>
2025-06-09 11:13:05 +00:00
</>
)}
</TableRow>
</TableHeader>
<TableBody>
{data.map((item, index) => (
<TableRow key={item.uniqueid || index}>
{who === "hit" ? (
2025-06-09 11:13:05 +00:00
<>
<TableCell>{(item as HitTranscriptionData).companyId || "-"}</TableCell>
<TableCell>{(item as HitTranscriptionData).uniqueid || "-"}</TableCell>
<TableCell>{(item as HitTranscriptionData).src || "-"}</TableCell>
<TableCell>{(item as HitTranscriptionData).dst || "-"}</TableCell>
<TableCell>{(item as HitTranscriptionData).total_billsec || "-"}</TableCell>
2025-06-16 19:26:37 +00:00
<TableCell>{(item as HitTranscriptionData).total_min || "-"}</TableCell>
2025-06-13 21:14:16 +00:00
<TableCell>
2025-06-16 19:26:37 +00:00
{(item as HitTranscriptionData)?.client_total_cost
? `R$ ${Number((item as HitTranscriptionData).client_total_cost).toFixed(2)}`
2025-06-13 21:14:16 +00:00
: "-"}
</TableCell>
2025-06-16 19:26:37 +00:00
2025-06-09 11:13:05 +00:00
<TableCell>
2025-06-13 21:14:16 +00:00
{(item as HitTranscriptionData).client_price
? `R$ ${(item as HitTranscriptionData).client_price}`
: "-"}
2025-06-09 11:13:05 +00:00
</TableCell>
<TableCell>
2025-06-16 19:26:37 +00:00
{(item as HitTranscriptionData)?.custo_hit
? `$ ${Number((item as HitTranscriptionData).custo_hit).toFixed(2)}`
2025-06-13 21:14:16 +00:00
: "-"}
2025-06-09 11:13:05 +00:00
</TableCell>
2025-06-16 19:26:37 +00:00
2025-06-13 21:14:16 +00:00
<TableCell>{formatDateTime((item as HitTranscriptionData).start_call || "-")}</TableCell>
<TableCell>{formatDateTime((item as HitTranscriptionData).end_call || "-")}</TableCell>
<TableCell>{(item as HitTranscriptionData).qtd_token_input || "-"}</TableCell>
<TableCell>{(item as HitTranscriptionData).qtd_token_output || "-"}</TableCell>
</>
) : (
<>
<TableCell>{(item as ClientTranscriptionData).uniqueid || "-"}</TableCell>
<TableCell>{(item as ClientTranscriptionData).src || "-"}</TableCell>
<TableCell>{(item as ClientTranscriptionData).dst || "-"}</TableCell>
2025-06-13 21:14:16 +00:00
<TableCell>{formatDateTime((item as ClientTranscriptionData).start_call || "-")}</TableCell>
<TableCell>{(item as ClientTranscriptionData).total_billsec || "-"}</TableCell>
<TableCell>{(item as ClientTranscriptionData).total_min || "-"}</TableCell>
2025-06-13 21:14:16 +00:00
<TableCell>
2025-06-16 19:26:37 +00:00
{(item as ClientTranscriptionData)?.client_total_cost
? `R$ ${Number((item as ClientTranscriptionData).client_total_cost).toFixed(2)}`
2025-06-13 21:14:16 +00:00
: "-"}
</TableCell>
2025-06-16 19:26:37 +00:00
2025-06-09 11:13:05 +00:00
</>
)}
</TableRow>
))}
</TableBody>
</Table>
</div>
) : (
<div className="text-center py-8 text-gray-500">
Nenhum dado encontrado. Use os filtros acima para buscar dados.
</div>
)}
{/* Componente de Paginação */}
{pagination.total > 0 && (
<div className="flex items-center justify-between px-2 py-4">
<div className="flex items-center space-x-2">
<p className="text-sm text-gray-700">
Mostrando <span className="font-medium">{(currentPage - 1) * pagination.page_size + 1}</span> até{" "}
<span className="font-medium">{Math.min(currentPage * pagination.page_size, pagination.total)}</span>{" "}
de <span className="font-medium">{pagination.total}</span> resultados
</p>
</div>
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-1">
<Button variant="outline" size="sm" onClick={goToFirstPage} disabled={currentPage === 1}>
Primeira
</Button>
<Button variant="outline" size="sm" onClick={goToPreviousPage} disabled={currentPage === 1}>
Anterior
</Button>
<div className="flex items-center space-x-1">
{/* Páginas numeradas */}
{Array.from({ length: Math.min(5, pagination.total_pages) }, (_, i) => {
let pageNum
if (pagination.total_pages <= 5) {
pageNum = i + 1
} else if (currentPage <= 3) {
pageNum = i + 1
} else if (currentPage >= pagination.total_pages - 2) {
pageNum = pagination.total_pages - 4 + i
} else {
pageNum = currentPage - 2 + i
}
return (
<Button
key={pageNum}
variant={currentPage === pageNum ? "default" : "outline"}
size="sm"
onClick={() => goToPage(pageNum)}
className="w-10"
>
{pageNum}
</Button>
)
})}
</div>
<Button
variant="outline"
size="sm"
onClick={goToNextPage}
disabled={currentPage === pagination.total_pages}
>
Próxima
</Button>
<Button
variant="outline"
size="sm"
onClick={goToLastPage}
disabled={currentPage === pagination.total_pages}
>
Última
</Button>
</div>
</div>
</div>
)}
</CardContent>
</Card>
</div>
)
}