feat: implement 2-step product registration with new catalog fields
Backend: - Add ean_code, manufacturer, category, subcategory, observations to Product model - Create migration 0006_product_catalog_fields.sql - Update repository queries (CreateProduct, ListProducts, ListRecords) Frontend: - Create ProductCreate.tsx with 2-step wizard form - Add route /products/new - Add 'Cadastrar Produto' button to Inventory page Seeder: - Update CREATE TABLE products with new columns - Update generateProducts with EAN codes and manufacturers Docs: - Update database-schema.md with new fields
This commit is contained in:
parent
091e8093c0
commit
607d942072
8 changed files with 464 additions and 28 deletions
|
|
@ -70,16 +70,21 @@ type UserPage struct {
|
|||
|
||||
// Product represents a medicine SKU with batch tracking.
|
||||
type Product struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
SellerID uuid.UUID `db:"seller_id" json:"seller_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Description string `db:"description" json:"description"`
|
||||
Batch string `db:"batch" json:"batch"`
|
||||
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
|
||||
PriceCents int64 `db:"price_cents" json:"price_cents"`
|
||||
Stock int64 `db:"stock" json:"stock"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
SellerID uuid.UUID `db:"seller_id" json:"seller_id"`
|
||||
EANCode string `db:"ean_code" json:"ean_code"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Description string `db:"description" json:"description"`
|
||||
Manufacturer string `db:"manufacturer" json:"manufacturer"`
|
||||
Category string `db:"category" json:"category"`
|
||||
Subcategory string `db:"subcategory" json:"subcategory"`
|
||||
Batch string `db:"batch" json:"batch"`
|
||||
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
|
||||
PriceCents int64 `db:"price_cents" json:"price_cents"`
|
||||
Stock int64 `db:"stock" json:"stock"`
|
||||
Observations string `db:"observations" json:"observations"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
// InventoryItem exposes stock tracking tied to product batches.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
-- Migration: Add product catalog fields
|
||||
-- +goose Up
|
||||
ALTER TABLE products ADD COLUMN IF NOT EXISTS ean_code TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE products ADD COLUMN IF NOT EXISTS manufacturer TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE products ADD COLUMN IF NOT EXISTS category TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE products ADD COLUMN IF NOT EXISTS subcategory TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE products ADD COLUMN IF NOT EXISTS observations TEXT NOT NULL DEFAULT '';
|
||||
|
||||
-- +goose Down
|
||||
ALTER TABLE products DROP COLUMN IF EXISTS ean_code;
|
||||
ALTER TABLE products DROP COLUMN IF EXISTS manufacturer;
|
||||
ALTER TABLE products DROP COLUMN IF EXISTS category;
|
||||
ALTER TABLE products DROP COLUMN IF EXISTS subcategory;
|
||||
ALTER TABLE products DROP COLUMN IF EXISTS observations;
|
||||
|
|
@ -159,8 +159,8 @@ func (r *Repository) DeleteCompany(ctx context.Context, id uuid.UUID) error {
|
|||
}
|
||||
|
||||
func (r *Repository) CreateProduct(ctx context.Context, product *domain.Product) error {
|
||||
query := `INSERT INTO products (id, seller_id, name, description, batch, expires_at, price_cents, stock)
|
||||
VALUES (:id, :seller_id, :name, :description, :batch, :expires_at, :price_cents, :stock)
|
||||
query := `INSERT INTO products (id, seller_id, ean_code, name, description, manufacturer, category, subcategory, batch, expires_at, price_cents, stock, observations)
|
||||
VALUES (:id, :seller_id, :ean_code, :name, :description, :manufacturer, :category, :subcategory, :batch, :expires_at, :price_cents, :stock, :observations)
|
||||
RETURNING created_at, updated_at`
|
||||
|
||||
rows, err := r.db.NamedQueryContext(ctx, query, product)
|
||||
|
|
@ -205,7 +205,7 @@ func (r *Repository) ListProducts(ctx context.Context, filter domain.ProductFilt
|
|||
filter.Limit = 20
|
||||
}
|
||||
args = append(args, filter.Limit, filter.Offset)
|
||||
listQuery := fmt.Sprintf("SELECT id, seller_id, name, description, batch, expires_at, price_cents, stock, created_at, updated_at %s%s ORDER BY created_at DESC LIMIT $%d OFFSET $%d", baseQuery, where, len(args)-1, len(args))
|
||||
listQuery := fmt.Sprintf("SELECT id, seller_id, ean_code, name, description, manufacturer, category, subcategory, batch, expires_at, price_cents, stock, observations, created_at, updated_at %s%s ORDER BY created_at DESC LIMIT $%d OFFSET $%d", baseQuery, where, len(args)-1, len(args))
|
||||
|
||||
var products []domain.Product
|
||||
if err := r.db.SelectContext(ctx, &products, listQuery, args...); err != nil {
|
||||
|
|
@ -256,7 +256,7 @@ func (r *Repository) ListRecords(ctx context.Context, filter domain.RecordSearch
|
|||
}
|
||||
args = append(args, filter.Limit, filter.Offset)
|
||||
|
||||
listQuery := fmt.Sprintf(`SELECT id, seller_id, name, description, batch, expires_at, price_cents, stock, created_at, updated_at,
|
||||
listQuery := fmt.Sprintf(`SELECT id, seller_id, ean_code, name, description, manufacturer, category, subcategory, batch, expires_at, price_cents, stock, observations, created_at, updated_at,
|
||||
COUNT(*) OVER() AS total_count
|
||||
%s%s ORDER BY %s %s LIMIT $%d OFFSET $%d`, baseQuery, where, sortBy, sortOrder, len(args)-1, len(args))
|
||||
|
||||
|
|
|
|||
|
|
@ -178,12 +178,17 @@ Produtos cadastrados para venda.
|
|||
|--------|------|-----------|
|
||||
| `id` | UUID | Chave primária |
|
||||
| `seller_id` | UUID | FK para companies (vendedor) |
|
||||
| `ean_code` | TEXT | Código EAN/barras |
|
||||
| `name` | TEXT | Nome do produto |
|
||||
| `description` | TEXT | Descrição |
|
||||
| `manufacturer` | TEXT | Laboratório/Fabricante |
|
||||
| `category` | TEXT | Categoria (Analgésicos, Antibióticos, etc) |
|
||||
| `subcategory` | TEXT | Subcategoria (opcional) |
|
||||
| `batch` | TEXT | Lote |
|
||||
| `expires_at` | DATE | Data de validade |
|
||||
| `price_cents` | BIGINT | Preço em centavos |
|
||||
| `stock` | BIGINT | Quantidade em estoque |
|
||||
| `observations` | TEXT | Observações adicionais |
|
||||
| `created_at` | TIMESTAMPTZ | Data de criação |
|
||||
| `updated_at` | TIMESTAMPTZ | Data de atualização |
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { LoginPage } from './pages/Login'
|
|||
import { CartPage } from './pages/Cart'
|
||||
import { OrdersPage as UserOrdersPage } from './pages/Orders'
|
||||
import { InventoryPage } from './pages/Inventory'
|
||||
import { ProductCreatePage } from './pages/ProductCreate'
|
||||
import { CompanyPage } from './pages/Company'
|
||||
import { SellerDashboardPage } from './pages/SellerDashboard'
|
||||
import { EmployeeDashboardPage } from './pages/EmployeeDashboard'
|
||||
|
|
@ -103,6 +104,14 @@ function App() {
|
|||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/products/new"
|
||||
element={
|
||||
<ProtectedRoute allowedRoles={['owner', 'seller']}>
|
||||
<ProductCreatePage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/company"
|
||||
element={
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Shell } from '../layouts/Shell'
|
||||
import { apiClient } from '../services/apiClient'
|
||||
import { formatCents } from '../utils/format'
|
||||
|
|
@ -63,7 +64,7 @@ export function InventoryPage() {
|
|||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-medicalBlue">Estoque</h1>
|
||||
<h1 className="text-xl font-semibold text-medicalBlue">Meus Produtos</h1>
|
||||
<p className="text-sm text-gray-600">Controle de inventário e validades</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
|
|
@ -79,10 +80,16 @@ export function InventoryPage() {
|
|||
</select>
|
||||
<button
|
||||
onClick={loadInventory}
|
||||
className="rounded bg-medicalBlue px-4 py-2 text-sm font-semibold text-white"
|
||||
className="rounded bg-gray-200 px-4 py-2 text-sm font-semibold text-gray-700 hover:bg-gray-300"
|
||||
>
|
||||
Atualizar
|
||||
</button>
|
||||
<Link
|
||||
to="/products/new"
|
||||
className="rounded bg-green-600 px-4 py-2 text-sm font-semibold text-white hover:bg-green-700"
|
||||
>
|
||||
+ Cadastrar Produto
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
381
marketplace/src/pages/ProductCreate.tsx
Normal file
381
marketplace/src/pages/ProductCreate.tsx
Normal file
|
|
@ -0,0 +1,381 @@
|
|||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Shell } from '../layouts/Shell'
|
||||
import { apiClient } from '../services/apiClient'
|
||||
|
||||
interface ProductForm {
|
||||
// Step 1 - Catalog
|
||||
ean_code: string
|
||||
name: string
|
||||
description: string
|
||||
manufacturer: string
|
||||
category: string
|
||||
subcategory: string
|
||||
// Step 2 - Stock/Sales
|
||||
stock: string
|
||||
price: string
|
||||
expires_at: string
|
||||
observations: string
|
||||
}
|
||||
|
||||
const categories = [
|
||||
'Analgésicos',
|
||||
'Antibióticos',
|
||||
'Anti-inflamatórios',
|
||||
'Cardiovasculares',
|
||||
'Dermatológicos',
|
||||
'Vitaminas',
|
||||
'Oftálmicos',
|
||||
'Respiratórios',
|
||||
'Gastrointestinais',
|
||||
'Antidiabéticos',
|
||||
'Perfumaria',
|
||||
'Higiene',
|
||||
'Outros'
|
||||
]
|
||||
|
||||
export function ProductCreatePage() {
|
||||
const navigate = useNavigate()
|
||||
const [step, setStep] = useState(1)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const [form, setForm] = useState<ProductForm>({
|
||||
ean_code: '',
|
||||
name: '',
|
||||
description: '',
|
||||
manufacturer: '',
|
||||
category: '',
|
||||
subcategory: '',
|
||||
stock: '',
|
||||
price: '',
|
||||
expires_at: '',
|
||||
observations: ''
|
||||
})
|
||||
|
||||
const updateField = (field: keyof ProductForm, value: string) => {
|
||||
setForm(prev => ({ ...prev, [field]: value }))
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
if (step === 1) {
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
ean_code: '',
|
||||
name: '',
|
||||
description: '',
|
||||
manufacturer: '',
|
||||
category: '',
|
||||
subcategory: ''
|
||||
}))
|
||||
} else {
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
stock: '',
|
||||
price: '',
|
||||
expires_at: '',
|
||||
observations: ''
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
// Validate step 1
|
||||
if (!form.ean_code || !form.name || !form.manufacturer || !form.category) {
|
||||
setError('Preencha todos os campos obrigatórios')
|
||||
return
|
||||
}
|
||||
setError(null)
|
||||
setStep(2)
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
setStep(1)
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// Validate step 2
|
||||
if (!form.stock || !form.price || !form.expires_at) {
|
||||
setError('Preencha todos os campos obrigatórios')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const priceCents = Math.round(parseFloat(form.price.replace(',', '.')) * 100)
|
||||
|
||||
await apiClient.post('/v1/products', {
|
||||
ean_code: form.ean_code,
|
||||
name: form.name,
|
||||
description: form.description,
|
||||
manufacturer: form.manufacturer,
|
||||
category: form.category,
|
||||
subcategory: form.subcategory,
|
||||
batch: `L${new Date().getFullYear()}${String(Math.floor(Math.random() * 999)).padStart(3, '0')}`,
|
||||
expires_at: form.expires_at,
|
||||
price_cents: priceCents,
|
||||
stock: parseInt(form.stock),
|
||||
observations: form.observations
|
||||
})
|
||||
|
||||
navigate('/inventory')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setError('Erro ao cadastrar produto')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Shell>
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="bg-white rounded-lg shadow-sm p-6 mb-6">
|
||||
<h1 className="text-2xl font-semibold text-gray-800">Novo cadastro de produto</h1>
|
||||
<p className="text-gray-500 mt-1">
|
||||
O fluxo é dividido em duas etapas: catálogo, venda/estoque. Informe os dados obrigatórios em cada aba para concluir o processo.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form Card */}
|
||||
<div className="bg-white rounded-lg shadow-sm p-6">
|
||||
{/* Steps Indicator */}
|
||||
<div className="flex justify-center mb-8">
|
||||
<div className="flex items-center gap-16">
|
||||
{/* Step 1 */}
|
||||
<div className="flex flex-col items-center">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center text-white font-semibold ${step >= 1 ? 'bg-blue-600' : 'bg-gray-300'
|
||||
}`}>
|
||||
1
|
||||
</div>
|
||||
<p className={`mt-2 text-sm font-medium ${step >= 1 ? 'text-gray-800' : 'text-gray-400'}`}>
|
||||
Etapa 1
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">Dados do catálogo</p>
|
||||
</div>
|
||||
|
||||
{/* Progress Line */}
|
||||
<div className={`w-32 h-1 ${step === 2 ? 'bg-blue-600' : 'bg-gray-200'}`} />
|
||||
|
||||
{/* Step 2 */}
|
||||
<div className="flex flex-col items-center">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center text-white font-semibold ${step === 2 ? 'bg-blue-600' : 'bg-gray-300'
|
||||
}`}>
|
||||
2
|
||||
</div>
|
||||
<p className={`mt-2 text-sm font-medium ${step === 2 ? 'text-gray-800' : 'text-gray-400'}`}>
|
||||
Etapa 2
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">Estoque e Venda</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded text-red-700 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 1 Form */}
|
||||
{step === 1 && (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Código EAN/Barras <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Digite o código EAN"
|
||||
value={form.ean_code}
|
||||
onChange={(e) => updateField('ean_code', e.target.value)}
|
||||
className="w-full px-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Nome <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Digite o nome do produto (mín. 3 caracteres para sugestões)"
|
||||
value={form.name}
|
||||
onChange={(e) => updateField('name', e.target.value)}
|
||||
className="w-full px-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Descrição
|
||||
</label>
|
||||
<textarea
|
||||
placeholder="Detalhes do produto"
|
||||
value={form.description}
|
||||
onChange={(e) => updateField('description', e.target.value)}
|
||||
rows={3}
|
||||
className="w-full px-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Laboratório/Fabricante <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Digite o nome do laboratório"
|
||||
value={form.manufacturer}
|
||||
onChange={(e) => updateField('manufacturer', e.target.value)}
|
||||
className="w-full px-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Categoria <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
value={form.category}
|
||||
onChange={(e) => updateField('category', e.target.value)}
|
||||
className="w-full px-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white"
|
||||
>
|
||||
<option value="">Selecione uma categoria</option>
|
||||
{categories.map(cat => (
|
||||
<option key={cat} value={cat}>{cat}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Subcategoria
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Subcategoria (opcional)"
|
||||
value={form.subcategory}
|
||||
onChange={(e) => updateField('subcategory', e.target.value)}
|
||||
className="w-full px-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step 1 Actions */}
|
||||
<div className="flex justify-end gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClear}
|
||||
className="px-6 py-2.5 border border-gray-300 rounded-lg text-gray-700 font-medium hover:bg-gray-50"
|
||||
>
|
||||
Limpar
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNext}
|
||||
className="px-6 py-2.5 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700"
|
||||
>
|
||||
Avançar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2 Form */}
|
||||
{step === 2 && (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Quantidade em estoque <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Digite a quantidade disponível"
|
||||
value={form.stock}
|
||||
onChange={(e) => updateField('stock', e.target.value)}
|
||||
min="0"
|
||||
className="w-full px-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Preço de venda (R$) <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="0.00"
|
||||
value={form.price}
|
||||
onChange={(e) => updateField('price', e.target.value)}
|
||||
className="w-full px-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Data de validade <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={form.expires_at}
|
||||
onChange={(e) => updateField('expires_at', e.target.value)}
|
||||
className="w-full max-w-xs px-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Observações
|
||||
</label>
|
||||
<textarea
|
||||
placeholder="Informações adicionais sobre o produto"
|
||||
value={form.observations}
|
||||
onChange={(e) => updateField('observations', e.target.value)}
|
||||
rows={4}
|
||||
className="w-full px-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Step 2 Actions */}
|
||||
<div className="flex justify-between pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleBack}
|
||||
className="px-6 py-2.5 border border-gray-300 rounded-lg text-gray-700 font-medium hover:bg-gray-50"
|
||||
>
|
||||
Voltar
|
||||
</button>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClear}
|
||||
className="px-6 py-2.5 border border-gray-300 rounded-lg text-gray-700 font-medium hover:bg-gray-50"
|
||||
>
|
||||
Reiniciar
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
disabled={loading}
|
||||
className="px-6 py-2.5 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700 disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Salvando...' : 'Concluir cadastro'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Shell>
|
||||
)
|
||||
}
|
||||
|
|
@ -135,12 +135,17 @@ func SeedLean(dsn string) (string, error) {
|
|||
mustExec(db, `CREATE TABLE products (
|
||||
id UUID PRIMARY KEY,
|
||||
seller_id UUID NOT NULL REFERENCES companies(id),
|
||||
ean_code TEXT NOT NULL DEFAULT '',
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
manufacturer TEXT NOT NULL DEFAULT '',
|
||||
category TEXT NOT NULL DEFAULT '',
|
||||
subcategory TEXT NOT NULL DEFAULT '',
|
||||
batch TEXT NOT NULL,
|
||||
expires_at DATE NOT NULL,
|
||||
price_cents BIGINT NOT NULL,
|
||||
stock BIGINT NOT NULL,
|
||||
observations TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
)`)
|
||||
|
|
@ -300,8 +305,8 @@ func SeedLean(dsn string) (string, error) {
|
|||
prods := generateProducts(rng, companyID, numProds)
|
||||
for _, p := range prods {
|
||||
_, err := db.NamedExecContext(ctx, `
|
||||
INSERT INTO products (id, seller_id, name, description, batch, expires_at, price_cents, stock, created_at, updated_at)
|
||||
VALUES (:id, :seller_id, :name, :description, :batch, :expires_at, :price_cents, :stock, :created_at, :updated_at)
|
||||
INSERT INTO products (id, seller_id, ean_code, name, description, manufacturer, category, subcategory, batch, expires_at, price_cents, stock, observations, created_at, updated_at)
|
||||
VALUES (:id, :seller_id, :ean_code, :name, :description, :manufacturer, :category, :subcategory, :batch, :expires_at, :price_cents, :stock, :observations, :created_at, :updated_at)
|
||||
ON CONFLICT DO NOTHING`, p)
|
||||
if err != nil {
|
||||
log.Printf("insert product lean: %v", err)
|
||||
|
|
@ -480,12 +485,17 @@ func SeedFull(dsn string) (string, error) {
|
|||
mustExec(db, `CREATE TABLE products (
|
||||
id UUID PRIMARY KEY,
|
||||
seller_id UUID NOT NULL REFERENCES companies(id),
|
||||
ean_code TEXT NOT NULL DEFAULT '',
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
manufacturer TEXT NOT NULL DEFAULT '',
|
||||
category TEXT NOT NULL DEFAULT '',
|
||||
subcategory TEXT NOT NULL DEFAULT '',
|
||||
batch TEXT NOT NULL,
|
||||
expires_at DATE NOT NULL,
|
||||
price_cents BIGINT NOT NULL,
|
||||
stock BIGINT NOT NULL,
|
||||
observations TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
)`)
|
||||
|
|
@ -606,16 +616,21 @@ func generateProducts(rng *rand.Rand, sellerID uuid.UUID, count int) []map[strin
|
|||
price := int64(float64(med.BasePrice) * priceMultiplier)
|
||||
|
||||
products = append(products, map[string]interface{}{
|
||||
"id": id,
|
||||
"seller_id": sellerID,
|
||||
"name": med.Name,
|
||||
"description": fmt.Sprintf("%s - %s", med.Name, med.Category),
|
||||
"batch": fmt.Sprintf("L%d%03d", now.Year(), rng.Intn(999)),
|
||||
"expires_at": expiresAt,
|
||||
"price_cents": price,
|
||||
"stock": int64(10 + rng.Intn(500)),
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
"id": id,
|
||||
"seller_id": sellerID,
|
||||
"ean_code": fmt.Sprintf("789%010d", rng.Int63n(9999999999)),
|
||||
"name": med.Name,
|
||||
"description": fmt.Sprintf("%s - %s", med.Name, med.Category),
|
||||
"manufacturer": []string{"EMS", "Medley", "Neo Química", "Eurofarma", "Aché", "Sanofi"}[rng.Intn(6)],
|
||||
"category": med.Category,
|
||||
"subcategory": "",
|
||||
"batch": fmt.Sprintf("L%d%03d", now.Year(), rng.Intn(999)),
|
||||
"expires_at": expiresAt,
|
||||
"price_cents": price,
|
||||
"stock": int64(10 + rng.Intn(500)),
|
||||
"observations": "",
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
})
|
||||
}
|
||||
return products
|
||||
|
|
|
|||
Loading…
Reference in a new issue