transcription-cost-usage-re.../frontend/components/product-management.tsx

545 lines
20 KiB
TypeScript
Raw Normal View History

"use client"
import { useState, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
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 { Badge } from "@/components/ui/badge"
import { Textarea } from "@/components/ui/textarea"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Edit2, Plus, Save, X, Loader2, RefreshCw, History } from "lucide-react"
// Usar as variáveis de ambiente para as URLs
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000/api/v1"
interface PriceHistory {
startDate: string
endDate: string | null
price: number
}
interface Product {
_id: string
name: string
description: string
priceHistory: PriceHistory[]
createdAt: string
updatedAt: string
__v: number
}
interface ProductsResponse {
msg: string
products: Product[]
}
interface CreateProductRequest {
name: string
description: string
price: number
}
interface UpdateProductRequest {
name?: string
description?: string
price?: number
}
export default function ProductManagement() {
const [products, setProducts] = useState<Product[]>([])
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState("")
const [success, setSuccess] = useState("")
// Estados para criação de produto
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
const [isCreating, setIsCreating] = useState(false)
const [createForm, setCreateForm] = useState<CreateProductRequest>({
name: "",
description: "",
price: 0,
})
// Estados para edição de produto
const [editingId, setEditingId] = useState<string | null>(null)
const [editForm, setEditForm] = useState<UpdateProductRequest>({
name: "",
description: "",
price: 0,
})
const [isUpdating, setIsUpdating] = useState(false)
// Estados para histórico de preços
const [historyDialogOpen, setHistoryDialogOpen] = useState(false)
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null)
const getAuthHeaders = () => {
const token = localStorage.getItem("access_token")
return {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
}
}
const fetchProducts = async () => {
setIsLoading(true)
setError("")
try {
const response = await fetch(`${API_BASE_URL}/billing/products`, {
method: "GET",
headers: getAuthHeaders(),
})
if (response.ok) {
const result: ProductsResponse = await response.json()
setProducts(result.products || [])
} else {
const errorData = await response.json()
// Para erro 400, exibir a mensagem específica da API
if (response.status === 400 && errorData.msg) {
setError(errorData.msg)
} else {
setError(errorData.message || errorData.msg || "Erro ao buscar produtos")
}
}
} catch (err) {
console.log(err)
setError("Erro de conexão com o servidor")
} finally {
setIsLoading(false)
}
}
const createProduct = async () => {
if (!createForm.name || !createForm.description || createForm.price <= 0) {
setError("Preencha todos os campos corretamente")
return
}
setIsCreating(true)
setError("")
setSuccess("")
try {
const response = await fetch(`${API_BASE_URL}/billing/product`, {
method: "POST",
headers: getAuthHeaders(),
body: JSON.stringify(createForm),
})
if (response.ok) {
setSuccess("Produto criado com sucesso!")
setCreateForm({ name: "", description: "", price: 0 })
setIsCreateDialogOpen(false)
await fetchProducts()
} else {
const errorData = await response.json()
// Para erro 400, exibir a mensagem específica da API
if (response.status === 400 && errorData.msg) {
setError(errorData.msg)
} else {
setError(errorData.message || errorData.msg || "Erro ao criar produto")
}
}
} catch (err) {
console.log(err)
setError("Erro de conexão com o servidor")
} finally {
setIsCreating(false)
}
}
const updateProduct = async (productId: string) => {
// Verificar se há pelo menos um campo para atualizar
if (Object.keys(editForm).length === 0) {
setError("Nenhuma alteração para salvar")
return
}
// Se o preço estiver definido, verificar se é válido
if (editForm.price !== undefined && (isNaN(editForm.price) || editForm.price <= 0)) {
setError("Digite um preço válido")
return
}
setIsUpdating(true)
setError("")
setSuccess("")
try {
const response = await fetch(`${API_BASE_URL}/billing/product/${productId}`, {
method: "PATCH",
headers: getAuthHeaders(),
body: JSON.stringify(editForm),
})
if (response.ok) {
setSuccess("Produto atualizado com sucesso!")
setEditingId(null)
setEditForm({})
await fetchProducts()
} else {
const errorData = await response.json()
// Para erro 400, exibir a mensagem específica da API
if (response.status === 400 && errorData.msg) {
setError(errorData.msg)
} else {
setError(errorData.message || errorData.msg || "Erro ao atualizar produto")
}
}
} catch (err) {
console.log(err)
setError("Erro de conexão com o servidor")
} finally {
setIsUpdating(false)
}
}
const startEdit = (product: Product) => {
setEditingId(product._id)
const currentPrice = getCurrentPrice(product)
setEditForm({
name: product.name,
description: product.description,
price: currentPrice,
})
}
const cancelEdit = () => {
setEditingId(null)
setEditForm({})
}
const getCurrentPrice = (product: Product): number => {
return product.priceHistory.find((p) => p.endDate === null)?.price || 0
}
const formatDate = (dateString: string): string => {
return new Date(dateString).toLocaleString("pt-BR")
}
const formatDateCompact = (dateString: string): string => {
const date = new Date(dateString)
return `${date.getDate().toString().padStart(2, "0")}/${(date.getMonth() + 1).toString().padStart(2, "0")}/${date.getFullYear()}, ${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}:${date.getSeconds().toString().padStart(2, "0")}`
}
const showHistory = (product: Product) => {
setSelectedProduct(product)
setHistoryDialogOpen(true)
}
useEffect(() => {
fetchProducts()
}, [])
return (
<div className="space-y-6">
{/* Cabeçalho com botão de criar */}
<div className="flex justify-between items-center">
<div>
<h3 className="text-lg font-medium">Produtos Cadastrados</h3>
<p className="text-sm text-gray-500">Gerencie produtos e seus preços</p>
</div>
<div className="flex gap-2">
<Button onClick={fetchProducts} disabled={isLoading} variant="outline">
{isLoading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <RefreshCw className="mr-2 h-4 w-4" />}
Atualizar
</Button>
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" />
Novo Produto
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Criar Novo Produto</DialogTitle>
<DialogDescription>Preencha as informações do novo produto</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Nome do Produto *</Label>
<Input
id="name"
value={createForm.name}
onChange={(e) => setCreateForm((prev) => ({ ...prev, name: e.target.value }))}
placeholder="Ex: Produto HIT"
/>
</div>
<div className="space-y-2">
<Label htmlFor="description">Descrição *</Label>
<Textarea
id="description"
value={createForm.description}
onChange={(e) => setCreateForm((prev) => ({ ...prev, description: e.target.value }))}
placeholder="Descreva o produto..."
rows={3}
/>
</div>
<div className="space-y-2">
<Label htmlFor="price">Preço Inicial *</Label>
<Input
id="price"
type="number"
step="0.001"
value={createForm.price}
onChange={(e) =>
setCreateForm((prev) => ({ ...prev, price: Number.parseFloat(e.target.value) || 0 }))
}
placeholder="0.060"
/>
</div>
<div className="flex gap-2 pt-4">
<Button onClick={createProduct} disabled={isCreating} className="flex-1">
{isCreating ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Save className="mr-2 h-4 w-4" />}
Criar Produto
</Button>
<Button variant="outline" onClick={() => setIsCreateDialogOpen(false)} disabled={isCreating}>
Cancelar
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
</div>
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{success && (
<Alert>
<AlertDescription>{success}</AlertDescription>
</Alert>
)}
{/* Tabela de Produtos */}
<Card>
<CardHeader>
<CardTitle>Lista de Produtos ({products.length} produtos)</CardTitle>
</CardHeader>
<CardContent>
{products.length > 0 ? (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Nome</TableHead>
<TableHead>Descrição</TableHead>
<TableHead>Preço Atual</TableHead>
<TableHead>Criado em</TableHead>
<TableHead>Atualizado em</TableHead>
<TableHead>Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{products.map((product) => (
<TableRow key={product._id}>
<TableCell>
{editingId === product._id ? (
<Input
value={editForm.name}
onChange={(e) => setEditForm((prev) => ({ ...prev, name: e.target.value }))}
/>
) : (
<div className="font-medium">{product.name}</div>
)}
</TableCell>
<TableCell>
{editingId === product._id ? (
<Textarea
value={editForm.description}
onChange={(e) => setEditForm((prev) => ({ ...prev, description: e.target.value }))}
rows={2}
/>
) : (
<div className="max-w-xs truncate" title={product.description}>
{product.description}
</div>
)}
</TableCell>
<TableCell>
{editingId === product._id ? (
<Input
type="number"
step="0.001"
value={editForm.price}
onChange={(e) =>
setEditForm((prev) => ({ ...prev, price: Number.parseFloat(e.target.value) || 0 }))
}
className="w-24"
/>
) : (
<div className="flex items-center gap-2">
<Badge
variant="secondary"
className="cursor-pointer hover:bg-gray-200 transition-colors"
onClick={() => showHistory(product)}
title="Clique para ver o histórico de preços"
>
${getCurrentPrice(product)}
</Badge>
{product.priceHistory && product.priceHistory.length > 1 && (
<Badge variant="outline" className="text-xs">
{product.priceHistory.length} alterações
</Badge>
)}
</div>
)}
</TableCell>
<TableCell>{formatDate(product.createdAt)}</TableCell>
<TableCell>{formatDate(product.updatedAt)}</TableCell>
<TableCell>
<div className="flex gap-2">
{editingId === product._id ? (
<>
<Button size="sm" onClick={() => updateProduct(product._id)} disabled={isUpdating}>
{isUpdating ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Save className="h-4 w-4" />
)}
</Button>
<Button size="sm" variant="outline" onClick={cancelEdit} disabled={isUpdating}>
<X className="h-4 w-4" />
</Button>
</>
) : (
<>
<Button size="sm" variant="outline" onClick={() => startEdit(product)}>
<Edit2 className="h-4 w-4" />
</Button>
<Button size="sm" variant="outline" onClick={() => showHistory(product)}>
<History className="h-4 w-4" />
</Button>
</>
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) : (
<div className="text-center py-8 text-gray-500">
{isLoading ? "Carregando..." : "Nenhum produto encontrado. Crie seu primeiro produto!"}
</div>
)}
</CardContent>
</Card>
{/* Dialog de Histórico de Preços - Ajustado para caber na tela */}
<Dialog open={historyDialogOpen} onOpenChange={setHistoryDialogOpen}>
<DialogContent className="w-[95%] max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Histórico de Preços - {selectedProduct?.name}</DialogTitle>
<DialogDescription>
Visualize todas as alterações de preço deste produto ({selectedProduct?.priceHistory?.length || 0}{" "}
registros)
</DialogDescription>
</DialogHeader>
{selectedProduct && selectedProduct.priceHistory && selectedProduct.priceHistory.length > 0 ? (
<div className="space-y-4">
<div className="overflow-x-auto">
<Table className="w-full">
<TableHeader>
<TableRow>
<TableHead>Data Início</TableHead>
<TableHead>Data Fim</TableHead>
<TableHead>Preço</TableHead>
<TableHead>Status</TableHead>
<TableHead>Duração</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{selectedProduct.priceHistory
.sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime())
.map((history, index) => {
const startDate = new Date(history.startDate)
const endDate = history.endDate ? new Date(history.endDate) : null
const duration = endDate
? Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24))
: Math.ceil((new Date().getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24))
return (
<TableRow key={index} className={history.endDate === null ? "bg-blue-50" : ""}>
<TableCell className="text-sm">{formatDateCompact(history.startDate)}</TableCell>
<TableCell className="text-sm">
{history.endDate ? formatDateCompact(history.endDate) : "Em vigor"}
</TableCell>
<TableCell>
<Badge variant={history.endDate === null ? "default" : "secondary"}>
${history.price}
</Badge>
</TableCell>
<TableCell>
<Badge variant={history.endDate === null ? "default" : "outline"}>
{history.endDate === null ? "Atual" : "Histórico"}
</Badge>
</TableCell>
<TableCell className="text-sm text-gray-600">
{duration} {duration === 1 ? "dia" : "dias"}
{history.endDate === null && " (em vigor)"}
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</div>
{/* Resumo do histórico - Simplificado para economizar espaço */}
<div className="mt-4 p-3 bg-gray-50 rounded-lg">
<h4 className="font-medium text-sm text-gray-700 mb-2">Resumo do Histórico</h4>
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<span className="text-gray-500">Total de alterações:</span>
<div className="font-medium">{selectedProduct.priceHistory.length}</div>
</div>
<div>
<span className="text-gray-500">Preço inicial:</span>
<div className="font-medium">
$
{selectedProduct.priceHistory
.sort((a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime())[0]
?.price.toFixed(3)}
</div>
</div>
<div>
<span className="text-gray-500">Preço atual:</span>
<div className="font-medium">${getCurrentPrice(selectedProduct)}</div>
</div>
<div>
<span className="text-gray-500">Produto criado em:</span>
<div className="font-medium">{formatDateCompact(selectedProduct.createdAt)}</div>
</div>
</div>
</div>
</div>
) : (
<div className="text-center py-8 text-gray-500">
Nenhum histórico de preços encontrado para este produto.
</div>
)}
</DialogContent>
</Dialog>
</div>
)
}