Local road incident reporting & traffic advisory management for Valenzuela City, Philippines.
VCRoad is a cross-platform mobile and web application that empowers citizens to report road incidents (accidents, potholes, floods, debris) and stay informed with real-time traffic advisories. Built for Valenzuela City, the app replaces paper-based reporting with a digital, map-driven experience β integrating incident reports, traffic advisories, road safety education, and role-based administration into a single platform.
πΊοΈ Map Dashboard
- Real-time map view of all reported incidents and active advisories using OpenStreetMap
- Interactive markers with category-based icons and color coding
- Barangay-level filtering and statistics
- "Center on me" GPS location with permission-gated access
- Marker clustering with responsive sizing across devices
π Incident Reporting
- Multi-step wizard: category selection β location pinning β media capture β review & submit
- Image upload with client-side validation (size, resolution, content heuristics)
- GPS-assisted barangay auto-detection
- Report history with status tracking
- Email verification required before first report
π¦ Traffic Advisories
- Create and publish advisories (road closures, stop-and-go, one-way, construction, etc.)
- Map-based route plotting with OSRM road snapping, undo/redo history, and editable auto-detected road names
- Real-time updates pushed to all users
- Enhanced data model β advisories carry a stable
barangayId(from the GeoJSONid), a FirestoreGeoPointcenterplusboundsNE/boundsSWfor geo-queryability, denormalized lowercasesearchKeywordsfor indexedarray-containssearch,createdByUid/updatedByUidaudit fields, a cappedversionHistoryaudit trail, andstatusUpdatedAt/nextStatusAttimestamps - Tolerant parsing β
fromJsoncoerces int coordinates, skips malformed route entries, validates weekday/time shapes, falls back unknownstatustoinactive(never published), and falls back bad dates to epoch instead of now - Optimistic locking β edits increment
versioninside a Firestore transaction and fail with a refresh prompt on conflict; security rules enforce theversion + 1bump and protect immutable fields (advisoryId,createdAt,createdBy,createdByUid) - Single consolidated stream β regular users share one realtime stream (active + scheduled, all barangays) that powers the home map, the advisory list, and the new-advisory alert popups (derived from new additions after the session baseline, with mute/dedupe/self-notification guards); the map always plots only active advisories and barangay selection on the advisory screen is a client-side filter. The admin/sysadmin all-statuses stream is capped at the 200 latest advisories for performance.
- Shared advisory UI β status badges render through one
AdvisoryStatusBadgewidget and category icons throughAdvisoryCategory.iconFor(labels/colors/icons come from the model, so cards, detail dialogs, and the wizard never drift apart). Cards show a fixed-height 96px photo strip (tap for fullscreen preview; identical height in loading/error/loaded states, sized image decode for cheap scrolling). The details dialog mirrors the exported share image's compact composition with native widgets: a poster-style list of labeled Reason/Location/Schedule rows, photo + navy-bordered map card (with an in-map route legend matching the export's red closed-segment / blue alternate-route swatches) beside them on wide surfaces or stacked below on phones, and lower-priority metadata (contractor, route counts, updated timestamp) collapsed behind a "More details" tile so the default view stays short, now including Created + Version rows and empty-state placeholders for missing photo/routes; its Edit/Download actions are wired from the management screen. Desktop/wide screens show the advisory list as a lazy responsive masonry grid (2 columns, 3 on very wide screens), and empty states offer an admin Create CTA. The advisory screen is split into testable widgets (AdvisoryCard,AdvisoryListView) so the list body can be exercised without Firebase. - Quick status toggle β admins can Activate/Deactivate/Cancel directly from a card or the details dialog (re-saves with the persisted status and a bumped
version; reactivations re-derive from the schedule so past-end advisories can't go live stale; no manual re-save needed).expiredis terminal and kept for the audit trail. Deleting an advisory enforces creator-or-admin ownership and removes its Supabase photo too (one silent retry, then an explicit warning with a Retry action on partial failure). The advisory list also supports Newest / Oldest / Recently-updated sorting. - Shareable advisory image β one-click Download on each card renders a share-ready 1920Γ1080 image (PNG/JPEG) fully client-side, no server or API key required. The static map (OSM tiles + affected/alternate polylines) sits in a right-column card with a navy border and an in-card route legend (red = closed segment, blue = alternate route). The left column is a WCAG-AA public notice: a category title ("ROAD CLOSURE & REROUTING NOTICE"), an accent divider, labeled Reason / Location / Schedule rows, and an "ADVISORY FOR MOTORISTS" bullet list derived from the reason (shown only when it has multiple segments). A top-left "TRAFFIC ADVISORY" banner (procedural traffic-light icon) and a top-right decorative dot grid frame the header, and a layered blue wave with the brand text ("VCRoad β’ City of Valenzuela") runs along the bottom. Category accents are darkened hue-preservingly (orange stays orange, yellow stays warm) so they read at β₯4.5:1 on the white panel, and the layout scales proportionally for landscape, square, and story formats. On web the PNG opens in a new tab via a blob URL; on native it saves a temp file with an Open action.
- UI/UX & layout (ISO 25010-aligned) β the advisory screen surfaces the active sort as a labeled control, the top stat cards (admin/sysadmin only) are tappable filter shortcuts through the same funnel as the chips and sidebar (with selected-ring + button semantics), and pull-to-refresh (mobile) or a refresh action (desktop) re-subscribe the stream. Filter dropdowns (category, sort, barangay) share one active-state language β accent border/content if and only if set β and search preserves the status view while matching category titles and contractor fields on legacy docs. Contextual empty states distinguish "no data yet" from "no results for your search/filters" (with a Clear search & filters action). The whole surface is theme-aware β stat cards, filter chips, the card grid, and the details dialog all use
Theme.colorScheme+AppColors.primaryAdaptivetokens (darker accents brightened in dark mode for ~3:1 contrast) with the brand-navy AppBar/FAB in light mode and theme defaults in dark; category icons pick dark or light ink from the chip color's luminance (e.g. the yellow partial-lane chip gets a dark icon) to keep WCAG contrast. Desktop uses a lazy responsive masonry grid with each card isolated in aRepaintBoundaryso scrolling doesn't repaint off-screen cards, and card action rows wrap instead of overflowing on narrow screens. The advisory details dialog shows a read-only mini map that auto-fits the entire affected + alternate route plot (padded, zoomed out one level, with a graceful fallback for single-point routes), and its image preview uses a stable hero tag. Screen-reader users get labeled review Edit buttons,selected/button semantics on filters and stats, live regions on errors and the result count, an initial-focus Close on dialogs, and semantically labeled date/time/reason fields. - Inline wizard validation & save safety β validation errors in the create/edit wizard surface as a persistent inline banner above the navigation bar (not just a transient snackbar), the Details step shows live inline hints on the reason, contractor, and schedule fields as you type, and an advisory photo (validated client-side: 5 MB cap, JPG/PNG, enforced on all platforms) uploads to Supabase Storage at
advisories/{id}/image.jpgwith the superseded image cleaned up on edit (same-URL replacements are skipped, not deleted), failed creates cleaning their fresh upload, and image caches evicted so replacements never render stale. Save failures surface in the same inline banner with the real error, and leaving the wizard with unsaved changes triggers a discard confirmation viaPopScope(withmountedguards on async saves). - Guided 4-step wizard β Type β Details β Routes β Review with tap-to-jump steppers (desktop rail pinned far-left, live summary far-right, config spreading between in a uniform full-height frame; compact stepper + collapsible summary on mobile). Forward jumps live-validate every intermediate step (backward jumps and Review Edit buttons are always free), the live summary shows actual road names with counts, and final submit re-validates all steps as defense-in-depth. Details offers one-time/recurring schedules (segmented switch, weekday strip, overnight windows, stacked date/time fields on phones) with conditional contractor fields and a photo drop-zone with fullscreen preview; Routes keeps map tiles mounted across steps with a viewport-sized map, legend, editable route names (commit on blur, empties rejected), and plotter undo/redo; Review shows a truthful derived status preview with per-section Edit jumps.
- Duplicate guard β before anything uploads, a modal warns when an active/scheduled advisory of the same type already exists nearby (1 km) or on the same road in the barangay (placeholder
Route Nnames excluded; edits exclude the advisory being edited). - Status lifecycle (client-side evaluator) β statuses (
active/inactive/expired/scheduled) are persisted on save, andnextStatusAtrecords when each should next change (one-timestartDate/endDateor the next recurring window boundary). Every stream snapshot re-evaluates immediately (no waiting on a possibly-stale timer), a timer re-arms at the earliestnextStatusAtboundary (clamped to 500 msβ6 h as a safety net), and changes persist in a single idempotent batch with audit-trail history (a re-emitted snapshot won't loop). Overnight windows attribute post-midnight hours to the previous day, one-time expiry is end-inclusive, and empty weekday sets evaluate daily.inactive(user-controlled) andexpired(terminal) are never auto-changed. Server-side auto-transitioning via a scheduled job is still a future option.
π Road Safety Education
- Interactive lessons with 4 question types: multiple choice, true/false, identification, matching type
- Question images supported across all types (e.g., road-signage photos): question-level images for identification/true-false, per-option images for multiple choice, and imageβmeaning pairs for matching type
- Admin question editor lets you attach images to any question or option (uploaded and compressed on save) β ideal for signage identification drills
- Learn dashboard β stats header with level badge & title (Student Driver β Road Master), XP progress bar, day streak, lessons completed X/Y, overall completion %, and a tappable due-review counter that jumps to the first lesson scheduled for review
- Per-chapter progress β expandable chapter headers show completed/total lesson counts with green progress bars
- Progressive unlocking β only the first lesson and each chapter's opener start unlocked (locked cards are greyed out); scoring 70% or higher completes a lesson and unlocks the next one
- Retake flow β scores below 70% show a "Keep Going!" result with a pass-hint card and a Retake Lesson button that resets the lesson's progress for another attempt
- Spaced-repetition reviews β completed lessons open a dedicated review screen: due questions by default (intervals double on correct answers, reset on wrong), an "All caught up!" state with a next-review countdown and a Review all questions anyway option, +5 XP per completed review, and a result screen showing the next review countdown
- Preview mode β admin/sysadmin-only toggle on the Learn page that unlocks every lesson card for browsing; regular users always follow the locked progression. Opening an unlocked-but-not-completed lesson in preview is a placeholder ("coming soon"), while completed lessons open normally.
- XP system with 5 levels (Student Driver β Road Master), streak tracking, and streak bonuses
- 8 achievement badges (First Steps, Perfect Score, On a Roll, Week Warrior, Chapter Master, Quick Learner, See It β Report It, Review Master) β 7 are earnable today; "See It, Report It" awaits the report-flow link-up
- Animated XP preview as final onboarding slide β shows level progression before entering the app
- Rich lesson result screen with animated score circle, XP counter, level-up indicator, and badge awards (plus a dedicated retake state when the pass threshold isn't met)
- "Report It" prompt after lesson completion β encourages filing a road report tied to what was learned (placeholder: the report link-up is not yet wired)
- Admin/sysadmin lesson management: create, edit, delete, publish/unpublish, and per-lesson question editor (lessons are auto-numbered; chapters and questions are drag-reorderable) β the lesson list adds search, summary chips (N Chapters / N Lessons / N Published), and pull-to-refresh, and the question editor guards against losing unsaved changes. Lessons auto-sum per-question points into their total, and per-answer analytics (
timesAnswered/timesCorrect) are tracked in Firestore. - Chapter manager with drag-to-reorder chapters
- Role-aware tutorial with animated widget previews (users: 5 slides including XP preview; admins: 3 slides with dashboard overview)
- Location permission requested contextually at point of need (center-on-map or first report), not during onboarding
π€ User Roles & Administration
- Three roles:
user,admin,sysadminenforced at both UI and Firestore rules level - Admin panel for user management, bans, and role elevation
- Account search scans the whole
userscollection (bounded scan loop over createdAt-ordered pages) so matches are found regardless of where they sit β no paid search service (Algolia) or Cloud Functions involved; pagination is cursor-based and compositely indexed infirestore.indexes.json - Registration flow with identity verification (valid ID + selfie capture)
- Reusable searchable Barangay dropdown (register, profile details, and create-admin) with instant cached names, normalized/prefixed search, load error + Retry, and a no-results state
- Profile details page: view/edit contact & address (phone, street, house number, barangay dropdown), read-only name & email, role/verification badges, selfie display, and an unsaved-changes guard when leaving while editing
- Profile management with appearance settings (theme toggle)
π Security & Session Management
- Firestore-based role verification in security rules (no self-elevation)
- Brute force protection: Firestore-based per-email lockout (5 failed attempts β 15-min block) + local SharedPreferences cache
- Single-device session enforcement: on login from a new device, the old device is notified and signed out
- Persistent login via Firebase Auth local persistence
- Password reset via email link with rate-limited resends (45s cooldown) and anti-enumeration messaging
π Theme System
- Light and dark themes with a navy-based dark palette
- Persistent theme toggle via SharedPreferences (Light / Dark / System)
- Accessible from Profile β Appearance β Theme
- Full dark-mode coverage, including the profile details page β surface cards, text, dividers, and outlined buttons adapt to the active theme while brand-blue header and input fields stay consistent
π± Framework & Language
| Component | Technology |
|---|---|
| Framework | Flutter ^3.12.2 |
| Language | Dart ^3.12.2 |
| Platforms | Android, iOS, Web, Windows, macOS, Linux |
ποΈ Backend & Database
| Service | Status | Details |
|---|---|---|
| Firebase Authentication | β Active | Email/password, Spark free tier (unlimited) |
| Cloud Firestore | β Active | Primary database, Spark free tier (50K reads/day) |
| Supabase Storage | β Active | Image uploads for advisories, reports & lessons (1 GB free tier) |
| Firebase Storage | βΈοΈ Parked | Not used β Supabase Storage is the live backend |
| Cloud Functions | βΈοΈ Parked | Login tracking migrated to Firestore directly |
πΊοΈ Maps & Location
| Service | Replacement For | Status |
|---|---|---|
OpenStreetMap via flutter_map |
Google Maps SDK | β Active |
| Nominatim (OSM geocoding) | Google Places API | β Active |
| OSRM Nearest API (road snapping) | Google Roads API | β Active |
| Client-side image validation | Google Cloud Vision | β Active |
| OSM tile rendering via Canvas | Static map generation | β Active |
π¦ Key Packages
| Category | Packages |
|---|---|
| State Management | provider |
| Routing | go_router, url_strategy |
| Firebase | firebase_core, firebase_auth, cloud_firestore |
| Maps & Location | flutter_map, latlong2, geolocator, permission_handler |
| HTTP & APIs | http |
| UI Components | lottie, carousel_slider, introduction_screen, cached_network_image |
| Media | image_picker, file_picker, image, flutter_image_compress, video_player, video_thumbnail |
| Utilities | shared_preferences, uuid, device_info_plus, intl, url_launcher |
| Fonts | Poppins (Regular + Bold) |
VCRoad follows Clean Architecture with feature-first organization, aligned with ISO 25010 quality standards.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Presentation Layer β
β Screens β Providers (ChangeNotifier) β Shared Widgets β
ββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββ
β calls
ββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββ
β Repository Layer β
β AuthRepo β ReportRepo β AdvisoryRepo β LessonRepo ... β
ββββββββ¬βββββββββββββββββββββββ¬βββββββββββββββββββββββββββββ
β β
ββββββββΌβββββββββββ ββββββββΌβββββββββββββββββββββββββββ
β Firebase β β Free APIs / Client-Side β
β (Spark tier) β β ββββββββββββββββββββββββββββ β
β ββββββββββββ β β β Nominatim (geocoding) β
β β
β β Auth β
β β β β OSRM (road snap) β
β β
β β Firestoreβ
β β β β Client vision heuristicsβ
β β
β ββββββββββββ β β β OSM tiles (static map) β
β β
β β β β CSV export β
β β
β β β ββββββββββββββββββββββββββββ β
ββββββββββββββββββββ βββββββββββββββββββββββββββββββββββ
| Quality | How the Structure Addresses It |
|---|---|
| Maintainability | Feature modules encapsulate related UI, state, and logic. Clear dependency direction (β presentation β data β core). |
| Portability | Repository pattern abstracts data sources. Platform-specific code isolated via conditional exports. |
| Modularity | Feature-first grouping makes it easy to add/remove/modify features without affecting others. |
| Analyzability | File names reflect their purpose (e.g., login_screen.dart, auth_service.dart). |
| Testability | Repositories can be mocked independently. Providers decoupled from UI. Domain logic testable without Flutter dependencies. |
| Reusability | Shared widgets used across features. Utility functions are pure Dart with no UI coupling. |
| Replaceability | External services isolated behind repository interfaces, swappable without touching UI code. |
Prerequisites
- Flutter SDK ^3.12.2
- Dart SDK ^3.12.2
- Firebase project (configured with Authentication and Firestore)
- A code editor (VS Code, Android Studio, or IntelliJ)
Setup
# Clone the repository
git clone https://github.com/yo-gerr/vcroad.git
cd vcroad
# Configure environment variables
cp .env.example .env
# -> Edit .env and fill in your Supabase project URL and anon/publishable key.
# Install dependencies
flutter pub get
# Run on your preferred platform
flutter run -d chrome # Web
flutter run -d android # Android
flutter run -d ios # iOS (macOS only)
# Generate launcher icons (if needed)
flutter pub run flutter_launcher_iconsNote:
.envis gitignored and holds the Supabase credentials. The anon/publishable key is public by design (it ships in the client binary), so production data security relies on Supabase RLS / storage-bucket policies, not on keeping the key secret.
Firebase Configuration
The app uses Firebase for authentication and data storage. Configuration files are included:
lib/firebase_options.dartβ auto-generated by FlutterFire CLIandroid/app/google-services.jsonβ Android Firebase configios/Runner/GoogleService-Info.plistβ iOS Firebase config (if present)
To use your own Firebase project, run:
flutter pub add firebase_core
flutterfire configureThis regenerates firebase_options.dart and the platform-specific config files.
Lint & Analyze
flutter analyzelib/
βββ main.dart # Entry point, provider setup, GoRouter
βββ core/ # Cross-cutting concerns
β βββ constants/ # App-wide constants (config, password policy)
β βββ errors/ # Error types and handling
β βββ theme/ # AppColors, AppTextStyles, AppTheme
β βββ utils/ # Pure utility modules (no UI)
β βββ debouncer/ # Debounce utility
β βββ exception/ # try/catch helpers
β βββ format/ # Date/text formatting
β βββ input/ # Input validation, styling
β βββ map/ # Map configuration
β βββ responsive/ # Responsive breakpoint helpers
β βββ routing/ # Role-based routing config
β βββ web/ # Web-specific stubs
βββ data/ # Data layer
β βββ models/ # Data models (DTOs, JSON serde)
β βββ repositories/ # Repository/service implementations
βββ presentation/ # UI layer
βββ app/ # App shell, splash screen
βββ providers/ # 7 ChangeNotifier state managers
βββ shared/ # Shared dialogs, snackbar, widgets (banner, location prompt, coach marks)
βββ features/ # Feature modules
βββ auth/ # Login, Register, Reset Password
βββ onboarding/ # Role-aware tutorial with animated slides
βββ home/ # Map dashboard
βββ reports/ # Incident reporting wizard
βββ advisories/ # Advisory management wizard
βββ lesson/ # Quiz/lesson system
βββ admin/ # User/account administration
βββ profile/ # User profile & settings
assets/
βββ fonts/ # Poppins (Regular, Bold)
βββ icons/ # Map markers, app icons (.webp)
βββ images/ # Feature illustrations, user content
βββ lottie/ # Lottie animation files
βββ json/ # Static configuration
βββ texts/ # Agreement text (EN + Tagalog)
βββ barangays/ # Barangay boundary GeoJSON
βββ database/ # Archived Firestore snapshots (reference only)
βββ downloads/ # Downloadable content
Role-based access control is enforced at the database level via firestore.rules, not just the client UI.
| Role | Read | Write | Elevation |
|---|---|---|---|
user |
Own data, public reports, advisories | Own profile, own reports (if verified, not banned) | Self-registration sets role: user only |
admin |
All users, reports, advisories, settings | User profiles (non-admin), reports, advisories, settings | Cannot self-promote to sysadmin |
sysadmin |
Everything | Everything | Full access |
- No self-elevation β role changes denied unless performed by admin/sysadmin on another user
- Report integrity β
userIdandreportedBymust match the authenticated user - Verification gating β only verified users can create reports
- Deny-all fallback β
match /{document=**}at the bottom rejects anything not explicitly allowed
firebase deploy --only firestore:rules,firestore:indexes
β οΈ Firestore may prompt you to accept the composite indexes added infirestore.indexes.json(advisoriesstatus/barangay/barangayId
createdAt, andstatus+nextStatusAt) before queries using them work.
After deploying rules + indexes, backfill existing advisory documents with the new schema (idempotent, dry-run by default):
node scripts/migrate_advisories.js # preview what will change
node scripts/migrate_advisories.js --run # apply the backfillPure-Dart and widget tests (no Firebase required):
flutter testtest/advisory_model_test.dartβ model units: status labels/colors, category lookup + icon fallback,buildSearchKeywords,computeCenter/computeBounds,computeNextStatusAt(one-time + recurring + wrap-around), tolerantfromJson/toJsonround-trips (int coordinates, malformed entries skipped, unknownstatusβinactive, epoch date fallback, legacy route shapes), and lifecycle edge cases (overnight spillover attribution, empty-weekday daily evaluation, end-inclusive expiry, stale-active transitions).test/advisory_announcement_test.dartβ public-notice phrasing: the fixed "NOTICE TO THE PUBLIC" header, all six category causes, place/barangay location variants, and the richer-field reason fallback.test/advisory_export_test.dartβ export layout geometry at five canvas sizes (banner/dot-grid/two-column/wave framing, left+map column separation, content band) and the WCAG contrast helpers (onDarkColorlifting, hue-preservingonLightColor).test/widget_test.dartβ widget tests for the shared advisory UI:AdvisoryStatusBadgelabels/colors andAdvisoryCardcontent, admin-action visibility, callback wiring, recurring-schedule rendering, the lazy desktop masonry grid, and the filtered-empty state's clear-filters action.test/advisory_details_dialog_test.dartβ details-dialog tests: primary poster-style notice rows always visible (one-time and recurring schedules), secondary metadata hidden until "More details" expands, route legend labels, and rendering at narrow + wide widths.
Planned Enhancements
| Priority | Feature | Notes |
|---|---|---|
| π΄ High | Add Firebase App Check | Protects API keys from unauthorized use |
| π‘ Medium | Storage quota & image optimization | Uploads already run on Supabase Storage (1 GB free tier) with client-side orphan cleanup and image-cache eviction; add a compression pipeline for larger media |
| π‘ Medium | Cloud Functions integration | Login tracking migrated; remaining functions pending Blaze |
| π‘ Medium | End-to-end testing suite | Provider-based architecture primed for integration tests |
| π‘ Medium | CI/CD pipeline | GitHub Actions for lint β test β build |
| π’ Low | Push notifications | Real-time alerting for new advisories |
| π’ Low | Offline-first support | Firestore persistence + local sync |
| π’ Low | Accessibility (a11y) completion | Screen-reader labels, live regions, focus management, and contrast largely covered in the advisory flows; remaining: keyboard navigation |
| π’ Low | Localization expansion | Beyond English/Tagalog |
Contributions are welcome. This project does not yet have a formal CONTRIBUTING.md β for now, please open an issue or pull request on GitHub. Basic guidelines:
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Commit your changes (
git commit -m 'Add my feature') - Push to the branch (
git push origin feature/my-feature) - Open a Pull Request
Distributed under the MIT License. See LICENSE for more information.
MIT is recommended for open-source Flutter projects β it's permissive, allows commercial use, and is the most widely adopted license in the Flutter ecosystem.
- Website: https://vcroad-a76a1.web.app
- Repository: https://github.com/yo-gerr/vcroad
- Issue Tracker: GitHub Issues
Built with Flutter. Backend on Firebase Spark free tier. Maps powered by OpenStreetMap and contributors. Architecture restructured July 2026.