feat: improve admin products with store column and seeder with orders/cart

- Add Loja (store) column to ProductsPage showing which company owns product
- Optimize ProductsPage to update local state instead of reloading list
- Add orders (5-10 random) and cart items to lean seeder for testing
- Fix expires_at date format to ISO 8601 for backend compatibility
- Improve delete error message for products with related orders
This commit is contained in:
Tiago Yamamoto 2025-12-22 08:29:22 -03:00
parent 6c0b4c4cd6
commit 4f6c96daf0
2 changed files with 128 additions and 50 deletions

View file

@ -172,6 +172,7 @@ export function ProductsPage() {
<thead className="bg-blue-900 text-white">
<tr>
<th className="px-4 py-3 text-left text-sm font-medium">Produto</th>
<th className="px-4 py-3 text-left text-sm font-medium">Loja</th>
<th className="px-4 py-3 text-left text-sm font-medium">Lote</th>
<th className="px-4 py-3 text-left text-sm font-medium">Validade</th>
<th className="px-4 py-3 text-right text-sm font-medium">Preço</th>
@ -182,59 +183,67 @@ export function ProductsPage() {
<tbody className="divide-y divide-gray-200">
{loading ? (
<tr>
<td colSpan={6} className="py-8 text-center text-gray-500">
<td colSpan={7} className="py-8 text-center text-gray-500">
Carregando...
</td>
</tr>
) : products.length === 0 ? (
<tr>
<td colSpan={6} className="py-8 text-center text-gray-500">
<td colSpan={7} className="py-8 text-center text-gray-500">
Nenhum produto encontrado
</td>
</tr>
) : (
products.map((product) => (
<tr key={product.id} className="hover:bg-gray-50">
<td className="px-4 py-3">
<div className="text-sm font-medium text-gray-900">{product.name}</div>
<div className="text-xs text-gray-500">{product.description}</div>
</td>
<td className="px-4 py-3 text-sm text-gray-600">{product.batch}</td>
<td className="px-4 py-3">
<span className={`rounded-full px-2 py-1 text-xs font-medium ${isExpiringSoon(product.expires_at)
? 'bg-red-100 text-red-800'
: 'bg-green-100 text-green-800'
}`}>
{formatDate(product.expires_at)}
</span>
</td>
<td className="px-4 py-3 text-right text-sm font-medium text-gray-900">
{formatPrice(product.price_cents)}
</td>
<td className="px-4 py-3 text-right">
<span className={`rounded-full px-2 py-1 text-xs font-medium ${product.stock < 10
? 'bg-yellow-100 text-yellow-800'
: 'bg-blue-100 text-blue-800'
}`}>
{product.stock}
</span>
</td>
<td className="px-4 py-3 text-right">
<button
onClick={() => openEdit(product)}
className="mr-2 text-sm text-blue-600 hover:underline"
>
Editar
</button>
<button
onClick={() => handleDelete(product.id)}
className="text-sm text-red-600 hover:underline"
>
Excluir
</button>
</td>
</tr>
))
products.map((product) => {
const company = companies.find(c => c.id === product.seller_id)
return (
<tr key={product.id} className="hover:bg-gray-50">
<td className="px-4 py-3">
<div className="text-sm font-medium text-gray-900">{product.name}</div>
<div className="text-xs text-gray-500">{product.description}</div>
</td>
<td className="px-4 py-3">
<span className="rounded bg-purple-100 px-2 py-1 text-xs font-medium text-purple-800">
{company?.corporate_name || 'N/A'}
</span>
</td>
<td className="px-4 py-3 text-sm text-gray-600">{product.batch}</td>
<td className="px-4 py-3">
<span className={`rounded-full px-2 py-1 text-xs font-medium ${isExpiringSoon(product.expires_at)
? 'bg-red-100 text-red-800'
: 'bg-green-100 text-green-800'
}`}>
{formatDate(product.expires_at)}
</span>
</td>
<td className="px-4 py-3 text-right text-sm font-medium text-gray-900">
{formatPrice(product.price_cents)}
</td>
<td className="px-4 py-3 text-right">
<span className={`rounded-full px-2 py-1 text-xs font-medium ${product.stock < 10
? 'bg-yellow-100 text-yellow-800'
: 'bg-blue-100 text-blue-800'
}`}>
{product.stock}
</span>
</td>
<td className="px-4 py-3 text-right">
<button
onClick={() => openEdit(product)}
className="mr-2 text-sm text-blue-600 hover:underline"
>
Editar
</button>
<button
onClick={() => handleDelete(product.id)}
className="text-sm text-red-600 hover:underline"
>
Excluir
</button>
</td>
</tr>
)
})
)}
</tbody>
</table>

View file

@ -189,6 +189,8 @@ func SeedLean(dsn string) (string, error) {
now := time.Now().UTC()
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
createdUsers := []string{}
var allProducts []map[string]interface{}
var pharmacyCompanyIDs []uuid.UUID
for _, ph := range pharmacies {
// 1. Create Company
@ -238,15 +240,82 @@ func SeedLean(dsn string) (string, error) {
log.Printf("insert product lean: %v", err)
}
}
// Store products for orders/cart creation later
allProducts = append(allProducts, prods...)
pharmacyCompanyIDs = append(pharmacyCompanyIDs, companyID)
log.Printf("✅ [Lean] %s created with %d products", ph.Name, len(prods))
}
// 4. Create Global Admin (linked to first pharmacy for FK constraint, or standalone if nullable? Schema says NOT NULL)
// Build Admin linked to "Farmácia Central" (Suffix 1)
// Find ID of first pharmacy? I need to track it.
// Actually, just query it or store it.
// Simpler: I'll just create a separate "Admin Company" or link to one.
// Linking to Central is fine.
// 4. Create some orders between pharmacies
if len(allProducts) > 0 && len(pharmacyCompanyIDs) > 1 {
// Create 5-10 orders
numOrders := 5 + rng.Intn(6)
for i := 0; i < numOrders; i++ {
// Random buyer and seller (different companies)
buyerIdx := rng.Intn(len(pharmacyCompanyIDs))
sellerIdx := (buyerIdx + 1) % len(pharmacyCompanyIDs)
buyerID := pharmacyCompanyIDs[buyerIdx]
sellerID := pharmacyCompanyIDs[sellerIdx]
// Find products from seller
var sellerProducts []map[string]interface{}
for _, p := range allProducts {
if p["seller_id"].(uuid.UUID).String() == sellerID.String() {
sellerProducts = append(sellerProducts, p)
}
}
if len(sellerProducts) == 0 {
continue
}
// Create order with 1-3 items
orderID := uuid.Must(uuid.NewV7())
statuses := []string{"Pendente", "Pago", "Faturado", "Entregue"}
status := statuses[rng.Intn(len(statuses))]
totalCents := int64(0)
// Create order items
numItems := 1 + rng.Intn(3)
for j := 0; j < numItems && j < len(sellerProducts); j++ {
prod := sellerProducts[rng.Intn(len(sellerProducts))]
qty := int64(1 + rng.Intn(5))
itemID := uuid.Must(uuid.NewV7())
priceCents := prod["price_cents"].(int64)
totalCents += priceCents * qty
mustExec(db, fmt.Sprintf(`INSERT INTO order_items (id, order_id, product_id, quantity, unit_cents, batch, expires_at)
VALUES ('%s', '%s', '%s', %d, %d, '%s', '%s')`,
itemID, orderID, prod["id"].(uuid.UUID), qty, priceCents, prod["batch"].(string), prod["expires_at"].(time.Time).Format(time.RFC3339),
))
}
mustExec(db, fmt.Sprintf(`INSERT INTO orders (id, buyer_id, seller_id, status, total_cents, created_at, updated_at)
VALUES ('%s', '%s', '%s', '%s', %d, NOW(), NOW())`,
orderID, buyerID, sellerID, status, totalCents,
))
}
log.Printf("✅ [Lean] Created %d orders", numOrders)
// 5. Add cart items for first pharmacy
if len(pharmacyCompanyIDs) > 0 && len(allProducts) > 3 {
buyerID := pharmacyCompanyIDs[0]
for i := 0; i < 3; i++ {
prod := allProducts[rng.Intn(len(allProducts))]
if prod["seller_id"].(uuid.UUID).String() == buyerID.String() {
continue // Skip own products
}
cartItemID := uuid.Must(uuid.NewV7())
qty := int64(1 + rng.Intn(3))
mustExec(db, fmt.Sprintf(`INSERT INTO cart_items (id, buyer_id, product_id, quantity, created_at)
VALUES ('%s', '%s', '%s', %d, NOW()) ON CONFLICT DO NOTHING`,
cartItemID, buyerID, prod["id"].(uuid.UUID), qty,
))
}
log.Printf("✅ [Lean] Added cart items")
}
}
// 6. Create Global Admin (linked to first pharmacy for FK constraint)
var centralID string
err = db.Get(&centralID, "SELECT id FROM companies WHERE cnpj = '11111111000111'")
if err == nil {