diff --git a/marketplace/src/pages/Company.tsx b/marketplace/src/pages/Company.tsx index 2fd103c..aa23afd 100644 --- a/marketplace/src/pages/Company.tsx +++ b/marketplace/src/pages/Company.tsx @@ -1,15 +1,164 @@ -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { Shell } from '../layouts/Shell' import { apiClient } from '../services/apiClient' interface Company { id: string - role: string cnpj: string corporate_name: string + category: string license_number: string is_verified: boolean + city: string + state: string + latitude: number + longitude: number created_at: string + updated_at: string +} + +interface CompanyRating { + company_id: string + average_score: number + total_reviews: number +} + +interface User { + id: string + company_id: string + role: string + name: string + username: string + email: string + email_verified: boolean + created_at: string +} + +interface UserPage { + users: User[] + total: number + page: number + page_size: number +} + +type ShippingMethodType = 'pickup' | 'own_delivery' | 'third_party_delivery' + +interface ShippingMethod { + id: string + vendor_id: string + type: ShippingMethodType + active: boolean + preparation_minutes: number + max_radius_km: number + min_fee_cents: number + price_per_km_cents: number + free_shipping_threshold_cents?: number | null + pickup_address: string + pickup_hours: string + created_at: string + updated_at: string +} + +interface ShippingMethodDraft { + type: ShippingMethodType + active: boolean + preparation_minutes: number + max_radius_km: number + min_fee_reais: string + price_per_km_reais: string + free_shipping_threshold_reais: string + pickup_address: string + pickup_hours: string +} + +const tabs = [ + { id: 'overview', label: 'Dados da empresa' }, + { id: 'users', label: 'Usuários' }, + { id: 'shipping', label: 'Frete' } +] + +const categoryLabels: Record = { + farmacia: 'Farmácia', + distribuidora: 'Distribuidora', + admin: 'Administrador' +} + +const shippingLabels: Record = { + pickup: 'Retirada no local', + own_delivery: 'Entrega própria', + third_party_delivery: 'Entrega via terceiros' +} + +const formatDate = (value: string) => new Date(value).toLocaleDateString('pt-BR') + +const formatCurrency = (value: number) => + value.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }) + +const centsToReaisString = (value?: number | null) => + value ? (value / 100).toFixed(2) : '0.00' + +const parseNumber = (value: string) => { + const parsed = Number.parseFloat(value.replace(',', '.')) + return Number.isNaN(parsed) ? 0 : parsed +} + +const createDefaultShippingDraft = (): ShippingMethodDraft[] => [ + { + type: 'pickup', + active: false, + preparation_minutes: 30, + max_radius_km: 0, + min_fee_reais: '0.00', + price_per_km_reais: '0.00', + free_shipping_threshold_reais: '', + pickup_address: '', + pickup_hours: '' + }, + { + type: 'own_delivery', + active: false, + preparation_minutes: 45, + max_radius_km: 10, + min_fee_reais: '5.00', + price_per_km_reais: '1.50', + free_shipping_threshold_reais: '', + pickup_address: '', + pickup_hours: '' + }, + { + type: 'third_party_delivery', + active: false, + preparation_minutes: 60, + max_radius_km: 15, + min_fee_reais: '8.00', + price_per_km_reais: '2.00', + free_shipping_threshold_reais: '', + pickup_address: '', + pickup_hours: '' + } +] + +const mergeShippingDrafts = (methods: ShippingMethod[]) => { + const defaults = createDefaultShippingDraft() + return defaults.map((fallback) => { + const existing = methods.find((method) => method.type === fallback.type) + if (!existing) { + return fallback + } + return { + type: existing.type, + active: existing.active, + preparation_minutes: existing.preparation_minutes, + max_radius_km: existing.max_radius_km, + min_fee_reais: centsToReaisString(existing.min_fee_cents), + price_per_km_reais: centsToReaisString(existing.price_per_km_cents), + free_shipping_threshold_reais: existing.free_shipping_threshold_cents + ? centsToReaisString(existing.free_shipping_threshold_cents) + : '', + pickup_address: existing.pickup_address, + pickup_hours: existing.pickup_hours + } + }) } export function CompanyPage() { @@ -17,6 +166,18 @@ export function CompanyPage() { const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [editing, setEditing] = useState(false) + const [activeTab, setActiveTab] = useState('overview') + const [rating, setRating] = useState(null) + const [ratingError, setRatingError] = useState(null) + const [users, setUsers] = useState([]) + const [usersLoading, setUsersLoading] = useState(false) + const [usersError, setUsersError] = useState(null) + const [usersMeta, setUsersMeta] = useState({ total: 0, page: 1, page_size: 20 }) + const [shippingMethods, setShippingMethods] = useState([]) + const [shippingDraft, setShippingDraft] = useState(createDefaultShippingDraft()) + const [shippingLoading, setShippingLoading] = useState(false) + const [shippingError, setShippingError] = useState(null) + const [shippingEditing, setShippingEditing] = useState(false) const [form, setForm] = useState({ corporate_name: '', license_number: '' @@ -36,6 +197,9 @@ export function CompanyPage() { license_number: data.license_number }) setError(null) + void loadUsers(data.id) + void loadShipping(data.id) + void loadRating(data.id) } catch (err) { setError('Erro ao carregar dados da empresa') console.error(err) @@ -44,6 +208,54 @@ export function CompanyPage() { } } + const loadUsers = async (companyId: string) => { + try { + setUsersLoading(true) + const data = await apiClient.get(`/v1/users?page=1&page_size=50&company_id=${companyId}`) + setUsers(data.users || []) + setUsersMeta({ + total: data.total, + page: data.page, + page_size: data.page_size + }) + setUsersError(null) + } catch (err) { + console.error('Erro ao carregar usuários:', err) + setUsersError('Erro ao carregar usuários da empresa') + } finally { + setUsersLoading(false) + } + } + + const loadShipping = async (companyId: string) => { + try { + setShippingLoading(true) + const data = await apiClient.get(`/v1/shipping/settings/${companyId}`) + setShippingMethods(data || []) + setShippingDraft(mergeShippingDrafts(data || [])) + setShippingError(null) + } catch (err) { + console.error('Erro ao carregar frete:', err) + setShippingMethods([]) + setShippingDraft(createDefaultShippingDraft()) + setShippingError('Erro ao carregar configurações de frete') + } finally { + setShippingLoading(false) + } + } + + const loadRating = async (companyId: string) => { + try { + const data = await apiClient.get(`/v1/companies/${companyId}/rating`) + setRating(data) + setRatingError(null) + } catch (err) { + console.error('Erro ao carregar avaliação:', err) + setRating(null) + setRatingError('Erro ao carregar avaliação da empresa') + } + } + const saveChanges = async () => { if (!company) return try { @@ -55,18 +267,53 @@ export function CompanyPage() { } } - const roleLabels: Record = { - pharmacy: 'Farmácia', - distributor: 'Distribuidora', - admin: 'Administrador' + const saveShippingSettings = async () => { + if (!company) return + try { + setShippingLoading(true) + const payload = { + methods: shippingDraft.map((method) => ({ + type: method.type, + active: method.active, + preparation_minutes: method.preparation_minutes, + max_radius_km: method.max_radius_km, + min_fee_cents: Math.round(parseNumber(method.min_fee_reais) * 100), + price_per_km_cents: Math.round(parseNumber(method.price_per_km_reais) * 100), + free_shipping_threshold_cents: method.free_shipping_threshold_reais + ? Math.round(parseNumber(method.free_shipping_threshold_reais) * 100) + : undefined, + pickup_address: method.pickup_address, + pickup_hours: method.pickup_hours + })) + } + await apiClient.put(`/v1/shipping/settings/${company.id}`, payload) + setShippingEditing(false) + await loadShipping(company.id) + } catch (err) { + console.error('Erro ao salvar frete:', err) + setShippingError('Erro ao salvar configurações de frete') + } finally { + setShippingLoading(false) + } } + const shippingSummary = useMemo(() => { + if (!shippingMethods.length) { + return 'Nenhuma configuração ativa.' + } + const actives = shippingMethods.filter((method) => method.active) + if (!actives.length) { + return 'Configurações cadastradas, mas sem métodos ativos.' + } + return `${actives.length} método(s) ativo(s)` + }, [shippingMethods]) + return (

Minha Empresa

- {company && !editing && ( + {company && !editing && activeTab === 'overview' && (
+
+ {tabs.map((tab) => ( + + ))} +
+ {loading && (
@@ -86,7 +348,7 @@ export function CompanyPage() {
{error}
)} - {company && !editing && ( + {company && activeTab === 'overview' && !editing && (
Status @@ -100,7 +362,7 @@ export function CompanyPage() { )}
-
+

Razão Social

{company.corporate_name}

@@ -110,24 +372,63 @@ export function CompanyPage() {

{company.cnpj}

-

Tipo

-

{roleLabels[company.role] || company.role}

+

Categoria

+

+ {categoryLabels[company.category] || company.category} +

Licença Sanitária

{company.license_number}

-

Cadastro

+

Localização

- {new Date(company.created_at).toLocaleDateString('pt-BR')} + {company.city}/{company.state}

+
+

Coordenadas

+

+ {company.latitude.toFixed(4)}, {company.longitude.toFixed(4)} +

+
+
+

Cadastro

+

+ {formatDate(company.created_at)} +

+
+
+

Atualização

+

+ {formatDate(company.updated_at)} +

+
+
+
+
+
+

Avaliação

+

+ {rating ? rating.average_score.toFixed(1) : 'Sem avaliação'} +

+
+
+

Total de reviews

+

+ {rating ? rating.total_reviews : 0} +

+
+
+ {ratingError && ( +

{ratingError}

+ )}
)} - {company && editing && ( + {company && activeTab === 'overview' && editing && (
)} + + {activeTab === 'users' && ( +
+
+
+

Usuários da empresa

+

+ {usersMeta.total} usuário(s) vinculados. +

+
+ +
+ {usersLoading ? ( +
Carregando usuários...
+ ) : usersError ? ( +
{usersError}
+ ) : users.length === 0 ? ( +
+ Nenhum usuário associado a esta empresa. +
+ ) : ( +
+ + + + + + + + + + + {users.map((user) => ( + + + + + + + ))} + +
NomeEmailPerfilStatus
+
{user.name}
+
@{user.username}
+
{user.email}{user.role} + + {user.email_verified ? 'Verificado' : 'Pendente'} + +
+
+ )} +
+ )} + + {activeTab === 'shipping' && ( +
+
+
+

Configuração de frete

+

{shippingSummary}

+
+
+ {!shippingEditing && ( + + )} + {shippingEditing && ( + <> + + + + )} +
+
+ + {shippingLoading && ( +
Carregando frete...
+ )} + + {shippingError && ( +
{shippingError}
+ )} + +
+ {(shippingEditing ? shippingDraft : mergeShippingDrafts(shippingMethods)).map((method, index) => ( +
+
+

+ {shippingLabels[method.type]} +

+ {shippingEditing ? ( + + ) : ( + + {method.active ? 'Ativo' : 'Inativo'} + + )} +
+ +
+
+

Tempo de preparo (min)

+ {shippingEditing ? ( + { + const updated = [...shippingDraft] + updated[index] = { + ...method, + preparation_minutes: Number(e.target.value) + } + setShippingDraft(updated) + }} + className="mt-1 w-full rounded border border-gray-200 px-3 py-2 text-sm" + /> + ) : ( +

{method.preparation_minutes} min

+ )} +
+
+

Raio máximo (km)

+ {shippingEditing ? ( + { + const updated = [...shippingDraft] + updated[index] = { + ...method, + max_radius_km: Number(e.target.value) + } + setShippingDraft(updated) + }} + className="mt-1 w-full rounded border border-gray-200 px-3 py-2 text-sm" + /> + ) : ( +

{method.max_radius_km} km

+ )} +
+
+

Taxa mínima (R$)

+ {shippingEditing ? ( + { + const updated = [...shippingDraft] + updated[index] = { + ...method, + min_fee_reais: e.target.value + } + setShippingDraft(updated) + }} + className="mt-1 w-full rounded border border-gray-200 px-3 py-2 text-sm" + /> + ) : ( +

+ {formatCurrency(parseNumber(method.min_fee_reais))} +

+ )} +
+
+

Preço por km (R$)

+ {shippingEditing ? ( + { + const updated = [...shippingDraft] + updated[index] = { + ...method, + price_per_km_reais: e.target.value + } + setShippingDraft(updated) + }} + className="mt-1 w-full rounded border border-gray-200 px-3 py-2 text-sm" + /> + ) : ( +

+ {formatCurrency(parseNumber(method.price_per_km_reais))} +

+ )} +
+
+

Frete grátis acima de (R$)

+ {shippingEditing ? ( + { + const updated = [...shippingDraft] + updated[index] = { + ...method, + free_shipping_threshold_reais: e.target.value + } + setShippingDraft(updated) + }} + className="mt-1 w-full rounded border border-gray-200 px-3 py-2 text-sm" + placeholder="Deixe em branco se não aplica" + /> + ) : ( +

+ {method.free_shipping_threshold_reais + ? formatCurrency(parseNumber(method.free_shipping_threshold_reais)) + : 'Não aplica'} +

+ )} +
+
+

Endereço de retirada

+ {shippingEditing ? ( + { + const updated = [...shippingDraft] + updated[index] = { + ...method, + pickup_address: e.target.value + } + setShippingDraft(updated) + }} + className="mt-1 w-full rounded border border-gray-200 px-3 py-2 text-sm" + placeholder="Informe o endereço para retirada" + /> + ) : ( +

+ {method.pickup_address || 'Não informado'} +

+ )} +
+
+

Horário de retirada

+ {shippingEditing ? ( + { + const updated = [...shippingDraft] + updated[index] = { + ...method, + pickup_hours: e.target.value + } + setShippingDraft(updated) + }} + className="mt-1 w-full rounded border border-gray-200 px-3 py-2 text-sm" + placeholder="Ex: Seg-Sex 08:00 - 18:00" + /> + ) : ( +

+ {method.pickup_hours || 'Não informado'} +

+ )} +
+
+
+ ))} +
+
+ )}
)