Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b5d093d2ce | |||
| 489d2753ee |
@@ -0,0 +1,45 @@
|
|||||||
|
# Marketplace transacional de Prótese
|
||||||
|
|
||||||
|
> Desenho (24/jun/2026). O último grande item do backlog do Provedor de Prótese.
|
||||||
|
> Princípio: **transformar o diretório (descoberta) em jornada de compra** — descobrir → ver
|
||||||
|
> serviços/preços → solicitar → cotar → contratar → vira OS. Reaproveita ao máximo o que já existe.
|
||||||
|
|
||||||
|
## O que já existe (não recriar)
|
||||||
|
- **Descoberta:** `/api/profissionais/marketplace` (busca por proximidade/especialidade) **+ Nota
|
||||||
|
ScoreOdonto** no card (feito).
|
||||||
|
- **Catálogo:** `protese_produtos` (nome, preço, garantia) + `GET /api/protese/laboratorios/:id/produtos`
|
||||||
|
— já acessível a quem está no **diretório** (`proteticoAcessivelPelaClinica` aceita `disponivel_diretorio`).
|
||||||
|
- **Propostas:** `propostas_profissional` (clínica→profissional: mensagem, `pendente→aceita/recusada`).
|
||||||
|
- **Contratação:** a clínica **já cria OS para qualquer protético do diretório** (Nova OS).
|
||||||
|
- **Pós-venda:** OS completa (etapas, custódia, ocorrências), financeiro, score — tudo pronto.
|
||||||
|
|
||||||
|
**Conclusão:** o "transacional" não exige nova fundação — falta **costurar a jornada** dentro do
|
||||||
|
marketplace (hoje a clínica vê nome/nota/distância, mas não os serviços, e contrata por outra tela).
|
||||||
|
|
||||||
|
## Fatias
|
||||||
|
|
||||||
|
### ✅ Fatia 1 — Vitrine (catálogo no marketplace) — IMPLEMENTADA
|
||||||
|
No card do protético no marketplace, **"Ver catálogo"** expande os produtos (nome, preço, garantia)
|
||||||
|
via `getLaboratorioProdutos(id)` (reuso — zero endpoint novo). A clínica passa a **decidir vendo
|
||||||
|
serviços + preço + garantia + ★nota** antes de contratar. Botão de contato vira **"Solicitar
|
||||||
|
prótese"** (proposta com contexto de prótese).
|
||||||
|
|
||||||
|
### Fatia 2 — Solicitação → OS direto
|
||||||
|
"Solicitar prótese" abre um fluxo enxuto (produto do catálogo + paciente + dentes + prazo + obs) que
|
||||||
|
**cria a OS** já com o lab selecionado (reusa `POST /api/protese/os`). Requer extrair o seletor de
|
||||||
|
paciente/dentista da Nova OS para um componente reutilizável fora da tela de Prótese.
|
||||||
|
|
||||||
|
### Fatia 3 — Cotação (RFQ) para trabalho sob medida
|
||||||
|
Quando não há produto de catálogo: a clínica descreve o trabalho e **N labs cotam** (preço + prazo);
|
||||||
|
a clínica escolhe e vira OS. Estende `propostas_profissional` com itens de prótese e valores, ou nova
|
||||||
|
`protese_rfq` + `protese_rfq_cotacao`.
|
||||||
|
|
||||||
|
### Fatia 4 — Reputação pública e conversão
|
||||||
|
Avaliação da clínica ao final da OS (alimenta a Nota, hoje só operacional), selo "lab verificado",
|
||||||
|
ordenar marketplace por nota dentro do raio.
|
||||||
|
|
||||||
|
## Decisões de modelagem
|
||||||
|
- **Catálogo público = catálogo atual** (`protese_produtos`); a vitrine só o exibe — sem duplicar.
|
||||||
|
- **Solicitação reusa a OS** (não cria entidade paralela): toda contratação termina em `protese_os`,
|
||||||
|
preservando rastreabilidade/financeiro/score.
|
||||||
|
- **Cotação (Fatia 3)** é o único caso que precisa de entidade nova (pré-OS multi-lab).
|
||||||
+3
-3
@@ -9,7 +9,7 @@
|
|||||||
### 1. Onde está o código?
|
### 1. Onde está o código?
|
||||||
- **Fonte da verdade:** Gitea na **VPS3**, repo `ruicesar/scoreodonto.com`.
|
- **Fonte da verdade:** Gitea na **VPS3**, repo `ruicesar/scoreodonto.com`.
|
||||||
- **Dev (trabalho):** VPS4 em `/home/deploy/stack/scoreodonto.com`.
|
- **Dev (trabalho):** VPS4 em `/home/deploy/stack/scoreodonto.com`.
|
||||||
- **Produção:** VPS1 roda **imagens versionadas do registry** (não código-fonte). No ar: **v1.0.25 · commit `8040e70`**.
|
- **Produção:** VPS1 roda **imagens versionadas do registry** (não código-fonte). No ar: **v1.0.26 · commit `d357f3a`**.
|
||||||
- **Documentação:** centralizada em `scoreodonto.com/doc/` (pasta única).
|
- **Documentação:** centralizada em `scoreodonto.com/doc/` (pasta única).
|
||||||
|
|
||||||
### 2. Onde está o banco?
|
### 2. Onde está o banco?
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
- ✅ **Build Once → Promote (Passo 5, validado em produção):** push builda 1× e publica por **commit** (`:<sha>`); a **tag re-tagueia** esse artefato como `:vX.Y.Z` (sem rebuild — digest idêntico) e deploya. Provado em v1.0.16 (`:v1.0.16` == `:471c2c2`, mesmo digest).
|
- ✅ **Build Once → Promote (Passo 5, validado em produção):** push builda 1× e publica por **commit** (`:<sha>`); a **tag re-tagueia** esse artefato como `:vX.Y.Z` (sem rebuild — digest idêntico) e deploya. Provado em v1.0.16 (`:v1.0.16` == `:471c2c2`, mesmo digest).
|
||||||
|
|
||||||
### 5. Como versiona?
|
### 5. Como versiona?
|
||||||
- A versão exibida vem do **backend em runtime**: `GET /api/version` (de `process.env`), mostrada em **Configurações → "Sobre o sistema"** e nas telas públicas (o frontend consome via `useAppVersion()`, não embute mais a versão). No ar: **v1.0.25 · `8040e70` · PROD**.
|
- A versão exibida vem do **backend em runtime**: `GET /api/version` (de `process.env`), mostrada em **Configurações → "Sobre o sistema"** e nas telas públicas (o frontend consome via `useAppVersion()`, não embute mais a versão). No ar: **v1.0.26 · `d357f3a` · PROD**.
|
||||||
- A versão **nasce da tag git** e é **injetada no deploy em runtime** (`APP_VERSION`=tag, `APP_ENV=PROD`); `commit`/`build` vêm carimbados na imagem.
|
- A versão **nasce da tag git** e é **injetada no deploy em runtime** (`APP_VERSION`=tag, `APP_ENV=PROD`); `commit`/`build` vêm carimbados na imagem.
|
||||||
- ✅ **`constants.ts`/`update-version.js` foram removidos** (Passo 5) — não há mais versão manual.
|
- ✅ **`constants.ts`/`update-version.js` foram removidos** (Passo 5) — não há mais versão manual.
|
||||||
|
|
||||||
@@ -39,4 +39,4 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
## Veredito de uma linha
|
## Veredito de uma linha
|
||||||
> O código vive no Gitea (VPS3); a **produção (VPS1) roda imagens versionadas do registry** (hoje **v1.0.25**), sem buildar; o **DEV está isolado no `pgdev`** (não toca o banco de produção). **CI/CD por tag ativo** no modelo **Build Once → Promote → Deploy** (push builda por commit; tag re-tagueia sem rebuild e deploya — validado em v1.0.16), com **versão visível em runtime** (`/api/version` → Configurações) nascendo da tag git.
|
> O código vive no Gitea (VPS3); a **produção (VPS1) roda imagens versionadas do registry** (hoje **v1.0.26**), sem buildar; o **DEV está isolado no `pgdev`** (não toca o banco de produção). **CI/CD por tag ativo** no modelo **Build Once → Promote → Deploy** (push builda por commit; tag re-tagueia sem rebuild e deploya — validado em v1.0.16), com **versão visível em runtime** (`/api/version` → Configurações) nascendo da tag git.
|
||||||
|
|||||||
@@ -2,8 +2,44 @@ import React, { useState, useEffect, useCallback } from 'react';
|
|||||||
import {
|
import {
|
||||||
UserSearch, Stethoscope, Wrench, Sparkles, MapPin, Search, Mail, Phone, RefreshCw,
|
UserSearch, Stethoscope, Wrench, Sparkles, MapPin, Search, Mail, Phone, RefreshCw,
|
||||||
Save, BadgeCheck, Inbox, Send, CheckCircle2, XCircle, CircleDollarSign, UserCircle2, X,
|
Save, BadgeCheck, Inbox, Send, CheckCircle2, XCircle, CircleDollarSign, UserCircle2, X,
|
||||||
ChevronUp, Eraser, FileText, Plus, Pencil, Trash2
|
ChevronUp, Eraser, FileText, Plus, Pencil, Trash2, Tags, ChevronDown
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
const fmtBRLp = (v: number) => 'R$ ' + Number(v || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 });
|
||||||
|
|
||||||
|
// Fatia 1 do marketplace transacional: vitrine do catálogo do laboratório (reusa getLaboratorioProdutos).
|
||||||
|
const VitrineProtetico: React.FC<{ proteticoId: string }> = ({ proteticoId }) => {
|
||||||
|
const [aberto, setAberto] = useState(false);
|
||||||
|
const [produtos, setProdutos] = useState<any[] | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const toggle = async () => {
|
||||||
|
if (!aberto && produtos === null) {
|
||||||
|
setLoading(true);
|
||||||
|
try { const p = await HybridBackend.getLaboratorioProdutos(proteticoId); setProdutos(Array.isArray(p) ? p : []); }
|
||||||
|
catch { setProdutos([]); } finally { setLoading(false); }
|
||||||
|
}
|
||||||
|
setAberto(a => !a);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="mb-2">
|
||||||
|
<button onClick={toggle} className="w-full py-2 rounded-xl border border-gray-200 text-gray-600 text-[10px] font-black uppercase flex items-center justify-center gap-1.5 hover:bg-gray-50">
|
||||||
|
<Tags size={12} /> {aberto ? 'Ocultar catálogo' : 'Ver catálogo de próteses'} {aberto ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
|
||||||
|
</button>
|
||||||
|
{aberto && (
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
{loading ? <p className="text-[10px] text-gray-300 uppercase font-bold text-center py-2">Carregando…</p>
|
||||||
|
: produtos && produtos.length ? produtos.map(pr => (
|
||||||
|
<div key={pr.id} className="flex items-center gap-2 text-xs bg-gray-50 rounded-lg px-3 py-1.5">
|
||||||
|
<span className="font-bold text-gray-700 flex-1 truncate">{pr.nome}</span>
|
||||||
|
{pr.garantia_meses ? <span className="text-[9px] text-emerald-600 font-black uppercase">{pr.garantia_meses}m gar.</span> : null}
|
||||||
|
<span className="font-black text-gray-800">{fmtBRLp(pr.preco)}</span>
|
||||||
|
</div>
|
||||||
|
)) : <p className="text-[10px] text-gray-300 uppercase font-bold text-center py-2">Sem catálogo publicado.</p>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
import { PageHeader } from '../../components/PageHeader.tsx';
|
import { PageHeader } from '../../components/PageHeader.tsx';
|
||||||
import { useToast } from '../../contexts/ToastContext.tsx';
|
import { useToast } from '../../contexts/ToastContext.tsx';
|
||||||
import { useConfirm } from '../../contexts/ConfirmContext.tsx';
|
import { useConfirm } from '../../contexts/ConfirmContext.tsx';
|
||||||
@@ -723,10 +759,11 @@ export const ProfissionaisPlugin: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{p.role === 'protetico' && <VitrineProtetico proteticoId={p.id} />}
|
||||||
{ws?.id && (
|
{ws?.id && (
|
||||||
<button onClick={() => setProposingTo(p)}
|
<button onClick={() => setProposingTo(p)}
|
||||||
className="w-full py-2.5 rounded-xl text-white text-[10px] font-black uppercase flex items-center justify-center gap-1.5 transition-colors hover:opacity-90" style={{ backgroundColor: COLOR }}>
|
className="w-full py-2.5 rounded-xl text-white text-[10px] font-black uppercase flex items-center justify-center gap-1.5 transition-colors hover:opacity-90" style={{ backgroundColor: COLOR }}>
|
||||||
<Send size={13} /> ENVIAR PROPOSTA
|
<Send size={13} /> {p.role === 'protetico' ? 'SOLICITAR PRÓTESE' : 'ENVIAR PROPOSTA'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user