Mitra §A: pre-home (S3a/S3b/AccountInactive) + design system + Bestie Home
- Port halo_tokens + halo_theme + HaloButton to mitra_app (rose palette, Bricolage display, Poppins body, JetBrainsMono). - Build S3a Input WhatsApp (figma-bestie BestieS3 first half) with +62 chip, leading-zero/62 normalization, allow '+' in input. - Build S3b OTP verification (6-digit, 60s resend timer, attempts hint, Focus(canRequestFocus:false) for maestro inputText compat) with full error branching (CODE_MISMATCH, OTP_EXPIRED, OTP_USED, ATTEMPTS_EXCEEDED, WRONG_FLOW, ACCOUNT_INACTIVE). - Add AccountInactive terminal screen for is_active=false mitras. - Typed MitraAuthError with Indonesian-first localized messages + retryAfterSeconds passthrough. - Rebuild home_screen.dart to match figma BestieHome (greeting + status card + Ganti Status CTA + Pengingat + 2-tile dark grid). - Backend: POST /internal/_test/seed-mitra (idempotent) and PATCH /internal/mitras/:id (display_name update). - Control center: inline Edit Nama on mitras row + expandable inline log table under clicked mitra (vs old below-table panel). - 5 maestro flows ts-mitra-A-01/03/04/05/06 covering invalid input, happy path, account inactive, phone-format normalization, and the back-to-S3a regression. All green. Plan + memory documented in: - requirement/phase4-mitra-prehome-plan.md - requirement/flow_mitra.md / flow_mitra.mermaid.md §A Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -332,6 +332,28 @@ export const internalTestRoutes = async (fastify) => {
|
||||
return { ok: true, ...row }
|
||||
})
|
||||
|
||||
// Upsert a mitra with the given phone and is_active flag. Used by the
|
||||
// mitra_app pre-home Maestro flows to ensure a known mitra exists (with
|
||||
// the right active state) before driving the OTP verify path. Idempotent —
|
||||
// safe to run on every test pass.
|
||||
fastify.post('/seed-mitra', async (request, reply) => {
|
||||
const phone = request.body?.phone
|
||||
const display_name = request.body?.display_name
|
||||
const is_active = request.body?.is_active !== false // default true
|
||||
if (!phone || !display_name) {
|
||||
return reply.code(400).send({ error: 'phone and display_name required in body' })
|
||||
}
|
||||
const [row] = await sql`
|
||||
INSERT INTO mitras (phone, display_name, is_active)
|
||||
VALUES (${phone}, ${display_name}, ${is_active})
|
||||
ON CONFLICT (phone) DO UPDATE
|
||||
SET display_name = EXCLUDED.display_name,
|
||||
is_active = EXCLUDED.is_active
|
||||
RETURNING id, phone, display_name, is_active
|
||||
`
|
||||
return { ok: true, ...row }
|
||||
})
|
||||
|
||||
// Mark EVERY mitra row online. Used by Maestro flows as a setup step to
|
||||
// ensure a clean known-good state regardless of what previous tests did
|
||||
// (e.g. force-mitra-offline leaving the dev DB with no online mitras).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { authenticate, requirePermission } from '../../plugins/auth.js'
|
||||
import { getCcUserById } from '../../services/cc-user.service.js'
|
||||
import { createMitra, listMitras, updateMitraStatus } from '../../services/mitra.service.js'
|
||||
import { createMitra, listMitras, updateMitraStatus, updateMitraDisplayName } from '../../services/mitra.service.js'
|
||||
import { getOnlineMitras, getOnlineLogs } from '../../services/mitra-status.service.js'
|
||||
import { UserType } from '../../constants.js'
|
||||
|
||||
@@ -48,6 +48,17 @@ export const mitraManagementRoutes = async (app) => {
|
||||
return reply.send({ success: true, data: mitra })
|
||||
})
|
||||
|
||||
app.patch('/:id', {
|
||||
preHandler: [authenticate, attachCcUser, requirePermission('mitra', 'update')],
|
||||
}, async (request, reply) => {
|
||||
const { display_name } = request.body ?? {}
|
||||
if (typeof display_name !== 'string' || display_name.trim().length === 0) {
|
||||
return reply.code(422).send({ success: false, error: { code: 'VALIDATION_ERROR', message: 'display_name must be a non-empty string' } })
|
||||
}
|
||||
const mitra = await updateMitraDisplayName(request.params.id, display_name.trim())
|
||||
return reply.send({ success: true, data: mitra })
|
||||
})
|
||||
|
||||
app.get('/online', {
|
||||
preHandler: [authenticate, attachCcUser, requirePermission('mitra', 'read')],
|
||||
}, async (request, reply) => {
|
||||
|
||||
@@ -39,6 +39,16 @@ export const updateMitraStatus = async (id, is_active) => {
|
||||
return mitra
|
||||
}
|
||||
|
||||
export const updateMitraDisplayName = async (id, display_name) => {
|
||||
const [mitra] = await sql`
|
||||
UPDATE mitras SET display_name = ${display_name}
|
||||
WHERE id = ${id}
|
||||
RETURNING id, phone, display_name, is_active, created_at
|
||||
`
|
||||
if (!mitra) throw Object.assign(new Error('Mitra not found'), { code: 'NOT_FOUND', statusCode: 404 })
|
||||
return mitra
|
||||
}
|
||||
|
||||
export const listMitras = async ({ page = 1, limit = 20, is_active }) => {
|
||||
const offset = (page - 1) * limit
|
||||
const conditions = is_active !== undefined
|
||||
|
||||
Reference in New Issue
Block a user