feat(api,ui,db): add address, guest users, image URLs; update API

- Backend:
  - Add address column to tickets and migration script
  - Create guest users when user_id is missing; accept user_name and address
  - Normalize stored image paths and expose absolute image_url
  - Introduce utils for path normalization and ticket serialization
  - Add CORS configuration for dashboard/emulator origins
  - Tickets API:
    - Serialize via ticket_to_dict with consistent schema
    - Change status update to PATCH /api/tickets/{id}/status with JSON body
    - Add DELETE /api/tickets/{id} with safe file removal
- Dashboard:
  - Fetch tickets from backend, show thumbnails, absolute image URLs
  - Status select + PATCH updates, toasts for feedback
  - Add i18n key btn.viewDetails
- Mobile app:
  - Persist device user_id via SharedPreferences
  - Fetch and merge API tickets; prefer network imageUrl
  - Submit user_name and address; delete via API when available
  - Make location acquisition robust with fallbacks and non-blocking UX
- Android/deps:
  - Disable Geolocator NMEA listener to prevent crashes
  - Downgrade geolocator to ^11.0.0 for stability

BREAKING CHANGE:
- Status endpoint changed from PATCH /api/tickets/{id}?new_status=... to
  PATCH /api/tickets/{id}/status with JSON body: {"status":"in_progress"}.
- /api/tickets and /api/tickets/{id} responses now use "id" (replacing
  "ticket_id"), include "image_url", and normalize fields for clients. Update
  consumers to use the new schema.
This commit is contained in:
2025-09-27 09:31:40 +08:00
parent 0e3eea7de9
commit 77d5be8fd1
27 changed files with 800 additions and 256 deletions

View File

@@ -21,6 +21,9 @@ class Report {
/// Base64 encoded photo for web platform
final String? base64Photo;
/// Remote image URL provided by backend (absolute URL)
final String? imageUrl;
/// Geographic location where the issue was reported
final LocationData location;
@@ -38,6 +41,8 @@ class Report {
/// Address or location description (placeholder for future use)
final String? address;
/// Name of the user who submitted the report (API reports)
final String? submittedBy;
/// Source of the photo ("camera" or "gallery")
final String source;
@@ -61,12 +66,14 @@ class Report {
required this.status,
this.photoPath,
this.base64Photo,
this.imageUrl,
required this.location,
required this.createdAt,
required this.updatedAt,
required this.deviceId,
this.notes,
this.address,
this.submittedBy,
required this.source,
this.editable = true,
this.deletable = true,
@@ -89,6 +96,8 @@ class Report {
String? photoPath,
String? base64Photo,
String? notes,
String? submittedBy,
String? address,
required String source,
required String deviceId,
required AISuggestion aiSuggestion,
@@ -106,6 +115,8 @@ class Report {
updatedAt: now,
deviceId: deviceId,
notes: notes,
address: address,
submittedBy: submittedBy,
source: source,
aiSuggestion: aiSuggestion,
);
@@ -118,6 +129,7 @@ class Report {
Status? status,
String? photoPath,
String? base64Photo,
String? imageUrl,
LocationData? location,
String? updatedAt,
String? notes,
@@ -133,6 +145,7 @@ class Report {
status: status ?? this.status,
photoPath: photoPath ?? this.photoPath,
base64Photo: base64Photo ?? this.base64Photo,
imageUrl: imageUrl ?? this.imageUrl,
location: location ?? this.location,
createdAt: createdAt,
updatedAt: updatedAt ?? this.updatedAt,
@@ -156,6 +169,7 @@ class Report {
'status': status.key,
'photoPath': photoPath,
'base64Photo': base64Photo,
'imageUrl': imageUrl,
'location': {
'lat': location.lat,
'lng': location.lng,
@@ -166,6 +180,7 @@ class Report {
'deviceId': deviceId,
'notes': notes,
'address': address,
'submittedBy': submittedBy,
'source': source,
'editable': editable,
'deletable': deletable,
@@ -187,6 +202,7 @@ class Report {
status: (json['status'] as String).toStatus() ?? Status.submitted,
photoPath: json['photoPath'] as String?,
base64Photo: json['base64Photo'] as String?,
imageUrl: json['imageUrl'] as String?,
location: LocationData(
lat: (json['location']['lat'] as num).toDouble(),
lng: (json['location']['lng'] as num).toDouble(),
@@ -199,6 +215,7 @@ class Report {
deviceId: json['deviceId'] as String,
notes: json['notes'] as String?,
address: json['address'] as String?,
submittedBy: json['submittedBy'] as String?,
source: json['source'] as String,
editable: json['editable'] as bool? ?? true,
deletable: json['deletable'] as bool? ?? true,