Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
fa46323
feat(posts): Phase 2 frontend — typed create flow + posts-backed mark…
NetworkTheoryAppliedResearchInstitute Jun 30, 2026
e3a2fa3
feat(posts): add Plans + Agrotourism post types (Phase 2 complete)
NetworkTheoryAppliedResearchInstitute Jun 30, 2026
0e72fbc
feat(lbtas): Phase 3 frontend — rating prompt + distribution display
NetworkTheoryAppliedResearchInstitute Jun 30, 2026
22d0baa
feat(escrow): Phase 4 frontend — buyer rating confirms receipt + rele…
NetworkTheoryAppliedResearchInstitute Jun 30, 2026
871f7d3
feat(escrow): value-aware RatePrompt for the -1 dispute freeze
NetworkTheoryAppliedResearchInstitute Jun 30, 2026
f9079f6
feat(lbtas): render reputation as separate distributions per role
NetworkTheoryAppliedResearchInstitute Jun 30, 2026
5a997f6
feat(admin): dispute resolution console with rating void/replace
NetworkTheoryAppliedResearchInstitute Jun 30, 2026
55d1b1b
feat(contracts): plan-aware ListingDetail — contract / fulfill action
NetworkTheoryAppliedResearchInstitute Jun 30, 2026
1abae75
feat(contracts): ContractModal — PING timeline + post-update + sell/t…
NetworkTheoryAppliedResearchInstitute Jun 30, 2026
8bbb917
feat(messaging+edit): Message button on posts + editable posts
NetworkTheoryAppliedResearchInstitute Jun 30, 2026
f4bd890
feat(contracts): plan allocation display + PING calendar
NetworkTheoryAppliedResearchInstitute Jun 30, 2026
cf093a4
feat(escrow): maturity-hold settlement UI
NetworkTheoryAppliedResearchInstitute Jun 30, 2026
400ab3e
feat(escrow): progress-payment tranches UI
NetworkTheoryAppliedResearchInstitute Jun 30, 2026
0716c57
fix(lbtas): P3-011 v2 audit — surface dismissals, contest flags, gove…
NetworkTheoryAppliedResearchInstitute Jun 30, 2026
2e3619e
feat(perfil): JFA §7.3 UI conformance — retire wallet top-up, show fee
NetworkTheoryAppliedResearchInstitute Jul 1, 2026
cde8f5e
fix(i18n): drop duplicate 'Reembolsado' key (already defined)
NetworkTheoryAppliedResearchInstitute Jul 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions app/admin/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { formatCurrency, formatDate } from '@/lib/format';
import { StatusBadge, Badge } from '@/components/ui/Badge';
import { useToast } from '@/components/ui/Toast';
import AnimatedStat from '@/components/motion/AnimatedStat';
import DisputeResolveModal from '@/components/admin/DisputeResolveModal';
import { useI18n } from '@/lib/i18n';

const STAT_CONFIG = [
Expand All @@ -32,6 +33,7 @@ export default function Admin() {
const [stats, setStats] = useState({});
const [tab, setTab] = useState('users');
const [data, setData] = useState(null);
const [resolveTarget, setResolveTarget] = useState(null);

useEffect(() => {
const u = getUser();
Expand Down Expand Up @@ -187,15 +189,12 @@ export default function Admin() {
{data.map(d => (
<tr key={d.id}>
<td className="text-xs text-text3 font-mono">{d.id?.substring(0,8)}…</td>
<td>{d.reason}</td>
<td className="max-w-[320px] truncate">{d.reason}</td>
<td><StatusBadge status={d.status} /></td>
<td>
{d.status === 'open' && (
<div className="flex gap-1.5">
<button className="btn btn-primary btn-sm" onClick={() => action(`/admin/disputes/${d.id}/resolve`,'POST',{resolution:'release'},'Liberado')}>{t('Liberar')}</button>
<button className="btn btn-danger btn-sm" onClick={() => action(`/admin/disputes/${d.id}/resolve`,'POST',{resolution:'refund'},'Reembolsado')}>{t('Reembolsar')}</button>
</div>
)}
{d.status === 'open'
? <button className="btn btn-primary btn-sm" onClick={() => setResolveTarget(d)}>{t('Resolver')}</button>
: <span className="text-xs text-text3">{d.resolution || d.status}</span>}
</td>
</tr>
))}
Expand Down Expand Up @@ -248,6 +247,14 @@ export default function Admin() {
</motion.div>
</AnimatePresence>
</div>

{resolveTarget && (
<DisputeResolveModal
dispute={resolveTarget}
onClose={() => setResolveTarget(null)}
onResolved={() => { loadTab('disputes'); loadStats(); }}
/>
)}
</div>
);
}
122 changes: 105 additions & 17 deletions app/contracts/page.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,135 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { api } from '@/lib/api';
import { getUser } from '@/lib/auth';
import { formatCurrency, formatDate } from '@/lib/format';
import { useI18n } from '@/lib/i18n';
import ReputationPanel from '@/components/ratings/ReputationPanel';
import RatePrompt from '@/components/ratings/RatePrompt';

// Contracts/PING/transactions side (whitepaper §4.5.4-.5). The existing
// transactions view + the LBTAS-gated escrow release wire in here in Phases 3-4.
const SECTIONS = [
{ icon: '📑', title: 'Plans & PINGs', desc: 'Consumer/producer plans and the PING update schedule.' },
{ icon: '⇄', title: 'Transactions', desc: 'Purchases, escrow holds, and settlement status.' },
{ icon: '📅', title: 'PING Calendar', desc: 'Planting, harvest, and PING report dates.' },
{ icon: '⭐', title: 'LBTAS Ratings', desc: 'One −1…+4 rating per transaction; required before escrow release.' },
];

// Contracts side (whitepaper §4.5.4-.5). Phase 3 surfaces the LBTAS prompt feed
// (transactions awaiting your rating) and your own reputation distribution.
export default function MyContracts() {
const router = useRouter();
const { t } = useI18n();
const [user, setUser] = useState(null);
const [pending, setPending] = useState(null);
const [myRep, setMyRep] = useState(null);
const [rateItem, setRateItem] = useState(null);
const [received, setReceived] = useState([]);

useEffect(() => {
const u = getUser();
if (!u) { router.push('/'); return; }
setUser(u);
load();
}, []);

async function load() {
try {
const [p, rep, rec] = await Promise.all([
api('/ratings/me/pending').catch(() => ({ pending: [] })),
api('/ratings/me').catch(() => null),
api('/ratings/me/received').catch(() => ({ received: [] })),
]);
setPending(p.pending || []);
setMyRep(rep);
setReceived(rec.received || []);
} catch {
setPending([]);
}
}

async function contest(ratingId) {
const reason = prompt(t('Por que esta avaliação é injusta?'));
if (!reason || !reason.trim()) return;
try {
await api('/ratings/' + ratingId + '/contest', 'POST', { reason: reason.trim() });
load();
} catch (e) { alert(e.message); }
}

if (!user) return null;

const openFlags = received.filter((r) => !r.voided);

return (
<div className="min-h-screen bg-cream">
<div style={{ maxWidth: 1000, margin: '0 auto', padding: 'clamp(20px,3vw,40px) var(--page-pad)' }}>
<h1 className="font-serif text-3xl font-black text-soil mb-1">{t('Meus Contratos')}</h1>
<p className="text-sm text-text3 mb-6">{t('Acompanhe planos, PINGs, transações e avaliações LBTAS.')}</p>
<div className="grid gap-4" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(min(240px,100%),1fr))' }}>
{SECTIONS.map(s => (
<div key={s.title} className="card-agro p-5 flex flex-col">
<div className="text-2xl mb-2">{s.icon}</div>
<div className="font-semibold text-soil">{s.title}</div>
<div className="text-sm text-text3 mt-1 flex-1">{s.desc}</div>
<span className="badge-agro badge-wheat text-[10px] mt-3 self-start">{t('Em breve')}</span>

{/* ── MY REPUTATION ── */}
<div className="card-agro p-5 mb-5">
<div className="text-xs font-semibold uppercase tracking-widest text-text3 mb-3">{t('Minha reputação')}</div>
<ReputationPanel rep={myRep} size="md" />
<p className="text-xs text-text3 mt-3 italic">{t('Reputação é uma distribuição por papel, nunca uma média.')}</p>
</div>

{/* ── PENDING RATINGS (the bidirectional prompt feed) ── */}
<div className="card-agro p-5">
<div className="text-xs font-semibold uppercase tracking-widest text-text3 mb-1">{t('Avaliações pendentes')}</div>
<p className="text-xs text-text3 mb-4">{t('Avalie suas transações para construir confiança na rede.')}</p>

{pending === null ? (
<div className="flex flex-col gap-2">{Array(2).fill(0).map((_, i) => <div key={i} className="skeleton h-16 rounded-xl" />)}</div>
) : pending.length === 0 ? (
<div className="flex flex-col items-center py-10 gap-2">
<span className="text-3xl">✅</span>
<p className="text-sm text-text3">{t('Nada pendente para avaliar')}</p>
</div>
) : (
<div className="flex flex-col gap-3">
{pending.map((item) => (
<div key={item.transaction_id} className="tx-item">
<div className="tx-icon">⭐</div>
<div className="flex-1 min-w-0">
<div className="text-[15px] font-semibold text-soil leading-tight">{item.listing_title || t('Produto')}</div>
<div className="text-xs text-text3 mt-1">
{item.role === 'buyer' ? t('Compra') : t('Venda')} · {formatCurrency(item.amount)} · {formatDate(item.created_at)}
</div>
</div>
<button className="btn btn-primary btn-sm self-center" onClick={() => setRateItem(item)}>{t('Avaliar')}</button>
</div>
))}
</div>
))}
)}
</div>

{/* ── FLAGS RECEIVED (contest a -1 made against you — either direction) ── */}
{openFlags.length > 0 && (
<div className="card-agro p-5 mt-5">
<div className="text-xs font-semibold uppercase tracking-widest text-rust mb-1">{t('Sinalizações recebidas')}</div>
<p className="text-xs text-text3 mb-4">{t('Uma avaliação “Sem confiança” contra você pode ser contestada para revisão.')}</p>
<div className="flex flex-col gap-3">
{openFlags.map((f) => (
<div key={f.id} className="tx-item">
<div className="tx-icon text-rust">-1</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-semibold text-soil">{t('Sem confiança')} · {f.rated_role}</div>
<div className="text-xs text-text3 mt-1">{formatDate(f.created_at)}{f.contested ? ' · ' + t('contestada') : ''}</div>
</div>
{f.contested
? <span className="badge-agro badge-wheat text-[10px] self-center">{t('contestada')}</span>
: <button className="btn btn-outline btn-sm self-center" onClick={() => contest(f.id)}>{t('Contestar')}</button>}
</div>
))}
</div>
</div>
)}
</div>

{rateItem && (
<RatePrompt
transactionId={rateItem.transaction_id}
title={rateItem.listing_title}
counterparty={rateItem.counterparty_id}
releasesEscrow={rateItem.role === 'buyer' && rateItem.status === 'paid'}
onClose={() => setRateItem(null)}
onRated={load}
/>
)}
</div>
);
}
50 changes: 10 additions & 40 deletions app/farm/page.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,16 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { getUser } from '@/lib/auth';
import { useI18n } from '@/lib/i18n';

// Producer side of the network (whitepaper §4.5.3.2-.6). Posting flows land in Phase 2.
const SECTIONS = [
{ icon: '🌱', title: 'Producer Plans', desc: 'Plant→harvest plans buyers can contract against, with scheduled PING updates.' },
{ icon: '🥕', title: 'Direct Market', desc: 'Harvested goods listed for immediate sale.' },
{ icon: '📦', title: 'Products', desc: 'Value-added goods, seeds & young plants, tools, infrastructure, soil inputs.' },
{ icon: '🌾', title: 'Agrotourism', desc: 'Market gardens, events, tours, education, and volunteering.' },
];
import MyPosts from '@/components/posts/MyPosts';

// Producer side of the network (whitepaper §4.5.3.2-.6).
// Phase 2 high-value types live here: Direct Market + Products (Plans & Agrotourism follow).
export default function MyFarm() {
const router = useRouter();
const { t } = useI18n();
const [user, setUser] = useState(null);

useEffect(() => {
const u = getUser();
if (!u) { router.push('/'); return; }
setUser(u);
}, []);

if (!user) return null;

return (
<div className="min-h-screen bg-cream">
<div style={{ maxWidth: 1000, margin: '0 auto', padding: 'clamp(20px,3vw,40px) var(--page-pad)' }}>
<h1 className="font-serif text-3xl font-black text-soil mb-1">{t('Minha Fazenda')}</h1>
<p className="text-sm text-text3 mb-6">{t('Gerencie sua produção: planos, produtos, venda direta e agroturismo.')}</p>
<div className="grid gap-4" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(min(240px,100%),1fr))' }}>
{SECTIONS.map(s => (
<div key={s.title} className="card-agro p-5 flex flex-col">
<div className="text-2xl mb-2">{s.icon}</div>
<div className="font-semibold text-soil">{s.title}</div>
<div className="text-sm text-text3 mt-1 flex-1">{s.desc}</div>
<span className="badge-agro badge-wheat text-[10px] mt-3 self-start">{t('Em breve')}</span>
</div>
))}
</div>
</div>
</div>
<MyPosts
title="Minha Fazenda"
subtitle="Gerencie sua produção: planos, produtos, venda direta e agroturismo."
types={['direct_market', 'product', 'plan_producer', 'plan_consumer', 'agrotourism']}
defaultType="direct_market"
newLabel="Novo anúncio"
/>
);
}
Loading