feat: add admin reviews, logistics, profile pages and update seeder

This commit is contained in:
Tiago Yamamoto 2025-12-22 09:08:42 -03:00
parent 4f6c96daf0
commit 4ccfa629cc
15 changed files with 700 additions and 11 deletions

View file

@ -251,6 +251,34 @@ type Shipment struct {
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
// ReviewFilter captures review listing constraints.
type ReviewFilter struct {
Limit int
Offset int
}
// ReviewPage wraps paginated review results.
type ReviewPage struct {
Reviews []Review `json:"reviews"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// ShipmentFilter captures shipment listing constraints.
type ShipmentFilter struct {
Limit int
Offset int
}
// ShipmentPage wraps paginated shipment results.
type ShipmentPage struct {
Shipments []Shipment `json:"shipments"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// OrderStatus enumerates supported transitions.
type OrderStatus string

View file

@ -0,0 +1,53 @@
package handler
import (
"net/http"
"github.com/saveinmed/backend-go/internal/domain"
)
// ListAllReviews godoc
// @Summary Lista todas as avaliações (Admin)
// @Tags Admin
// @Security BearerAuth
// @Produce json
// @Param page query int false "Página"
// @Param page_size query int false "Tamanho da página"
// @Success 200 {object} domain.ReviewPage
// @Failure 401 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/reviews [get]
func (h *Handler) ListAllReviews(w http.ResponseWriter, r *http.Request) {
page, pageSize := parsePagination(r)
result, err := h.svc.ListReviews(r.Context(), domain.ReviewFilter{}, page, pageSize)
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
writeJSON(w, http.StatusOK, result)
}
// ListAllShipments godoc
// @Summary Lista todos os envios (Admin)
// @Tags Admin
// @Security BearerAuth
// @Produce json
// @Param page query int false "Página"
// @Param page_size query int false "Tamanho da página"
// @Success 200 {object} domain.ShipmentPage
// @Failure 401 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/admin/shipments [get]
func (h *Handler) ListAllShipments(w http.ResponseWriter, r *http.Request) {
page, pageSize := parsePagination(r)
result, err := h.svc.ListShipments(r.Context(), domain.ShipmentFilter{}, page, pageSize)
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
writeJSON(w, http.StatusOK, result)
}

View file

@ -1090,3 +1090,53 @@ SET active = EXCLUDED.active,
}
return nil
}
func (r *Repository) ListReviews(ctx context.Context, filter domain.ReviewFilter) ([]domain.Review, int64, error) {
baseQuery := `FROM reviews`
var args []any
// Add where clauses if needed in future
var total int64
if err := r.db.GetContext(ctx, &total, "SELECT count(*) "+baseQuery, args...); err != nil {
return nil, 0, err
}
if filter.Limit <= 0 {
filter.Limit = 20
}
args = append(args, filter.Limit, filter.Offset)
listQuery := fmt.Sprintf("SELECT id, order_id, buyer_id, seller_id, rating, comment, created_at %s ORDER BY created_at DESC LIMIT $%d OFFSET $%d", baseQuery, len(args)-1, len(args))
var reviews []domain.Review
if err := r.db.SelectContext(ctx, &reviews, listQuery, args...); err != nil {
return nil, 0, err
}
return reviews, total, nil
}
func (r *Repository) ListShipments(ctx context.Context, filter domain.ShipmentFilter) ([]domain.Shipment, int64, error) {
baseQuery := `FROM shipments`
var args []any
// Add where clauses if needed in future
var total int64
if err := r.db.GetContext(ctx, &total, "SELECT count(*) "+baseQuery, args...); err != nil {
return nil, 0, err
}
if filter.Limit <= 0 {
filter.Limit = 20
}
args = append(args, filter.Limit, filter.Offset)
listQuery := fmt.Sprintf("SELECT id, order_id, carrier, tracking_code, external_tracking, status, created_at, updated_at %s ORDER BY created_at DESC LIMIT $%d OFFSET $%d", baseQuery, len(args)-1, len(args))
var shipments []domain.Shipment
if err := r.db.SelectContext(ctx, &shipments, listQuery, args...); err != nil {
return nil, 0, err
}
return shipments, total, nil
}

View file

@ -98,6 +98,9 @@ func New(cfg config.Config) (*Server, error) {
mux.Handle("GET /api/v1/dashboard/seller", chain(http.HandlerFunc(h.GetSellerDashboard), middleware.Logger, middleware.Gzip, auth))
mux.Handle("GET /api/v1/dashboard/admin", chain(http.HandlerFunc(h.GetAdminDashboard), middleware.Logger, middleware.Gzip, adminOnly))
mux.Handle("GET /api/v1/admin/reviews", chain(http.HandlerFunc(h.ListAllReviews), middleware.Logger, middleware.Gzip, adminOnly))
mux.Handle("GET /api/v1/admin/shipments", chain(http.HandlerFunc(h.ListAllShipments), middleware.Logger, middleware.Gzip, adminOnly))
mux.Handle("POST /api/v1/auth/register", chain(http.HandlerFunc(h.Register), middleware.Logger, middleware.Gzip))
mux.Handle("POST /api/v1/auth/register/customer", chain(http.HandlerFunc(h.RegisterCustomer), middleware.Logger, middleware.Gzip))
mux.Handle("POST /api/v1/auth/register/tenant", chain(http.HandlerFunc(h.RegisterTenant), middleware.Logger, middleware.Gzip))

View file

@ -59,7 +59,10 @@ type Repository interface {
SellerDashboard(ctx context.Context, sellerID uuid.UUID) (*domain.SellerDashboard, error)
AdminDashboard(ctx context.Context, since time.Time) (*domain.AdminDashboard, error)
GetShippingMethodsByVendor(ctx context.Context, vendorID uuid.UUID) ([]domain.ShippingMethod, error)
UpsertShippingMethods(ctx context.Context, methods []domain.ShippingMethod) error
ListReviews(ctx context.Context, filter domain.ReviewFilter) ([]domain.Review, int64, error)
ListShipments(ctx context.Context, filter domain.ShipmentFilter) ([]domain.Shipment, int64, error)
}
// PaymentGateway abstracts Mercado Pago integration.
@ -807,3 +810,35 @@ func (s *Service) VerifyCompany(ctx context.Context, id uuid.UUID) (*domain.Comp
}
return company, nil
}
func (s *Service) ListReviews(ctx context.Context, filter domain.ReviewFilter, page, pageSize int) (*domain.ReviewPage, error) {
if pageSize <= 0 {
pageSize = 20
}
if page <= 0 {
page = 1
}
filter.Limit = pageSize
filter.Offset = (page - 1) * pageSize
reviews, total, err := s.repo.ListReviews(ctx, filter)
if err != nil {
return nil, err
}
return &domain.ReviewPage{Reviews: reviews, Total: total, Page: page, PageSize: pageSize}, nil
}
func (s *Service) ListShipments(ctx context.Context, filter domain.ShipmentFilter, page, pageSize int) (*domain.ShipmentPage, error) {
if pageSize <= 0 {
pageSize = 20
}
if page <= 0 {
page = 1
}
filter.Limit = pageSize
filter.Offset = (page - 1) * pageSize
shipments, total, err := s.repo.ListShipments(ctx, filter)
if err != nil {
return nil, err
}
return &domain.ShipmentPage{Shipments: shipments, Total: total, Page: page, PageSize: pageSize}, nil
}

View file

@ -2,7 +2,6 @@ import { Navigate, Route, Routes } from 'react-router-dom'
import { LoginPage } from './pages/Login'
import { CartPage } from './pages/Cart'
import { CheckoutPage } from './pages/Checkout'
import { ProfilePage } from './pages/Profile'
import { OrdersPage as UserOrdersPage } from './pages/Orders'
import { InventoryPage } from './pages/Inventory'
import { CompanyPage } from './pages/Company'
@ -16,7 +15,10 @@ import {
UsersPage,
CompaniesPage,
ProductsPage,
OrdersPage
OrdersPage,
ReviewsPage,
LogisticsPage,
ProfilePage
} from './pages/admin'
function App() {
@ -38,6 +40,9 @@ function App() {
<Route path="companies" element={<CompaniesPage />} />
<Route path="products" element={<ProductsPage />} />
<Route path="orders" element={<OrdersPage />} />
<Route path="reviews" element={<ReviewsPage />} />
<Route path="logistics" element={<LogisticsPage />} />
<Route path="profile" element={<ProfilePage />} />
</Route>
{/* Legacy admin route - redirect to dashboard */}

View file

@ -7,6 +7,8 @@ const navItems = [
{ path: '/dashboard/companies', label: 'Empresas' },
{ path: '/dashboard/products', label: 'Produtos' },
{ path: '/dashboard/orders', label: 'Pedidos' },
{ path: '/dashboard/reviews', label: 'Avaliações' },
{ path: '/dashboard/logistics', label: 'Logística' },
]
export function Header() {
@ -48,7 +50,9 @@ export function Header() {
{/* User info */}
<div className="flex items-center gap-4">
<div className="hidden sm:block text-right">
<p className="text-sm font-medium">{user?.name}</p>
<Link to="/dashboard/profile" className="text-sm font-medium hover:underline">
{user?.name}
</Link>
<p className="text-xs text-white/70 capitalize">{user?.role}</p>
</div>
<button

View file

@ -6,7 +6,10 @@ import { authService } from '../services/auth'
export type UserRole = 'admin' | 'owner' | 'employee' | 'delivery' | 'seller' | 'customer'
export interface AuthUser {
id: string
name: string
username?: string
email?: string
role: UserRole
token: string
}
@ -14,8 +17,9 @@ export interface AuthUser {
interface AuthContextValue {
user: AuthUser | null
loading: boolean
login: (token: string, role: UserRole, name: string) => void
login: (token: string, role: UserRole, name: string, id: string, email?: string, username?: string) => void
logout: () => void
setUser: (user: AuthUser) => void
}
const AuthContext = createContext<AuthContextValue | undefined>(undefined)
@ -44,8 +48,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}
}, [user])
const login = (token: string, role: UserRole, name: string) => {
setUser({ token, role, name })
const login = (token: string, role: UserRole, name: string, id: string, email?: string, username?: string) => {
setUser({ token, role, name, id, email, username })
// Redirect based on role
switch (role) {
@ -78,7 +82,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
user,
loading,
login,
logout
logout,
setUser
}),
[user, loading]
)

View file

@ -52,13 +52,13 @@ export function LoginPage() {
throw new Error('Resposta de login inválida. Verifique o usuário e a senha.')
}
const payload = decodeJwtPayload<{ role?: string }>(token)
const payload = decodeJwtPayload<{ role?: string, sub: string }>(token)
console.log('🔐 [Login] JWT payload decoded:', payload)
const role = resolveRole(payload?.role)
console.log('🔐 [Login] Role resolved:', role)
login(token, role, username)
login(token, role, username, payload?.sub || '', undefined, username)
console.log('🔐 [Login] Login successful!')
} catch (error) {
console.error('🔐 [Login] ERROR caught:', error)

View file

@ -0,0 +1,140 @@
import { useEffect, useState } from 'react'
import { adminService, Shipment } from '../../services/adminService'
export function LogisticsPage() {
const [shipments, setShipments] = useState<Shipment[]>([])
const [loading, setLoading] = useState(true)
const [page, setPage] = useState(1)
const [total, setTotal] = useState(0)
const pageSize = 10
useEffect(() => {
loadShipments()
}, [page])
const loadShipments = async () => {
setLoading(true)
try {
const data = await adminService.listShipments(page, pageSize)
setShipments(data.shipments || [])
setTotal(data.total)
} catch (err) {
console.error('Error loading shipments:', err)
} finally {
setLoading(false)
}
}
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('pt-BR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
}
const getStatusBadge = (status: string) => {
const colors: Record<string, string> = {
'Enviado': 'bg-blue-100 text-blue-800',
'Entregue': 'bg-green-100 text-green-800',
'Pendente': 'bg-yellow-100 text-yellow-800',
}
return (
<span className={`rounded-full px-2 py-1 text-xs font-medium ${colors[status] || 'bg-gray-100 text-gray-800'}`}>
{status}
</span>
)
}
const totalPages = Math.ceil(total / pageSize)
return (
<div>
<div className="mb-6 flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-900">Logística de Envios</h1>
</div>
{/* Table */}
<div className="overflow-hidden rounded-lg bg-white shadow">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-blue-900 text-white">
<tr>
<th className="px-4 py-3 text-left text-sm font-medium">Data</th>
<th className="px-4 py-3 text-left text-sm font-medium">Transportadora</th>
<th className="px-4 py-3 text-left text-sm font-medium">Rastreio</th>
<th className="px-4 py-3 text-left text-sm font-medium">External</th>
<th className="px-4 py-3 text-left text-sm font-medium">Status</th>
<th className="px-4 py-3 text-left text-sm font-medium">Pedido ID</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{loading ? (
<tr>
<td colSpan={6} className="py-8 text-center text-gray-500">
Carregando...
</td>
</tr>
) : shipments.length === 0 ? (
<tr>
<td colSpan={6} className="py-8 text-center text-gray-500">
Nenhum envio encontrado
</td>
</tr>
) : (
shipments.map((shipment) => (
<tr key={shipment.id} className="hover:bg-gray-50">
<td className="px-4 py-3 text-sm text-gray-600">
{formatDate(shipment.created_at)}
</td>
<td className="px-4 py-3 text-sm font-medium text-gray-900">
{shipment.carrier}
</td>
<td className="px-4 py-3 text-sm font-mono text-blue-600">
{shipment.tracking_code}
</td>
<td className="px-4 py-3 text-sm text-gray-500">
{shipment.external_tracking || '-'}
</td>
<td className="px-4 py-3">
{getStatusBadge(shipment.status)}
</td>
<td className="px-4 py-3 text-xs font-mono text-gray-500">
{shipment.order_id.substring(0, 8)}...
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="mt-4 flex items-center justify-between">
<p className="text-sm text-gray-600">
Mostrando {(page - 1) * pageSize + 1} a {Math.min(page * pageSize, total)} de {total}
</p>
<div className="flex gap-2">
<button
onClick={() => setPage(page - 1)}
disabled={page === 1}
className="rounded border px-3 py-1 text-sm disabled:opacity-50"
>
Anterior
</button>
<button
onClick={() => setPage(page + 1)}
disabled={page >= totalPages}
className="rounded border px-3 py-1 text-sm disabled:opacity-50"
>
Próxima
</button>
</div>
</div>
)}
</div>
)
}

View file

@ -0,0 +1,156 @@
import { useEffect, useState } from 'react'
import { adminService, User } from '../../services/adminService'
import { useAuth } from '../../context/AuthContext'
export function ProfilePage() {
const { user, setUser: setAuthUser } = useAuth()
const [formData, setFormData] = useState({
name: '',
username: '',
email: '',
password: '',
confirmPassword: ''
})
const [loading, setLoading] = useState(false)
const [message, setMessage] = useState('')
useEffect(() => {
if (user) {
setFormData(prev => ({
...prev,
name: user.name,
username: user.username || '',
email: user.email || ''
}))
}
}, [user])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!user) return
if (formData.password && formData.password !== formData.confirmPassword) {
alert('Senhas não conferem')
return
}
setLoading(true)
setMessage('')
try {
const updateData: any = {
name: formData.name,
username: formData.username,
email: formData.email
}
if (formData.password) {
updateData.password = formData.password
}
const updatedUser = await adminService.updateUser(user.id, updateData)
// Update auth context
// We need to map AdminService User to AuthContext User (they are similar)
setAuthUser({
...user,
name: updatedUser.name,
username: updatedUser.username,
email: updatedUser.email
})
setMessage('Perfil atualizado com sucesso!')
setFormData(prev => ({ ...prev, password: '', confirmPassword: '' }))
} catch (err) {
console.error('Error updating profile:', err)
setMessage('Erro ao atualizar perfil')
} finally {
setLoading(false)
}
}
return (
<div className="mx-auto max-w-2xl">
<h1 className="mb-6 text-2xl font-bold text-gray-900">Meu Perfil</h1>
<div className="rounded-lg bg-white p-6 shadow">
{message && (
<div className={`mb-4 rounded p-3 text-sm ${message.includes('Erro') ? 'bg-red-100 text-red-700' : 'bg-green-100 text-green-700'}`}>
{message}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">Nome</label>
<input
type="text"
value={formData.name}
onChange={e => setFormData({ ...formData, name: e.target.value })}
className="mt-1 block w-full rounded-md border border-gray-300 p-2 focus:border-blue-500 focus:outline-none"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Usuário</label>
<input
type="text"
value={formData.username}
onChange={e => setFormData({ ...formData, username: e.target.value })}
className="mt-1 block w-full rounded-md border border-gray-300 p-2 focus:border-blue-500 focus:outline-none"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Email</label>
<input
type="email"
value={formData.email}
onChange={e => setFormData({ ...formData, email: e.target.value })}
className="mt-1 block w-full rounded-md border border-gray-300 p-2 focus:border-blue-500 focus:outline-none"
required
/>
</div>
<div className="pt-4">
<h3 className="mb-2 text-sm font-medium text-gray-900">Alterar Senha</h3>
<p className="mb-3 text-xs text-gray-500">Deixe em branco para manter a senha atual</p>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">Nova Senha</label>
<input
type="password"
value={formData.password}
onChange={e => setFormData({ ...formData, password: e.target.value })}
className="mt-1 block w-full rounded-md border border-gray-300 p-2 focus:border-blue-500 focus:outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Confirmar Nova Senha</label>
<input
type="password"
value={formData.confirmPassword}
onChange={e => setFormData({ ...formData, confirmPassword: e.target.value })}
className="mt-1 block w-full rounded-md border border-gray-300 p-2 focus:border-blue-500 focus:outline-none"
/>
</div>
</div>
</div>
<div className="pt-4">
<button
type="submit"
disabled={loading}
className="flex w-full justify-center rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:bg-blue-300"
>
{loading ? 'Salvando...' : 'Salvar Alterações'}
</button>
</div>
</form>
</div>
</div>
)
}

View file

@ -0,0 +1,135 @@
import { useEffect, useState } from 'react'
import { adminService, Review } from '../../services/adminService'
export function ReviewsPage() {
const [reviews, setReviews] = useState<Review[]>([])
const [loading, setLoading] = useState(true)
const [page, setPage] = useState(1)
const [total, setTotal] = useState(0)
const pageSize = 10
useEffect(() => {
loadReviews()
}, [page])
const loadReviews = async () => {
setLoading(true)
try {
const data = await adminService.listReviews(page, pageSize)
setReviews(data.reviews || [])
setTotal(data.total)
} catch (err) {
console.error('Error loading reviews:', err)
} finally {
setLoading(false)
}
}
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('pt-BR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
}
const renderStars = (rating: number) => {
return (
<div className="flex text-yellow-500">
{[...Array(5)].map((_, i) => (
<span key={i}>
{i < rating ? '★' : '☆'}
</span>
))}
</div>
)
}
const totalPages = Math.ceil(total / pageSize)
return (
<div>
<div className="mb-6 flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-900">Avaliações</h1>
</div>
{/* Table */}
<div className="overflow-hidden rounded-lg bg-white shadow">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-blue-900 text-white">
<tr>
<th className="px-4 py-3 text-left text-sm font-medium">Data</th>
<th className="px-4 py-3 text-left text-sm font-medium">Pedido</th>
<th className="px-4 py-3 text-left text-sm font-medium">Nota</th>
<th className="px-4 py-3 text-left text-sm font-medium">Comentário</th>
<th className="px-4 py-3 text-left text-sm font-medium">Comprador ID</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{loading ? (
<tr>
<td colSpan={5} className="py-8 text-center text-gray-500">
Carregando...
</td>
</tr>
) : reviews.length === 0 ? (
<tr>
<td colSpan={5} className="py-8 text-center text-gray-500">
Nenhuma avaliação encontrada
</td>
</tr>
) : (
reviews.map((review) => (
<tr key={review.id} className="hover:bg-gray-50">
<td className="px-4 py-3 text-sm text-gray-600">
{formatDate(review.created_at)}
</td>
<td className="px-4 py-3 text-sm font-mono text-gray-600">
{review.order_id.substring(0, 8)}...
</td>
<td className="px-4 py-3">
{renderStars(review.rating)}
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{review.comment}
</td>
<td className="px-4 py-3 text-xs font-mono text-gray-500">
{review.buyer_id.substring(0, 8)}...
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="mt-4 flex items-center justify-between">
<p className="text-sm text-gray-600">
Mostrando {(page - 1) * pageSize + 1} a {Math.min(page * pageSize, total)} de {total}
</p>
<div className="flex gap-2">
<button
onClick={() => setPage(page - 1)}
disabled={page === 1}
className="rounded border px-3 py-1 text-sm disabled:opacity-50"
>
Anterior
</button>
<button
onClick={() => setPage(page + 1)}
disabled={page >= totalPages}
className="rounded border px-3 py-1 text-sm disabled:opacity-50"
>
Próxima
</button>
</div>
</div>
)}
</div>
)
}

View file

@ -3,3 +3,6 @@ export { CompaniesPage } from './CompaniesPage'
export { ProductsPage } from './ProductsPage'
export { OrdersPage } from './OrdersPage'
export { DashboardHome } from './DashboardHome'
export { ReviewsPage } from './ReviewsPage'
export { LogisticsPage } from './LogisticsPage'
export { ProfilePage } from './ProfilePage'

View file

@ -302,5 +302,57 @@ export const adminService = {
log('deleteOrder done')
return result
},
// ================== REVIEWS ==================
listReviews: async (page = 1, pageSize = 20) => {
log('listReviews', { page, pageSize })
const result = await apiClient.get<ReviewPage>(`/v1/admin/reviews?page=${page}&page_size=${pageSize}`)
log('listReviews result', result)
return result
},
// ================== SHIPMENTS ==================
listShipments: async (page = 1, pageSize = 20) => {
log('listShipments', { page, pageSize })
const result = await apiClient.get<ShipmentPage>(`/v1/admin/shipments?page=${page}&page_size=${pageSize}`)
log('listShipments result', result)
return result
},
}
// ================== REVIEWS & SHIPMENTS TYPES ==================
export interface Review {
id: string
order_id: string
buyer_id: string
seller_id: string
rating: number
comment: string
created_at: string
}
export interface ReviewPage {
reviews: Review[]
total: number
page: number
page_size: number
}
export interface Shipment {
id: string
order_id: string
carrier: string
tracking_code: string
external_tracking: string
status: string
created_at: string
updated_at: string
}
export interface ShipmentPage {
shipments: Shipment[]
total: number
page: number
page_size: number
}

View file

@ -293,6 +293,26 @@ func SeedLean(dsn string) (string, error) {
VALUES ('%s', '%s', '%s', '%s', %d, NOW(), NOW())`,
orderID, buyerID, sellerID, status, totalCents,
))
// Create Review if Delivered
if status == "Entregue" {
rating := 3 + rng.Intn(3) // 3-5 stars
comment := []string{"Ótimo atendimento!", "Entrega rápida", "Recomendo", "Tudo certo"}[rng.Intn(4)]
mustExec(db, fmt.Sprintf(`INSERT INTO reviews (id, order_id, buyer_id, seller_id, rating, comment, created_at)
VALUES ('%s', '%s', '%s', '%s', %d, '%s', NOW())`,
uuid.Must(uuid.NewV7()), orderID, buyerID, sellerID, rating, comment,
))
}
// Create Shipment if Faturado or Entregue
if status == "Faturado" || status == "Entregue" {
carrier := []string{"Correios", "Loggi", "TotalExpress"}[rng.Intn(3)]
tracking := fmt.Sprintf("TRK-%d%d", rng.Intn(1000), rng.Intn(1000))
mustExec(db, fmt.Sprintf(`INSERT INTO shipments (id, order_id, carrier, tracking_code, external_tracking, status, created_at, updated_at)
VALUES ('%s', '%s', '%s', '%s', '', 'Enviado', NOW(), NOW())`,
uuid.Must(uuid.NewV7()), orderID, carrier, tracking,
))
}
}
log.Printf("✅ [Lean] Created %d orders", numOrders)