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">
|
<thead className="bg-blue-900 text-white">
|
||||||
<tr>
|
<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">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">Lote</th>
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium">Validade</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>
|
<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">
|
<tbody className="divide-y divide-gray-200">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={6} className="py-8 text-center text-gray-500">
|
<td colSpan={7} className="py-8 text-center text-gray-500">
|
||||||
Carregando...
|
Carregando...
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : products.length === 0 ? (
|
) : products.length === 0 ? (
|
||||||
<tr>
|
<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
|
Nenhum produto encontrado
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
products.map((product) => (
|
products.map((product) => {
|
||||||
<tr key={product.id} className="hover:bg-gray-50">
|
const company = companies.find(c => c.id === product.seller_id)
|
||||||
<td className="px-4 py-3">
|
return (
|
||||||
<div className="text-sm font-medium text-gray-900">{product.name}</div>
|
<tr key={product.id} className="hover:bg-gray-50">
|
||||||
<div className="text-xs text-gray-500">{product.description}</div>
|
<td className="px-4 py-3">
|
||||||
</td>
|
<div className="text-sm font-medium text-gray-900">{product.name}</div>
|
||||||
<td className="px-4 py-3 text-sm text-gray-600">{product.batch}</td>
|
<div className="text-xs text-gray-500">{product.description}</div>
|
||||||
<td className="px-4 py-3">
|
</td>
|
||||||
<span className={`rounded-full px-2 py-1 text-xs font-medium ${isExpiringSoon(product.expires_at)
|
<td className="px-4 py-3">
|
||||||
? 'bg-red-100 text-red-800'
|
<span className="rounded bg-purple-100 px-2 py-1 text-xs font-medium text-purple-800">
|
||||||
: 'bg-green-100 text-green-800'
|
{company?.corporate_name || 'N/A'}
|
||||||
}`}>
|
</span>
|
||||||
{formatDate(product.expires_at)}
|
</td>
|
||||||
</span>
|
<td className="px-4 py-3 text-sm text-gray-600">{product.batch}</td>
|
||||||
</td>
|
<td className="px-4 py-3">
|
||||||
<td className="px-4 py-3 text-right text-sm font-medium text-gray-900">
|
<span className={`rounded-full px-2 py-1 text-xs font-medium ${isExpiringSoon(product.expires_at)
|
||||||
{formatPrice(product.price_cents)}
|
? 'bg-red-100 text-red-800'
|
||||||
</td>
|
: 'bg-green-100 text-green-800'
|
||||||
<td className="px-4 py-3 text-right">
|
}`}>
|
||||||
<span className={`rounded-full px-2 py-1 text-xs font-medium ${product.stock < 10
|
{formatDate(product.expires_at)}
|
||||||
? 'bg-yellow-100 text-yellow-800'
|
</span>
|
||||||
: 'bg-blue-100 text-blue-800'
|
</td>
|
||||||
}`}>
|
<td className="px-4 py-3 text-right text-sm font-medium text-gray-900">
|
||||||
{product.stock}
|
{formatPrice(product.price_cents)}
|
||||||
</span>
|
</td>
|
||||||
</td>
|
<td className="px-4 py-3 text-right">
|
||||||
<td className="px-4 py-3 text-right">
|
<span className={`rounded-full px-2 py-1 text-xs font-medium ${product.stock < 10
|
||||||
<button
|
? 'bg-yellow-100 text-yellow-800'
|
||||||
onClick={() => openEdit(product)}
|
: 'bg-blue-100 text-blue-800'
|
||||||
className="mr-2 text-sm text-blue-600 hover:underline"
|
}`}>
|
||||||
>
|
{product.stock}
|
||||||
Editar
|
</span>
|
||||||
</button>
|
</td>
|
||||||
<button
|
<td className="px-4 py-3 text-right">
|
||||||
onClick={() => handleDelete(product.id)}
|
<button
|
||||||
className="text-sm text-red-600 hover:underline"
|
onClick={() => openEdit(product)}
|
||||||
>
|
className="mr-2 text-sm text-blue-600 hover:underline"
|
||||||
Excluir
|
>
|
||||||
</button>
|
Editar
|
||||||
</td>
|
</button>
|
||||||
</tr>
|
<button
|
||||||
))
|
onClick={() => handleDelete(product.id)}
|
||||||
|
className="text-sm text-red-600 hover:underline"
|
||||||
|
>
|
||||||
|
Excluir
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,8 @@ func SeedLean(dsn string) (string, error) {
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
createdUsers := []string{}
|
createdUsers := []string{}
|
||||||
|
var allProducts []map[string]interface{}
|
||||||
|
var pharmacyCompanyIDs []uuid.UUID
|
||||||
|
|
||||||
for _, ph := range pharmacies {
|
for _, ph := range pharmacies {
|
||||||
// 1. Create Company
|
// 1. Create Company
|
||||||
|
|
@ -238,15 +240,82 @@ func SeedLean(dsn string) (string, error) {
|
||||||
log.Printf("insert product lean: %v", err)
|
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))
|
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)
|
// 4. Create some orders between pharmacies
|
||||||
// Build Admin linked to "Farmácia Central" (Suffix 1)
|
if len(allProducts) > 0 && len(pharmacyCompanyIDs) > 1 {
|
||||||
// Find ID of first pharmacy? I need to track it.
|
// Create 5-10 orders
|
||||||
// Actually, just query it or store it.
|
numOrders := 5 + rng.Intn(6)
|
||||||
// Simpler: I'll just create a separate "Admin Company" or link to one.
|
for i := 0; i < numOrders; i++ {
|
||||||
// Linking to Central is fine.
|
// 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
|
var centralID string
|
||||||
err = db.Get(¢ralID, "SELECT id FROM companies WHERE cnpj = '11111111000111'")
|
err = db.Get(¢ralID, "SELECT id FROM companies WHERE cnpj = '11111111000111'")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue