added card total cost
parent
306ba95bc2
commit
b0fa541fbc
|
@ -20,6 +20,8 @@ flask-bcrypt = "*"
|
|||
flask-cors = "*"
|
||||
mypy = "*"
|
||||
requests = "*"
|
||||
redis = "*"
|
||||
flask-redis = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "3798a8dec1f82bd3b8d8e6089e8110ae5e152e1e76d9b78810ba5b4cfab33c2f"
|
||||
"sha256": "424e88c981f7de9a38bc3d2c0189df4f377bb72daa06d4b7a0b742a51b7ad51e"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
|
@ -276,6 +276,15 @@
|
|||
"markers": "python_version >= '3.9' and python_version < '4'",
|
||||
"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": {
|
||||
"hashes": [
|
||||
"sha256:4f3d3fa7b6191fcc715b18c201a12cd875176f92ba4acc61626ccfd571ee1728",
|
||||
|
@ -664,12 +673,12 @@
|
|||
},
|
||||
"pydantic": {
|
||||
"hashes": [
|
||||
"sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a",
|
||||
"sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7"
|
||||
"sha256:12b45cfb4af17e555d3c6283d0b55271865fb0b43cc16dd0d52749dc7abf70e7",
|
||||
"sha256:a24478d2be1b91b6d3bc9597439f69ed5e87f68ebd285d86f7c7932a084b72e7"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==2.11.5"
|
||||
"version": "==2.11.6"
|
||||
},
|
||||
"pydantic-core": {
|
||||
"hashes": [
|
||||
|
@ -906,6 +915,15 @@
|
|||
],
|
||||
"version": "==2025.2"
|
||||
},
|
||||
"redis": {
|
||||
"hashes": [
|
||||
"sha256:c8ddf316ee0aab65f04a11229e94a64b2618451dab7a67cb2f77eb799d872d5e",
|
||||
"sha256:e821f129b75dde6cb99dd35e5c76e8c49512a5a0d8dfdc560b2fbd44b85ca977"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==6.2.0"
|
||||
},
|
||||
"referencing": {
|
||||
"hashes": [
|
||||
"sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from flask import Flask
|
||||
from flask_restx import Api
|
||||
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 .routes.usage_routes import usage_ns
|
||||
from .routes.auth_routes import auth_ns
|
||||
|
@ -16,6 +16,7 @@ def create_app():
|
|||
|
||||
init_mongo(app)
|
||||
init_jwt(app)
|
||||
init_redis(app)
|
||||
|
||||
api = Api(
|
||||
app,
|
||||
|
|
|
@ -26,6 +26,6 @@ class Config:
|
|||
BILLING_API_URL = os.getenv("BILLING_API_URL")
|
||||
BILLING_API_TOKEN = os.getenv("BILLING_API_TOKEN")
|
||||
|
||||
|
||||
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379")
|
||||
|
||||
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
from pymongo import MongoClient
|
||||
from flask_jwt_extended import JWTManager
|
||||
from flask_redis import FlaskRedis
|
||||
|
||||
jwt = JWTManager()
|
||||
|
||||
redis_client = FlaskRedis()
|
||||
|
||||
def init_mongo(app):
|
||||
app.mongo_client = MongoClient(app.config["MONGO_URI"])
|
||||
|
||||
def init_redis(app):
|
||||
redis_client.init_app(app)
|
||||
|
||||
def init_jwt(app):
|
||||
jwt.init_app(app)
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
import os
|
||||
import hashlib
|
||||
from bson import json_util
|
||||
from flask_restx import Resource
|
||||
from flask_jwt_extended import jwt_required
|
||||
|
@ -16,18 +17,20 @@ from app.docs.usage_models import (
|
|||
transcription_data_query_params, usage_ns)
|
||||
|
||||
|
||||
TMP_DIR = '/tmp'
|
||||
|
||||
@usage_ns.route('/export/trascription')
|
||||
class TranscriptionExport(Resource):
|
||||
@usage_ns.doc(security='Bearer Auth')
|
||||
@usage_ns.doc(params=transcription_data_query_params)
|
||||
@usage_ns.response(200, 'success')
|
||||
@usage_ns.response(400, 'Validation error')
|
||||
@usage_ns.response(400, 'Validation error')
|
||||
@usage_ns.response(404, 'File not found')
|
||||
@jwt_required()
|
||||
def get(self):
|
||||
"""
|
||||
Export transcription report in XLSX.
|
||||
"""
|
||||
"""
|
||||
data = {
|
||||
"company_id": request.args.get("companyId", type=str),
|
||||
"start_date": request.args.get("startDate"),
|
||||
|
@ -37,35 +40,39 @@ class TranscriptionExport(Resource):
|
|||
|
||||
validated = TranscriptionRequest(**data)
|
||||
|
||||
service = TranscriptionReportService(
|
||||
validated.company_id,
|
||||
validated.start_date,
|
||||
validated.end_date
|
||||
# Cria um nome de arquivo baseado nos parâmetros (evita caracteres inválidos)
|
||||
hash_key = hashlib.md5(f"{validated.company_id}_{validated.start_date}_{validated.end_date}_{validated.who}".encode()).hexdigest()
|
||||
filename = f"transcription_{hash_key}.xlsx"
|
||||
filepath = os.path.join(TMP_DIR, filename)
|
||||
|
||||
# Verifica se o arquivo já existe
|
||||
if not os.path.exists(filepath):
|
||||
# Gera o relatório e salva no caminho desejado
|
||||
service = TranscriptionReportService(
|
||||
validated.company_id,
|
||||
validated.start_date,
|
||||
validated.end_date
|
||||
)
|
||||
|
||||
if validated.who == "hit":
|
||||
generated_path = service.reportDataXLSX(hit_report=True)
|
||||
else:
|
||||
generated_path = service.reportDataXLSX()
|
||||
|
||||
if not os.path.exists(generated_path):
|
||||
return {"error": "File generation failed"}, 500
|
||||
|
||||
# Move o arquivo gerado para o TMP
|
||||
os.rename(generated_path, filepath)
|
||||
|
||||
# Retorna o arquivo
|
||||
return send_file(
|
||||
path_or_file=filepath,
|
||||
as_attachment=True,
|
||||
download_name=os.path.basename(filepath),
|
||||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
)
|
||||
|
||||
if validated.who == "hit":
|
||||
report_path = service.reportDataXLSX(hit_report=True)
|
||||
else:
|
||||
report_path = service.reportDataXLSX()
|
||||
|
||||
if not os.path.exists(report_path):
|
||||
return {"error": "File not found"}, 404
|
||||
|
||||
@after_this_request
|
||||
def remove_file(response):
|
||||
try:
|
||||
os.remove(report_path)
|
||||
except Exception as delete_error:
|
||||
current_app.logger.warning(f"Error trying to delete file: {delete_error}")
|
||||
return response
|
||||
|
||||
return send_file(
|
||||
path_or_file=report_path,
|
||||
as_attachment=True,
|
||||
download_name=os.path.basename(report_path),
|
||||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
)
|
||||
|
||||
|
||||
@usage_ns.route('/data/trascription')
|
||||
class TranscriptionUsageData(Resource):
|
||||
|
@ -95,13 +102,14 @@ class TranscriptionUsageData(Resource):
|
|||
validated.start_date,
|
||||
validated.end_date
|
||||
)
|
||||
|
||||
|
||||
if validated.who == "hit":
|
||||
result = service.reportData(page=page, page_size=page_size,hit_report=True)
|
||||
else:
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -7,11 +7,12 @@ from openpyxl.utils.dataframe import dataframe_to_rows
|
|||
from openpyxl.styles import Font, PatternFill
|
||||
import pandas as pd
|
||||
import os
|
||||
import hashlib
|
||||
from typing import List, Dict, Any, Optional
|
||||
from app.utils.mysql_query import execute_query
|
||||
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:
|
||||
def __init__(self, company_id: str, start_date: str, end_date: str):
|
||||
|
@ -155,10 +156,7 @@ class TranscriptionReportService:
|
|||
"page_size": page_size,
|
||||
"total_pages": (total + page_size - 1) // page_size
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def _fetch_mysql_data(self, hit_report: Optional[bool] = False)-> List[Dict[str, Any]]:
|
||||
|
||||
|
@ -185,12 +183,7 @@ class TranscriptionReportService:
|
|||
uniqueid, src, dst;"""
|
||||
rows = execute_query(self.company_id, sql)
|
||||
|
||||
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]
|
||||
|
||||
if hit_report:
|
||||
for row in rows:
|
||||
row["companyId"] = self.company_id
|
||||
|
||||
|
@ -198,6 +191,7 @@ class TranscriptionReportService:
|
|||
row["custo_hit"] = f"{float(rowMongo["totalCost"])}"
|
||||
row["qtd_token_input"] = rowMongo.get('usageByType', {}).get('input', 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.formate_properties(row)
|
||||
|
@ -209,6 +203,8 @@ class TranscriptionReportService:
|
|||
self.client_price_row(products, row)
|
||||
self.formate_properties(row)
|
||||
|
||||
|
||||
|
||||
return rows
|
||||
|
||||
def formate_properties(self, row):
|
||||
|
@ -258,6 +254,7 @@ class TranscriptionReportService:
|
|||
"src": "Origem",
|
||||
"dst": "Destino",
|
||||
"total_billsec": "Quantidade de segundos",
|
||||
"total_min": "Duração (Em minutos)",
|
||||
"custo_hit": "Custo HIT",
|
||||
"qtd_token_input": "Quantidade de tokens(input)",
|
||||
"qtd_token_output": "Quantidade de tokens(output)",
|
||||
|
@ -311,7 +308,34 @@ class TranscriptionReportService:
|
|||
wb.save(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:
|
||||
self._fetch_mongo_data(all_data=True)
|
||||
|
||||
|
@ -333,7 +357,8 @@ class TranscriptionReportService:
|
|||
|
||||
return {
|
||||
"pagination": mongo_data,
|
||||
"data": mysql_data
|
||||
"data": mysql_data,
|
||||
"cost": self._reportDataTotalCost()
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ export default function Dashboard() {
|
|||
<header className="bg-white shadow">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<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">
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
Sair
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
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"
|
||||
|
||||
// Primeiro, vamos atualizar as interfaces para os diferentes tipos de dados
|
||||
// Substitua a interface TranscriptionData existente com estas interfaces:
|
||||
function formatDateBr(dateStr:string) {
|
||||
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 {
|
||||
uniqueid: string
|
||||
|
@ -33,7 +56,8 @@ interface HitTranscriptionData {
|
|||
total_billsec: number // Quantidade de segundos
|
||||
qtd_token_input: number // Quantidade de tokens(input)
|
||||
qtd_token_output: number // Quantidade de tokens(output)
|
||||
custo_hit: string // Custo HIT
|
||||
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
|
||||
|
@ -42,7 +66,32 @@ interface 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 {
|
||||
total: number
|
||||
page: number
|
||||
|
@ -55,13 +104,14 @@ interface ApiResponse {
|
|||
data: {
|
||||
data: TranscriptionData[]
|
||||
pagination: PaginationInfo
|
||||
cost?: CostInfo
|
||||
}
|
||||
}
|
||||
|
||||
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 [costInfo, setCostInfo] = useState<CostInfo | null>(null)
|
||||
const [exchangeRate, setExchangeRate] = useState<number | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isExporting, setIsExporting] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
|
@ -70,10 +120,9 @@ export default function TranscriptionTable() {
|
|||
const [companyId, setCompanyId] = useState("")
|
||||
const [startDate, setStartDate] = useState("")
|
||||
const [endDate, setEndDate] = useState("")
|
||||
// E atualize a definição do estado who:
|
||||
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>({
|
||||
total: 0,
|
||||
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) => {
|
||||
if (!companyId || !startDate || !endDate) {
|
||||
setError("Preencha todos os campos obrigatórios")
|
||||
|
@ -102,6 +169,9 @@ export default function TranscriptionTable() {
|
|||
setError("")
|
||||
|
||||
try {
|
||||
// Buscar cotação do dólar em paralelo
|
||||
const ratePromise = fetchExchangeRate()
|
||||
|
||||
const params = new URLSearchParams({
|
||||
companyId,
|
||||
startDate,
|
||||
|
@ -120,15 +190,20 @@ export default function TranscriptionTable() {
|
|||
if (result.success && result.data) {
|
||||
setData(result.data.data || [])
|
||||
setPagination(result.data.pagination)
|
||||
setCostInfo(result.data.cost || null)
|
||||
setCurrentPage(result.data.pagination.page)
|
||||
} else {
|
||||
setData([])
|
||||
setCostInfo(null)
|
||||
setPagination({ total: 0, page: 1, page_size: 20, total_pages: 0 })
|
||||
}
|
||||
} else {
|
||||
const errorData = await response.json()
|
||||
setError(errorData.message || "Erro ao buscar dados")
|
||||
}
|
||||
|
||||
// Aguardar a cotação do dólar
|
||||
await ratePromise
|
||||
} catch (err) {
|
||||
console.log("====> Erro de conexão com o servidor: ", err)
|
||||
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) => {
|
||||
if (page >= 1 && page <= pagination.total_pages) {
|
||||
setCurrentPage(page)
|
||||
|
@ -193,8 +268,84 @@ export default function TranscriptionTable() {
|
|||
const goToNextPage = () => goToPage(currentPage + 1)
|
||||
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 (
|
||||
<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 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
@ -224,8 +375,6 @@ export default function TranscriptionTable() {
|
|||
|
||||
<div className="space-y-2">
|
||||
<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)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
|
@ -237,12 +386,11 @@ export default function TranscriptionTable() {
|
|||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Adicionar seletor de tamanho de página nos filtros: */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="pageSize">Registros por página</Label>
|
||||
<Select
|
||||
value={pageSize.toString()}
|
||||
onValueChange={(value:string) => {
|
||||
onValueChange={(value: string) => {
|
||||
setPageSize(Number(value))
|
||||
setCurrentPage(1)
|
||||
}}
|
||||
|
@ -281,10 +429,18 @@ export default function TranscriptionTable() {
|
|||
</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 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
{/* Atualizar o título da tabela para mostrar informações de paginação: */}
|
||||
<CardTitle>
|
||||
Resultados da Consulta
|
||||
{pagination.total > 0 && (
|
||||
|
@ -295,8 +451,6 @@ export default function TranscriptionTable() {
|
|||
</CardTitle>
|
||||
</CardHeader>
|
||||
<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 ? (
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
|
@ -308,14 +462,15 @@ export default function TranscriptionTable() {
|
|||
<TableHead>Identificador da chamada</TableHead>
|
||||
<TableHead>Origem</TableHead>
|
||||
<TableHead>Destino</TableHead>
|
||||
<TableHead>Quantidade de segundos</TableHead>
|
||||
<TableHead>Quantidade de tokens (input)</TableHead>
|
||||
<TableHead>Quantidade de tokens (output)</TableHead>
|
||||
<TableHead>Custo HIT</TableHead>
|
||||
<TableHead>Custo Cliente</TableHead>
|
||||
<TableHead>Preço Cliente por Minuto</TableHead>
|
||||
<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>
|
||||
<TableHead>Tokens (Input)</TableHead>
|
||||
<TableHead>Tokens (Output)</TableHead>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
@ -324,8 +479,8 @@ export default function TranscriptionTable() {
|
|||
<TableHead>Destino</TableHead>
|
||||
<TableHead>Início</TableHead>
|
||||
<TableHead>Duração (s)</TableHead>
|
||||
<TableHead>Total (min)</TableHead>
|
||||
<TableHead>Custo Cliente</TableHead>
|
||||
<TableHead>Duração (m)</TableHead>
|
||||
<TableHead>Custo (R$)</TableHead>
|
||||
</>
|
||||
)}
|
||||
</TableRow>
|
||||
|
@ -340,27 +495,40 @@ export default function TranscriptionTable() {
|
|||
<TableCell>{(item as HitTranscriptionData).src || "-"}</TableCell>
|
||||
<TableCell>{(item as HitTranscriptionData).dst || "-"}</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_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).src || "-"}</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_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>
|
||||
|
|
Loading…
Reference in New Issue