feat(marketplace): add company management and user editing features

Backend:
- Add phone, operating_hours, is_24_hours fields to Tenant model
- Create migration 0005_tenants_operating_hours.sql for new columns
- Update postgres repository queries for new fields

Frontend Company.tsx:
- Expand company edit form with phone, city, state
- Add operating hours section with 24h toggle
- Add user edit/delete buttons with Actions column
- Add user edit modal with name, email, role fields
- Add handleDeleteUser and handleSaveUser functions
This commit is contained in:
Tiago Yamamoto 2025-12-23 16:44:51 -03:00
parent 352ef86617
commit 8d4731268e
4 changed files with 240 additions and 32 deletions

View file

@ -15,10 +15,15 @@ type Tenant struct {
LicenseNumber string `db:"license_number" json:"license_number"`
IsVerified bool `db:"is_verified" json:"is_verified"`
// Location
Latitude float64 `db:"latitude" json:"latitude"`
Longitude float64 `db:"longitude" json:"longitude"`
City string `db:"city" json:"city"`
State string `db:"state" json:"state"`
Latitude float64 `db:"latitude" json:"latitude"`
Longitude float64 `db:"longitude" json:"longitude"`
City string `db:"city" json:"city"`
State string `db:"state" json:"state"`
// Contact & Hours
Phone string `db:"phone" json:"phone"`
OperatingHours string `db:"operating_hours" json:"operating_hours"` // e.g. "Seg-Sex: 08:00-18:00, Sab: 08:00-12:00"
Is24Hours bool `db:"is_24_hours" json:"is_24_hours"`
// Timestamps
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}

View file

@ -0,0 +1,9 @@
-- +goose Up
ALTER TABLE companies ADD COLUMN phone TEXT NOT NULL DEFAULT '';
ALTER TABLE companies ADD COLUMN operating_hours TEXT NOT NULL DEFAULT '';
ALTER TABLE companies ADD COLUMN is_24_hours BOOLEAN NOT NULL DEFAULT false;
-- +goose Down
ALTER TABLE companies DROP COLUMN phone;
ALTER TABLE companies DROP COLUMN operating_hours;
ALTER TABLE companies DROP COLUMN is_24_hours;

View file

@ -29,8 +29,8 @@ func (r *Repository) CreateCompany(ctx context.Context, company *domain.Company)
company.CreatedAt = now
company.UpdatedAt = now
query := `INSERT INTO companies (id, cnpj, corporate_name, category, license_number, is_verified, latitude, longitude, city, state, created_at, updated_at)
VALUES (:id, :cnpj, :corporate_name, :category, :license_number, :is_verified, :latitude, :longitude, :city, :state, :created_at, :updated_at)`
query := `INSERT INTO companies (id, cnpj, corporate_name, category, license_number, is_verified, latitude, longitude, city, state, phone, operating_hours, is_24_hours, created_at, updated_at)
VALUES (:id, :cnpj, :corporate_name, :category, :license_number, :is_verified, :latitude, :longitude, :city, :state, :phone, :operating_hours, :is_24_hours, :created_at, :updated_at)`
_, err := r.db.NamedExecContext(ctx, query, company)
return err
@ -72,7 +72,7 @@ func (r *Repository) ListCompanies(ctx context.Context, filter domain.CompanyFil
filter.Limit = 20
}
args = append(args, filter.Limit, filter.Offset)
listQuery := fmt.Sprintf("SELECT id, cnpj, corporate_name, category, license_number, is_verified, latitude, longitude, city, state, 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, cnpj, corporate_name, category, license_number, is_verified, latitude, longitude, city, state, phone, operating_hours, is_24_hours, created_at, updated_at %s%s ORDER BY created_at DESC LIMIT $%d OFFSET $%d", baseQuery, where, len(args)-1, len(args))
var companies []domain.Company
if err := r.db.SelectContext(ctx, &companies, listQuery, args...); err != nil {
@ -83,7 +83,7 @@ func (r *Repository) ListCompanies(ctx context.Context, filter domain.CompanyFil
func (r *Repository) GetCompany(ctx context.Context, id uuid.UUID) (*domain.Company, error) {
var company domain.Company
query := `SELECT id, cnpj, corporate_name, category, license_number, is_verified, latitude, longitude, city, state, created_at, updated_at FROM companies WHERE id = $1`
query := `SELECT id, cnpj, corporate_name, category, license_number, is_verified, latitude, longitude, city, state, phone, operating_hours, is_24_hours, created_at, updated_at FROM companies WHERE id = $1`
if err := r.db.GetContext(ctx, &company, query, id); err != nil {
return nil, err
}
@ -94,7 +94,7 @@ func (r *Repository) UpdateCompany(ctx context.Context, company *domain.Company)
company.UpdatedAt = time.Now().UTC()
query := `UPDATE companies
SET cnpj = :cnpj, corporate_name = :corporate_name, category = :category, license_number = :license_number, is_verified = :is_verified, latitude = :latitude, longitude = :longitude, city = :city, state = :state, updated_at = :updated_at
SET cnpj = :cnpj, corporate_name = :corporate_name, category = :category, license_number = :license_number, is_verified = :is_verified, latitude = :latitude, longitude = :longitude, city = :city, state = :state, phone = :phone, operating_hours = :operating_hours, is_24_hours = :is_24_hours, updated_at = :updated_at
WHERE id = :id`
res, err := r.db.NamedExecContext(ctx, query, company)

View file

@ -13,6 +13,9 @@ interface Company {
state: string
latitude: number
longitude: number
phone: string
operating_hours: string
is_24_hours: boolean
created_at: string
updated_at: string
}
@ -180,8 +183,43 @@ export function CompanyPage() {
const [shippingEditing, setShippingEditing] = useState(false)
const [form, setForm] = useState({
corporate_name: '',
license_number: ''
license_number: '',
phone: '',
operating_hours: '',
is_24_hours: false,
city: '',
state: ''
})
// User editing state
const [editingUser, setEditingUser] = useState<User | null>(null)
const [userForm, setUserForm] = useState({ name: '', email: '', role: '' })
const handleDeleteUser = async (userId: string) => {
if (!confirm('Tem certeza que deseja excluir este usuário?')) return
try {
await apiClient.delete(`/v1/users/${userId}`)
if (company) void loadUsers(company.id)
} catch (err) {
console.error('Erro ao excluir usuário:', err)
setUsersError('Erro ao excluir usuário')
}
}
const handleSaveUser = async () => {
if (!editingUser) return
try {
await apiClient.patch(`/v1/users/${editingUser.id}`, {
name: userForm.name,
email: userForm.email,
role: userForm.role
})
setEditingUser(null)
if (company) void loadUsers(company.id)
} catch (err) {
console.error('Erro ao salvar usuário:', err)
setUsersError('Erro ao salvar usuário')
}
}
useEffect(() => {
loadCompany()
@ -194,7 +232,12 @@ export function CompanyPage() {
setCompany(data)
setForm({
corporate_name: data.corporate_name,
license_number: data.license_number
license_number: data.license_number,
phone: data.phone || '',
operating_hours: data.operating_hours || '',
is_24_hours: data.is_24_hours || false,
city: data.city,
state: data.state
})
setError(null)
void loadUsers(data.id)
@ -457,29 +500,99 @@ export function CompanyPage() {
{company && activeTab === 'overview' && editing && (
<div className="rounded-lg bg-white p-6 shadow-sm space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Razão Social
</label>
<input
type="text"
value={form.corporate_name}
onChange={(e) => setForm({ ...form, corporate_name: e.target.value })}
className="w-full rounded border border-gray-200 px-3 py-2"
/>
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Razão Social
</label>
<input
type="text"
value={form.corporate_name}
onChange={(e) => setForm({ ...form, corporate_name: e.target.value })}
className="w-full rounded border border-gray-200 px-3 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Licença Sanitária
</label>
<input
type="text"
value={form.license_number}
onChange={(e) => setForm({ ...form, license_number: e.target.value })}
className="w-full rounded border border-gray-200 px-3 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Telefone
</label>
<input
type="tel"
value={form.phone}
onChange={(e) => setForm({ ...form, phone: e.target.value })}
className="w-full rounded border border-gray-200 px-3 py-2"
placeholder="(00) 00000-0000"
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Cidade
</label>
<input
type="text"
value={form.city}
onChange={(e) => setForm({ ...form, city: e.target.value })}
className="w-full rounded border border-gray-200 px-3 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Estado
</label>
<input
type="text"
value={form.state}
onChange={(e) => setForm({ ...form, state: e.target.value })}
className="w-full rounded border border-gray-200 px-3 py-2"
maxLength={2}
placeholder="GO"
/>
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Licença Sanitária
</label>
<input
type="text"
value={form.license_number}
onChange={(e) => setForm({ ...form, license_number: e.target.value })}
className="w-full rounded border border-gray-200 px-3 py-2"
/>
<div className="border-t pt-4">
<h3 className="text-sm font-semibold text-gray-700 mb-3">Horário de Funcionamento</h3>
<div className="space-y-3">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={form.is_24_hours}
onChange={(e) => setForm({ ...form, is_24_hours: e.target.checked })}
className="rounded border-gray-300"
/>
<span className="text-sm text-gray-700">Funciona 24 horas</span>
</label>
{!form.is_24_hours && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Horários
</label>
<textarea
value={form.operating_hours}
onChange={(e) => setForm({ ...form, operating_hours: e.target.value })}
className="w-full rounded border border-gray-200 px-3 py-2 text-sm"
rows={3}
placeholder="Seg-Sex: 08:00 - 18:00&#10;Sáb: 08:00 - 12:00&#10;Dom: Fechado"
/>
</div>
)}
</div>
</div>
<div className="flex gap-3">
<div className="flex gap-3 pt-2">
<button
onClick={saveChanges}
className="rounded bg-healthGreen px-4 py-2 text-sm font-semibold text-white"
@ -529,6 +642,7 @@ export function CompanyPage() {
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-500">Email</th>
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-500">Perfil</th>
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-500">Status</th>
<th className="px-4 py-2 text-right text-xs font-semibold text-gray-500">Ações</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 bg-white">
@ -550,6 +664,23 @@ export function CompanyPage() {
{user.email_verified ? 'Verificado' : 'Pendente'}
</span>
</td>
<td className="px-4 py-3 text-sm text-right">
<button
onClick={() => {
setEditingUser(user)
setUserForm({ name: user.name, email: user.email, role: user.role })
}}
className="text-medicalBlue hover:underline text-xs font-medium mr-2"
>
Editar
</button>
<button
onClick={() => handleDeleteUser(user.id)}
className="text-red-600 hover:underline text-xs font-medium"
>
Excluir
</button>
</td>
</tr>
))}
</tbody>
@ -800,6 +931,69 @@ export function CompanyPage() {
</div>
)}
</div>
{/* User Edit Modal */}
{editingUser && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4" onClick={() => setEditingUser(null)}>
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6 space-y-4" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900">Editar Usuário</h2>
<button onClick={() => setEditingUser(null)} className="text-gray-400 hover:text-gray-600">
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Nome</label>
<input
type="text"
value={userForm.name}
onChange={(e) => setUserForm({ ...userForm, name: e.target.value })}
className="w-full rounded border border-gray-200 px-3 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Email</label>
<input
type="email"
value={userForm.email}
onChange={(e) => setUserForm({ ...userForm, email: e.target.value })}
className="w-full rounded border border-gray-200 px-3 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Perfil</label>
<select
value={userForm.role}
onChange={(e) => setUserForm({ ...userForm, role: e.target.value })}
className="w-full rounded border border-gray-200 px-3 py-2"
>
<option value="Dono">Dono</option>
<option value="Colaborador">Colaborador</option>
<option value="Entregador">Entregador</option>
<option value="Admin">Admin</option>
</select>
</div>
</div>
<div className="flex gap-3 pt-2">
<button
onClick={handleSaveUser}
className="rounded bg-healthGreen px-4 py-2 text-sm font-semibold text-white"
>
Salvar
</button>
<button
onClick={() => setEditingUser(null)}
className="rounded bg-gray-200 px-4 py-2 text-sm font-semibold text-gray-700"
>
Cancelar
</button>
</div>
</div>
</div>
)}
</Shell>
)
}