+
+
+
+
+
Where and who?
+
All fields below are required
+
+ {locationMethod === null && !gpsLoading ? (
+
+
How would you like to provide your location?
+
{
+ void attemptGps()
+ setGpsLoading(true)
+ }}
+ >
+
+ Use current location (GPS)
+
+
{
+ setLocationMethod('manual')
+ }}
+ >
+
+ Choose municipality manually
+
+
+ ) : null}
+
+ {gpsLoading ? (
+
+
+
Getting your location...
+
+ ) : null}
+
+ {locationMethod === 'gps' && location ? (
+
+
Location
+
{
+ setLocationMethod(null)
+ setLocation(null)
+ }}
+ >
+
+
+
+
+
+ {location.lat.toFixed(5)}, {location.lng.toFixed(5)}
+
+
GPS · accuracy varies
+
+ Change
+
+ {locationError &&
{locationError}
}
+
+
+ ) : null}
+
+ {locationMethod === 'manual' ? (
+
+
Municipality
+
{
+ handleSelectMunicipality(e.target.value)
+ setLocationError(null)
+ }}
+ >
+ Select municipality...
+ {MUNI_LABELS_SORTED.map((m) => (
+
+ {m.label}
+
+ ))}
+
+ {locationError &&
{locationError}
}
+
+ ) : null}
+
+ {locationMethod === 'manual' && selectedMunicipalityId ? (
+
+
+ Barangay
+ — optional
+
+
{
+ setSelectedBarangayId(e.target.value)
+ }}
+ >
+ Select barangay (optional)...
+ {barangayOptions.map((b) => (
+
+ {b.name}
+
+ ))}
+
+
+ ) : null}
+
+ {locationMethod === 'manual' && selectedMunicipalityId ? (
+
+
+ Nearest landmark
+ — optional
+
+
{
+ setNearestLandmark(e.target.value)
+ }}
+ placeholder="e.g. Near the town plaza, across from Mang Juan Store"
+ className="text-input"
+ maxLength={200}
+ />
+
+ ) : null}
+
+ {locationMethod !== null ? (
+ <>
+
+
Your name
+
{
+ setReporterName(e.target.value)
+ setNameError(null)
+ }}
+ placeholder="Maria Dela Cruz"
+ className="text-input"
+ required
+ />
+ {nameError &&
{nameError}
}
+
+
+
+
Phone number
+
{
+ setReporterMsisdn(e.target.value)
+ setPhoneError(null)
+ }}
+ placeholder="+63 912 345 6789"
+ className="text-input"
+ required
+ />
+ {phoneError &&
{phoneError}
}
+
+ Gives you faster help. Admins call this number if they need more
+ details. Mas mabilis kang matutulungan.
+
+
+
+
+
Is anyone hurt?
+
+ {
+ setAnyoneHurt(true)
+ }}
+ className={`toggle-btn${anyoneHurt ? ' toggle-btn--selected' : ''}`}
+ >
+ Yes
+
+ {
+ setAnyoneHurt(false)
+ }}
+ className={`toggle-btn${!anyoneHurt ? ' toggle-btn--selected' : ''}`}
+ >
+ No
+
+
+
+ {anyoneHurt && (
+
+
How many patients?
+
+
{
+ setPatientCount(Math.max(0, patientCount - 1))
+ }}
+ className="counter-btn"
+ disabled={patientCount === 0}
+ >
+ −
+
+
{patientCount}
+
{
+ setPatientCount(patientCount + 1)
+ }}
+ className="counter-increment-btn"
+ >
+ +
+
+
+
+ )}
+
+
+
+ {isSubmitting ? 'Please wait...' : 'Continue'}
+
+ >
+ ) : null}
+
+ )
+}
diff --git a/apps/citizen-pwa/src/components/SubmitReportForm/Step3Review.tsx b/apps/citizen-pwa/src/components/SubmitReportForm/Step3Review.tsx
new file mode 100644
index 00000000..a4457d85
--- /dev/null
+++ b/apps/citizen-pwa/src/components/SubmitReportForm/Step3Review.tsx
@@ -0,0 +1,152 @@
+import { useState } from 'react'
+import {
+ ArrowLeft,
+ Heart,
+ Droplets,
+ Flame,
+ Wind,
+ Mountain,
+ Waves,
+ AlertTriangle,
+} from 'lucide-react'
+import { Button } from '../ui/Button'
+
+interface Step3ReviewProps {
+ onBack: () => void
+ onSubmit: () => void
+ reportData: {
+ reportType: string
+ location: { lat: number; lng: number }
+ reporterName: string
+ reporterMsisdn: string
+ patientCount: number
+ locationMethod: 'gps' | 'manual'
+ municipalityLabel?: string
+ barangayId?: string
+ nearestLandmark?: string
+ }
+ isSubmitting?: boolean
+}
+
+const INCIDENT_TYPES = [
+ { value: 'flood', label: 'Flood', Icon: Droplets },
+ { value: 'fire', label: 'Fire', Icon: Flame },
+ { value: 'earthquake', label: 'Earthquake', Icon: AlertTriangle },
+ { value: 'typhoon', label: 'Typhoon', Icon: Wind },
+ { value: 'landslide', label: 'Landslide', Icon: Mountain },
+ { value: 'storm_surge', label: 'Storm Surge', Icon: Waves },
+] as const
+
+export function Step3Review({
+ onBack,
+ onSubmit,
+ reportData,
+ isSubmitting = false,
+}: Step3ReviewProps) {
+ const [consent, setConsent] = useState(false)
+
+ const incident = INCIDENT_TYPES.find((t) => t.value === reportData.reportType)
+ const Icon = incident?.Icon ?? AlertTriangle
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ We heard you. We are here. We'll let you know when help is on the
+ way. Please keep your line open.
+
+
+
+
+
Review your report
+
+
+
Incident
+
+
+ {incident?.label ?? reportData.reportType}
+
+
+ {reportData.patientCount} {reportData.patientCount === 1 ? 'patient' : 'patients'}
+
+
+
+
+
Contact
+
{reportData.reporterName}
+
{reportData.reporterMsisdn}
+
+
+
+
Location
+ {reportData.locationMethod === 'manual' && reportData.municipalityLabel ? (
+ <>
+
+ {reportData.municipalityLabel}
+ {reportData.barangayId ? `, ${reportData.barangayId}` : ''}
+
+ {reportData.nearestLandmark ? (
+
{reportData.nearestLandmark}
+ ) : null}
+
Manual location
+ >
+ ) : (
+ <>
+
+ {reportData.location.lat.toFixed(5)}, {reportData.location.lng.toFixed(5)}
+
+
GPS coordinates
+ >
+ )}
+
+
+
+ {
+ setConsent(e.target.checked)
+ }}
+ className="consent-checkbox"
+ />
+
+ I confirm this report is true. You may contact me.{' '}
+ {
+ e.stopPropagation()
+ }}
+ >
+ Privacy notice ›
+
+
+
+
+
+ {isSubmitting ? 'Submitting...' : 'Submit report'}
+
+
+ )
+}
diff --git a/apps/citizen-pwa/src/components/SubmitReportForm/index.tsx b/apps/citizen-pwa/src/components/SubmitReportForm/index.tsx
new file mode 100644
index 00000000..8c8f220e
--- /dev/null
+++ b/apps/citizen-pwa/src/components/SubmitReportForm/index.tsx
@@ -0,0 +1,197 @@
+import { useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import { httpsCallable } from 'firebase/functions'
+import { db, fns, ensureSignedIn } from '../../services/firebase.js'
+import { submitReport, type SubmitReportDeps } from '../../services/submit-report.js'
+import { normalizeMsisdn } from '@bantayog/shared-validators'
+import { useSubmissionMachine } from '../../hooks/useSubmissionMachine'
+import { RevealSheet } from '../RevealSheet'
+import { Step1Evidence } from './Step1Evidence'
+import { Step2WhoWhere } from './Step2WhoWhere'
+import { Step3Review } from './Step3Review'
+
+interface FormData {
+ reportType: string
+ photoFile: File | null
+ location: { lat: number; lng: number }
+ reporterName: string
+ reporterMsisdn: string
+ patientCount: number
+ locationMethod: 'gps' | 'manual'
+ municipalityId?: string
+ municipalityLabel?: string
+ barangayId?: string
+ nearestLandmark?: string
+}
+
+function randomPublicRef(): string {
+ const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789'
+ const bytes = crypto.getRandomValues(new Uint8Array(8))
+ return Array.from(bytes, (b) => alphabet[b % alphabet.length]).join('')
+}
+
+function randomSecret(): string {
+ return crypto.randomUUID()
+}
+
+async function sha256Hex(input: string | Blob): Promise