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:
parent
6c0b4c4cd6
commit
4f6c96daf0
2 changed files with 128 additions and 50 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(¢ralID, "SELECT id FROM companies WHERE cnpj = '11111111000111'")
|
||||
if err == nil {
|
||||
|
|
|
|||
Loading…
Reference in a new issue