added card total cost

master
adriano 2025-06-13 18:14:16 -03:00
parent 306ba95bc2
commit b0fa541fbc
10 changed files with 330 additions and 89 deletions

View File

@ -20,6 +20,8 @@ flask-bcrypt = "*"
flask-cors = "*" flask-cors = "*"
mypy = "*" mypy = "*"
requests = "*" requests = "*"
redis = "*"
flask-redis = "*"
[dev-packages] [dev-packages]

26
backend/Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "3798a8dec1f82bd3b8d8e6089e8110ae5e152e1e76d9b78810ba5b4cfab33c2f" "sha256": "424e88c981f7de9a38bc3d2c0189df4f377bb72daa06d4b7a0b742a51b7ad51e"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -276,6 +276,15 @@
"markers": "python_version >= '3.9' and python_version < '4'", "markers": "python_version >= '3.9' and python_version < '4'",
"version": "==4.7.1" "version": "==4.7.1"
}, },
"flask-redis": {
"hashes": [
"sha256:8d79eef4eb1217095edab603acc52f935b983ae4b7655ee7c82c0dfd87315d17",
"sha256:e1fccc11e7ea35c2a4d68c0b9aa58226a098e45e834d615c7b6c4928b01ddd6c"
],
"index": "pypi",
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==0.4.0"
},
"flask-restx": { "flask-restx": {
"hashes": [ "hashes": [
"sha256:4f3d3fa7b6191fcc715b18c201a12cd875176f92ba4acc61626ccfd571ee1728", "sha256:4f3d3fa7b6191fcc715b18c201a12cd875176f92ba4acc61626ccfd571ee1728",
@ -664,12 +673,12 @@
}, },
"pydantic": { "pydantic": {
"hashes": [ "hashes": [
"sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", "sha256:12b45cfb4af17e555d3c6283d0b55271865fb0b43cc16dd0d52749dc7abf70e7",
"sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7" "sha256:a24478d2be1b91b6d3bc9597439f69ed5e87f68ebd285d86f7c7932a084b72e7"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.9'", "markers": "python_version >= '3.9'",
"version": "==2.11.5" "version": "==2.11.6"
}, },
"pydantic-core": { "pydantic-core": {
"hashes": [ "hashes": [
@ -906,6 +915,15 @@
], ],
"version": "==2025.2" "version": "==2025.2"
}, },
"redis": {
"hashes": [
"sha256:c8ddf316ee0aab65f04a11229e94a64b2618451dab7a67cb2f77eb799d872d5e",
"sha256:e821f129b75dde6cb99dd35e5c76e8c49512a5a0d8dfdc560b2fbd44b85ca977"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==6.2.0"
},
"referencing": { "referencing": {
"hashes": [ "hashes": [
"sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa",

View File

@ -1,7 +1,7 @@
from flask import Flask from flask import Flask
from flask_restx import Api from flask_restx import Api
from .config import Config from .config import Config
from .extensions import init_mongo, init_jwt from .extensions import init_mongo, init_jwt, init_redis
from .errors.handlers import register_error_handlers from .errors.handlers import register_error_handlers
from .routes.usage_routes import usage_ns from .routes.usage_routes import usage_ns
from .routes.auth_routes import auth_ns from .routes.auth_routes import auth_ns
@ -16,6 +16,7 @@ def create_app():
init_mongo(app) init_mongo(app)
init_jwt(app) init_jwt(app)
init_redis(app)
api = Api( api = Api(
app, app,

View File

@ -26,6 +26,6 @@ class Config:
BILLING_API_URL = os.getenv("BILLING_API_URL") BILLING_API_URL = os.getenv("BILLING_API_URL")
BILLING_API_TOKEN = os.getenv("BILLING_API_TOKEN") BILLING_API_TOKEN = os.getenv("BILLING_API_TOKEN")
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379")

View File

@ -1,10 +1,16 @@
from pymongo import MongoClient from pymongo import MongoClient
from flask_jwt_extended import JWTManager from flask_jwt_extended import JWTManager
from flask_redis import FlaskRedis
jwt = JWTManager() jwt = JWTManager()
redis_client = FlaskRedis()
def init_mongo(app): def init_mongo(app):
app.mongo_client = MongoClient(app.config["MONGO_URI"]) app.mongo_client = MongoClient(app.config["MONGO_URI"])
def init_redis(app):
redis_client.init_app(app)
def init_jwt(app): def init_jwt(app):
jwt.init_app(app) jwt.init_app(app)

View File

@ -1,4 +1,5 @@
import os import os
import hashlib
from bson import json_util from bson import json_util
from flask_restx import Resource from flask_restx import Resource
from flask_jwt_extended import jwt_required from flask_jwt_extended import jwt_required
@ -16,6 +17,8 @@ from app.docs.usage_models import (
transcription_data_query_params, usage_ns) transcription_data_query_params, usage_ns)
TMP_DIR = '/tmp'
@usage_ns.route('/export/trascription') @usage_ns.route('/export/trascription')
class TranscriptionExport(Resource): class TranscriptionExport(Resource):
@usage_ns.doc(security='Bearer Auth') @usage_ns.doc(security='Bearer Auth')
@ -37,32 +40,36 @@ class TranscriptionExport(Resource):
validated = TranscriptionRequest(**data) validated = TranscriptionRequest(**data)
service = TranscriptionReportService( # Cria um nome de arquivo baseado nos parâmetros (evita caracteres inválidos)
validated.company_id, hash_key = hashlib.md5(f"{validated.company_id}_{validated.start_date}_{validated.end_date}_{validated.who}".encode()).hexdigest()
validated.start_date, filename = f"transcription_{hash_key}.xlsx"
validated.end_date filepath = os.path.join(TMP_DIR, filename)
)
if validated.who == "hit": # Verifica se o arquivo já existe
report_path = service.reportDataXLSX(hit_report=True) if not os.path.exists(filepath):
else: # Gera o relatório e salva no caminho desejado
report_path = service.reportDataXLSX() service = TranscriptionReportService(
validated.company_id,
validated.start_date,
validated.end_date
)
if not os.path.exists(report_path): if validated.who == "hit":
return {"error": "File not found"}, 404 generated_path = service.reportDataXLSX(hit_report=True)
else:
generated_path = service.reportDataXLSX()
@after_this_request if not os.path.exists(generated_path):
def remove_file(response): return {"error": "File generation failed"}, 500
try:
os.remove(report_path)
except Exception as delete_error:
current_app.logger.warning(f"Error trying to delete file: {delete_error}")
return response
# Move o arquivo gerado para o TMP
os.rename(generated_path, filepath)
# Retorna o arquivo
return send_file( return send_file(
path_or_file=report_path, path_or_file=filepath,
as_attachment=True, as_attachment=True,
download_name=os.path.basename(report_path), download_name=os.path.basename(filepath),
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
) )
@ -96,12 +103,13 @@ class TranscriptionUsageData(Resource):
validated.end_date validated.end_date
) )
if validated.who == "hit": if validated.who == "hit":
result = service.reportData(page=page, page_size=page_size,hit_report=True) result = service.reportData(page=page, page_size=page_size,hit_report=True)
else: else:
result = service.reportData(page=page, page_size=page_size) result = service.reportData(page=page, page_size=page_size)
return {"success": True, "data": {"data": result["data"], "pagination": result["pagination"]}}, 200 return {"success": True, "data": {"data": result["data"], "pagination": result["pagination"], "cost":result["cost"]}}, 200

View File

@ -0,0 +1,13 @@
from app.extensions import redis_client
def cache_set(key, value, ex=0):
if ex:
redis_client.set(key, value, ex=ex)
else:
redis_client.set(key, value)
def cache_get(key):
return redis_client.get(key)
def cache_del(key):
redis_client.delete(key)

View File

@ -7,11 +7,12 @@ from openpyxl.utils.dataframe import dataframe_to_rows
from openpyxl.styles import Font, PatternFill from openpyxl.styles import Font, PatternFill
import pandas as pd import pandas as pd
import os import os
import hashlib
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from app.utils.mysql_query import execute_query from app.utils.mysql_query import execute_query
from app.utils.calc_api_usage import calculate_api_usage from app.utils.calc_api_usage import calculate_api_usage
import math from app.services.redis_service import cache_del, cache_get, cache_set
import json
class TranscriptionReportService: class TranscriptionReportService:
def __init__(self, company_id: str, start_date: str, end_date: str): def __init__(self, company_id: str, start_date: str, end_date: str):
@ -157,9 +158,6 @@ class TranscriptionReportService:
} }
def _fetch_mysql_data(self, hit_report: Optional[bool] = False)-> List[Dict[str, Any]]: def _fetch_mysql_data(self, hit_report: Optional[bool] = False)-> List[Dict[str, Any]]:
collection = self.mongo_client["billing-api"]["api_products"] collection = self.mongo_client["billing-api"]["api_products"]
@ -186,11 +184,6 @@ class TranscriptionReportService:
rows = execute_query(self.company_id, sql) rows = execute_query(self.company_id, sql)
if hit_report: if hit_report:
# collection = self.mongo_client["billing-api"]["api_pricings"]
# result_stt = collection.find({"type": "stt"})
# result_stt = [{"product": t["product"], "clientPrice": t["clientPrice"]} for t in result_stt if "clientPrice" in t]
for row in rows: for row in rows:
row["companyId"] = self.company_id row["companyId"] = self.company_id
@ -198,6 +191,7 @@ class TranscriptionReportService:
row["custo_hit"] = f"{float(rowMongo["totalCost"])}" row["custo_hit"] = f"{float(rowMongo["totalCost"])}"
row["qtd_token_input"] = rowMongo.get('usageByType', {}).get('input', 0) row["qtd_token_input"] = rowMongo.get('usageByType', {}).get('input', 0)
row["qtd_token_output"] = rowMongo.get('usageByType', {}).get('output', 0) row["qtd_token_output"] = rowMongo.get('usageByType', {}).get('output', 0)
row["total_min"] = f"{(int(row['total_billsec']) / 60):.2f}"
self.client_price_row(products, row) self.client_price_row(products, row)
self.formate_properties(row) self.formate_properties(row)
@ -209,6 +203,8 @@ class TranscriptionReportService:
self.client_price_row(products, row) self.client_price_row(products, row)
self.formate_properties(row) self.formate_properties(row)
return rows return rows
def formate_properties(self, row): def formate_properties(self, row):
@ -258,6 +254,7 @@ class TranscriptionReportService:
"src": "Origem", "src": "Origem",
"dst": "Destino", "dst": "Destino",
"total_billsec": "Quantidade de segundos", "total_billsec": "Quantidade de segundos",
"total_min": "Duração (Em minutos)",
"custo_hit": "Custo HIT", "custo_hit": "Custo HIT",
"qtd_token_input": "Quantidade de tokens(input)", "qtd_token_input": "Quantidade de tokens(input)",
"qtd_token_output": "Quantidade de tokens(output)", "qtd_token_output": "Quantidade de tokens(output)",
@ -312,6 +309,33 @@ class TranscriptionReportService:
return path return path
def _reportDataTotalCost(self):
hash_key = hashlib.md5(f"{self.company_id}_{self.start_date}_{self.end_date}".encode()).hexdigest()
sum_key = f"sum_total_cost_{hash_key}"
if data := cache_get(f'report_model_usage:total_cost:{sum_key}'):
return json.loads(data)
self._fetch_mongo_data(all_data=True)
mysql_data = self._fetch_mysql_data(hit_report=True)
total_cost_hit = sum(float(item.get('custo_hit') or 0) for item in mysql_data)
total_client_cost = sum(float(item.get('client_total_cost') or 0) for item in mysql_data)
sum_total_cost = {
'company_id': self.company_id,
'start_date': self.start_date,
'end_date': self.end_date,
'total_cost_hit': total_cost_hit,
'total_client_cost': total_client_cost
}
cache_set(f'report_model_usage:total_cost:{sum_key}', json.dumps(sum_total_cost))
return sum_total_cost
def reportDataXLSX(self, hit_report: Optional[bool] = False) -> str: def reportDataXLSX(self, hit_report: Optional[bool] = False) -> str:
self._fetch_mongo_data(all_data=True) self._fetch_mongo_data(all_data=True)
@ -333,7 +357,8 @@ class TranscriptionReportService:
return { return {
"pagination": mongo_data, "pagination": mongo_data,
"data": mysql_data "data": mysql_data,
"cost": self._reportDataTotalCost()
} }

View File

@ -48,7 +48,7 @@ export default function Dashboard() {
<header className="bg-white shadow"> <header className="bg-white shadow">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center py-6"> <div className="flex justify-between items-center py-6">
<h1 className="text-3xl font-bold text-gray-900">Dashboard - Sistema de Transcrição</h1> <h1 className="text-3xl font-bold text-gray-900">Dashboard - Custo de Produtos</h1>
<Button onClick={handleLogout} variant="outline"> <Button onClick={handleLogout} variant="outline">
<LogOut className="mr-2 h-4 w-4" /> <LogOut className="mr-2 h-4 w-4" />
Sair Sair

View File

@ -1,6 +1,6 @@
"use client" "use client"
import { useState } from "react" import { useState, useEffect } from "react"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
@ -8,12 +8,35 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Alert, AlertDescription } from "@/components/ui/alert" import { Alert, AlertDescription } from "@/components/ui/alert"
import { Download, Search, Loader2 } from "lucide-react" import { Download, Search, Loader2, DollarSign, TrendingUp, Calculator } from "lucide-react"
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000/api/v1" const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000/api/v1"
// Primeiro, vamos atualizar as interfaces para os diferentes tipos de dados function formatDateBr(dateStr:string) {
// Substitua a interface TranscriptionData existente com estas interfaces: 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,
})
)
}
interface ClientTranscriptionData { interface ClientTranscriptionData {
uniqueid: string uniqueid: string
@ -33,6 +56,7 @@ interface HitTranscriptionData {
total_billsec: number // Quantidade de segundos total_billsec: number // Quantidade de segundos
qtd_token_input: number // Quantidade de tokens(input) qtd_token_input: number // Quantidade de tokens(input)
qtd_token_output: number // Quantidade de tokens(output) qtd_token_output: number // Quantidade de tokens(output)
total_min: number,
custo_hit: string // Custo HIT custo_hit: string // Custo HIT
client_total_cost: string // Custo Cliente client_total_cost: string // Custo Cliente
client_price: string // Preço Cliente por Minuto client_price: string // Preço Cliente por Minuto
@ -42,7 +66,32 @@ interface HitTranscriptionData {
type TranscriptionData = ClientTranscriptionData | HitTranscriptionData type TranscriptionData = ClientTranscriptionData | HitTranscriptionData
// Primeiro, vamos atualizar as interfaces para refletir a nova estrutura da API: // 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
}
}
interface PaginationInfo { interface PaginationInfo {
total: number total: number
page: number page: number
@ -55,13 +104,14 @@ interface ApiResponse {
data: { data: {
data: TranscriptionData[] data: TranscriptionData[]
pagination: PaginationInfo pagination: PaginationInfo
cost?: CostInfo
} }
} }
export default function TranscriptionTable() { export default function TranscriptionTable() {
// Agora, atualize o estado para usar o tipo correto
// Substitua a linha do useState por:
const [data, setData] = useState<TranscriptionData[]>([]) const [data, setData] = useState<TranscriptionData[]>([])
const [costInfo, setCostInfo] = useState<CostInfo | null>(null)
const [exchangeRate, setExchangeRate] = useState<number | null>(null)
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [isExporting, setIsExporting] = useState(false) const [isExporting, setIsExporting] = useState(false)
const [error, setError] = useState("") const [error, setError] = useState("")
@ -70,10 +120,9 @@ export default function TranscriptionTable() {
const [companyId, setCompanyId] = useState("") const [companyId, setCompanyId] = useState("")
const [startDate, setStartDate] = useState("") const [startDate, setStartDate] = useState("")
const [endDate, setEndDate] = useState("") const [endDate, setEndDate] = useState("")
// E atualize a definição do estado who:
const [who, setWho] = useState<"client" | "hit">("client") const [who, setWho] = useState<"client" | "hit">("client")
// Adicionar estados para paginação após os estados existentes: // Estados para paginação
const [pagination, setPagination] = useState<PaginationInfo>({ const [pagination, setPagination] = useState<PaginationInfo>({
total: 0, total: 0,
page: 1, page: 1,
@ -91,7 +140,25 @@ export default function TranscriptionTable() {
} }
} }
// Atualizar a função fetchData para incluir parâmetros de paginação: // 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
}
const fetchData = async (page: number = currentPage) => { const fetchData = async (page: number = currentPage) => {
if (!companyId || !startDate || !endDate) { if (!companyId || !startDate || !endDate) {
setError("Preencha todos os campos obrigatórios") setError("Preencha todos os campos obrigatórios")
@ -102,6 +169,9 @@ export default function TranscriptionTable() {
setError("") setError("")
try { try {
// Buscar cotação do dólar em paralelo
const ratePromise = fetchExchangeRate()
const params = new URLSearchParams({ const params = new URLSearchParams({
companyId, companyId,
startDate, startDate,
@ -120,15 +190,20 @@ export default function TranscriptionTable() {
if (result.success && result.data) { if (result.success && result.data) {
setData(result.data.data || []) setData(result.data.data || [])
setPagination(result.data.pagination) setPagination(result.data.pagination)
setCostInfo(result.data.cost || null)
setCurrentPage(result.data.pagination.page) setCurrentPage(result.data.pagination.page)
} else { } else {
setData([]) setData([])
setCostInfo(null)
setPagination({ total: 0, page: 1, page_size: 20, total_pages: 0 }) setPagination({ total: 0, page: 1, page_size: 20, total_pages: 0 })
} }
} else { } else {
const errorData = await response.json() const errorData = await response.json()
setError(errorData.message || "Erro ao buscar dados") setError(errorData.message || "Erro ao buscar dados")
} }
// Aguardar a cotação do dólar
await ratePromise
} catch (err) { } catch (err) {
console.log("====> Erro de conexão com o servidor: ", err) console.log("====> Erro de conexão com o servidor: ", err)
setError("Erro de conexão com o servidor") setError("Erro de conexão com o servidor")
@ -180,7 +255,7 @@ export default function TranscriptionTable() {
} }
} }
// Adicionar funções de navegação de páginas: // Funções de navegação de páginas
const goToPage = (page: number) => { const goToPage = (page: number) => {
if (page >= 1 && page <= pagination.total_pages) { if (page >= 1 && page <= pagination.total_pages) {
setCurrentPage(page) setCurrentPage(page)
@ -193,8 +268,84 @@ export default function TranscriptionTable() {
const goToNextPage = () => goToPage(currentPage + 1) const goToNextPage = () => goToPage(currentPage + 1)
const goToLastPage = () => goToPage(pagination.total_pages) const goToLastPage = () => goToPage(pagination.total_pages)
// 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()
}, [])
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* 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">
{formatDateBr(costInfo.start_date)} até {formatDateBr(costInfo.end_date)}
</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>
)}
{/* Filtros */} {/* Filtros */}
<Card> <Card>
<CardHeader> <CardHeader>
@ -224,8 +375,6 @@ export default function TranscriptionTable() {
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="who">Tipo de Consulta *</Label> <Label htmlFor="who">Tipo de Consulta *</Label>
{/* Também vamos atualizar o Select para usar "hit" em vez de "company"
// Substitua o componente Select para o campo "who": */}
<Select value={who} onValueChange={(value: "client" | "hit") => setWho(value)}> <Select value={who} onValueChange={(value: "client" | "hit") => setWho(value)}>
<SelectTrigger> <SelectTrigger>
<SelectValue /> <SelectValue />
@ -237,12 +386,11 @@ export default function TranscriptionTable() {
</Select> </Select>
</div> </div>
{/* Adicionar seletor de tamanho de página nos filtros: */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="pageSize">Registros por página</Label> <Label htmlFor="pageSize">Registros por página</Label>
<Select <Select
value={pageSize.toString()} value={pageSize.toString()}
onValueChange={(value:string) => { onValueChange={(value: string) => {
setPageSize(Number(value)) setPageSize(Number(value))
setCurrentPage(1) setCurrentPage(1)
}} }}
@ -281,10 +429,18 @@ export default function TranscriptionTable() {
</Alert> </Alert>
)} )}
{/* 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>
)}
{/* Tabela de Dados */} {/* Tabela de Dados */}
<Card> <Card>
<CardHeader> <CardHeader>
{/* Atualizar o título da tabela para mostrar informações de paginação: */}
<CardTitle> <CardTitle>
Resultados da Consulta Resultados da Consulta
{pagination.total > 0 && ( {pagination.total > 0 && (
@ -295,8 +451,6 @@ export default function TranscriptionTable() {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{/* Agora, atualize a parte da tabela para exibir colunas diferentes com base no valor de who
// Substitua a seção da tabela com: */}
{data.length > 0 ? ( {data.length > 0 ? (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<Table> <Table>
@ -308,14 +462,15 @@ export default function TranscriptionTable() {
<TableHead>Identificador da chamada</TableHead> <TableHead>Identificador da chamada</TableHead>
<TableHead>Origem</TableHead> <TableHead>Origem</TableHead>
<TableHead>Destino</TableHead> <TableHead>Destino</TableHead>
<TableHead>Quantidade de segundos</TableHead> <TableHead>Segundos</TableHead>
<TableHead>Quantidade de tokens (input)</TableHead> <TableHead>Minutos</TableHead>
<TableHead>Quantidade de tokens (output)</TableHead> <TableHead>Custo Cliente (R$)</TableHead>
<TableHead>Custo HIT</TableHead> <TableHead>Preço Cliente p/ Minuto (R$)</TableHead>
<TableHead>Custo Cliente</TableHead> <TableHead>Custo HIT ($)</TableHead>
<TableHead>Preço Cliente por Minuto</TableHead>
<TableHead>Início</TableHead> <TableHead>Início</TableHead>
<TableHead>Fim</TableHead> <TableHead>Fim</TableHead>
<TableHead>Tokens (Input)</TableHead>
<TableHead>Tokens (Output)</TableHead>
</> </>
) : ( ) : (
<> <>
@ -324,8 +479,8 @@ export default function TranscriptionTable() {
<TableHead>Destino</TableHead> <TableHead>Destino</TableHead>
<TableHead>Início</TableHead> <TableHead>Início</TableHead>
<TableHead>Duração (s)</TableHead> <TableHead>Duração (s)</TableHead>
<TableHead>Total (min)</TableHead> <TableHead>Duração (m)</TableHead>
<TableHead>Custo Cliente</TableHead> <TableHead>Custo (R$)</TableHead>
</> </>
)} )}
</TableRow> </TableRow>
@ -340,27 +495,40 @@ export default function TranscriptionTable() {
<TableCell>{(item as HitTranscriptionData).src || "-"}</TableCell> <TableCell>{(item as HitTranscriptionData).src || "-"}</TableCell>
<TableCell>{(item as HitTranscriptionData).dst || "-"}</TableCell> <TableCell>{(item as HitTranscriptionData).dst || "-"}</TableCell>
<TableCell>{(item as HitTranscriptionData).total_billsec || "-"}</TableCell> <TableCell>{(item as HitTranscriptionData).total_billsec || "-"}</TableCell>
<TableCell>{(item as HitTranscriptionData).total_min || "-"}</TableCell>
<TableCell>
{(item as HitTranscriptionData).client_total_cost
? `R$ ${(item as HitTranscriptionData).client_total_cost.slice(0, 4)}`
: "-"}
</TableCell>
<TableCell>
{(item as HitTranscriptionData).client_price
? `R$ ${(item as HitTranscriptionData).client_price}`
: "-"}
</TableCell>
<TableCell>
{(item as HitTranscriptionData).custo_hit
? `$ ${(item as HitTranscriptionData).custo_hit.slice(0, 4)}`
: "-"}
</TableCell>
<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_input || "-"}</TableCell>
<TableCell>{(item as HitTranscriptionData).qtd_token_output || "-"}</TableCell> <TableCell>{(item as HitTranscriptionData).qtd_token_output || "-"}</TableCell>
<TableCell>
{(item as HitTranscriptionData).custo_hit ? `$${(item as HitTranscriptionData).custo_hit}` : "-"}
</TableCell>
<TableCell>{(item as HitTranscriptionData).client_total_cost || "-"}</TableCell>
<TableCell>
{(item as HitTranscriptionData).client_price ? `$${(item as HitTranscriptionData).client_price}` : "-"}
</TableCell>
<TableCell>{(item as HitTranscriptionData).start_call || "-"}</TableCell>
<TableCell>{(item as HitTranscriptionData).end_call || "-"}</TableCell>
</> </>
) : ( ) : (
<> <>
<TableCell>{(item as ClientTranscriptionData).uniqueid || "-"}</TableCell> <TableCell>{(item as ClientTranscriptionData).uniqueid || "-"}</TableCell>
<TableCell>{(item as ClientTranscriptionData).src || "-"}</TableCell> <TableCell>{(item as ClientTranscriptionData).src || "-"}</TableCell>
<TableCell>{(item as ClientTranscriptionData).dst || "-"}</TableCell> <TableCell>{(item as ClientTranscriptionData).dst || "-"}</TableCell>
<TableCell>{(item as ClientTranscriptionData).start_call || "-"}</TableCell> <TableCell>{formatDateTime((item as ClientTranscriptionData).start_call || "-")}</TableCell>
<TableCell>{(item as ClientTranscriptionData).total_billsec || "-"}</TableCell> <TableCell>{(item as ClientTranscriptionData).total_billsec || "-"}</TableCell>
<TableCell>{(item as ClientTranscriptionData).total_min || "-"}</TableCell> <TableCell>{(item as ClientTranscriptionData).total_min || "-"}</TableCell>
<TableCell>{(item as ClientTranscriptionData).client_total_cost || "-"}</TableCell> <TableCell>
{(item as ClientTranscriptionData).client_total_cost
? `R$ ${(item as ClientTranscriptionData).client_total_cost.slice(0, 4)}`
: "-"}
</TableCell>
</> </>
)} )}
</TableRow> </TableRow>