- Backend: payment_sessions + pairing_failures tables; payment.service.js and pairing-failure.service.js (new); rewritten pairing.service.js (payment-gated blast + targeted "Curhat lagi" + cancel + fallback); rewritten extension.service.js (data-driven auto-approve with offline safeguard, charge-at-approval); pricing.service.js (extension tiers without free trial); mitra-status.service.js (countAvailableMitras cached path); 60s sweeper for stale payment sessions - Backend routes: client.payment.routes, client.mitra-availability.routes, internal/failed-pairings.routes; client.chat.routes rewritten for payment-gated start + /returning + /cancel + /fallback-to-blast; internal/config.routes adds 4 new keys with Valkey invalidate publish - client_app: mitra-availability poll, payment screen + notifier, pairing notifier rewrite (PairingTargetedWaiting + PairingFailed states), targeted-waiting overlay + bestie-unavailable dialog, "Curhat lagi" CTA, failed-pairing terminal, extension via payment-session - mitra_app: PairingRequestType enum, returning-chat 20s countdown auto-dismiss, extension card "otomatis disetujui" copy - control_center: 4 new config rows in Settings, Failed Pairings page (filter + paginate + action menu), sidebar + route registered - Test infrastructure: Vitest backend (7/7 pass), Playwright CC (4/4 pass), Maestro mobile scaffold (CLI install pending) - Bugs found via Playwright + fixed: LoginPage labels not associated with inputs (a11y); backend internal CORS missing PATCH/PUT/DELETE in allow-methods (silent settings breakage in browsers since Stage 4) - Docs: phase3.7.md PRD, phase3.7-plan.md, phase3.7-questions.md (Q&A), phase3.7-testing.md (E2E checklist), phase3.7-test-run-2026-05-03.md (today's run results) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
84 lines
3.4 KiB
JavaScript
84 lines
3.4 KiB
JavaScript
import { useState } from 'react'
|
|
import { Outlet, NavLink } from 'react-router-dom'
|
|
import { useAuth } from '../core/auth/AuthContext'
|
|
import { apiClient } from '../core/api/api-client'
|
|
|
|
const PasswordChangeForm = ({ onDone }) => {
|
|
const [current, setCurrent] = useState('')
|
|
const [next, setNext] = useState('')
|
|
const [saving, setSaving] = useState(false)
|
|
const [error, setError] = useState('')
|
|
const [success, setSuccess] = useState(false)
|
|
|
|
const submit = async (e) => {
|
|
e.preventDefault()
|
|
setError('')
|
|
setSaving(true)
|
|
try {
|
|
await apiClient.patch('/internal/control-center-users/me/password', {
|
|
current_password: current,
|
|
new_password: next,
|
|
})
|
|
setSuccess(true)
|
|
setCurrent('')
|
|
setNext('')
|
|
} catch (err) {
|
|
const code = err?.response?.data?.error?.code
|
|
const msg = err?.response?.data?.error?.message
|
|
if (code === 'INVALID_CREDENTIALS') setError('Password saat ini salah.')
|
|
else if (code?.startsWith('PASSWORD_')) setError(msg || 'Password tidak memenuhi syarat.')
|
|
else setError('Gagal mengubah password.')
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={submit} style={{ padding: 8, border: '1px solid #eee', marginTop: 8 }}>
|
|
<input type="password" placeholder="Password lama" value={current}
|
|
onChange={e => setCurrent(e.target.value)} required
|
|
style={{ display: 'block', width: '100%', marginBottom: 6 }} />
|
|
<input type="password" placeholder="Password baru (min 8, huruf besar/kecil + angka)" value={next}
|
|
onChange={e => setNext(e.target.value)} required minLength={8}
|
|
style={{ display: 'block', width: '100%', marginBottom: 6 }} />
|
|
{error && <p style={{ color: 'red', margin: '4px 0', fontSize: 12 }}>{error}</p>}
|
|
{success && <p style={{ color: 'green', margin: '4px 0', fontSize: 12 }}>Password berhasil diubah.</p>}
|
|
<div style={{ display: 'flex', gap: 6 }}>
|
|
<button type="submit" disabled={saving}>{saving ? '...' : 'Simpan'}</button>
|
|
<button type="button" onClick={onDone}>Tutup</button>
|
|
</div>
|
|
</form>
|
|
)
|
|
}
|
|
|
|
export default function Layout() {
|
|
const { user, logout } = useAuth()
|
|
const [showPwForm, setShowPwForm] = useState(false)
|
|
|
|
return (
|
|
<div style={{ display: 'flex', minHeight: '100vh' }}>
|
|
<nav style={{ width: 220, borderRight: '1px solid #eee', padding: 16 }}>
|
|
<h2>Control Center</h2>
|
|
<ul style={{ listStyle: 'none', padding: 0 }}>
|
|
<li><NavLink to="/dashboard">Dashboard</NavLink></li>
|
|
<li><NavLink to="/mitras">Mitra</NavLink></li>
|
|
<li><NavLink to="/sessions">Sesi</NavLink></li>
|
|
<li><NavLink to="/failed-pairings">Failed Pairings</NavLink></li>
|
|
<li><NavLink to="/users">Users</NavLink></li>
|
|
<li><NavLink to="/mitra-activity">Aktivitas Mitra</NavLink></li>
|
|
<li><NavLink to="/settings">Settings</NavLink></li>
|
|
</ul>
|
|
<div style={{ marginTop: 'auto', paddingTop: 16 }}>
|
|
<p style={{ fontSize: 12 }}>{user?.email}</p>
|
|
<button onClick={() => setShowPwForm(v => !v)} style={{ marginRight: 6 }}>Ganti password</button>
|
|
<button onClick={logout}>Logout</button>
|
|
{showPwForm && <PasswordChangeForm onDone={() => setShowPwForm(false)} />}
|
|
</div>
|
|
</nav>
|
|
<main style={{ flex: 1, padding: 24 }}>
|
|
<Outlet />
|
|
</main>
|
|
</div>
|
|
)
|
|
}
|