Homeowner property hub: manage addresses, track jobs, store documents, schedule maintenance, and review service history
PropertiesList — useQuery(api.poster.properties.list), FAB navigates to AddProperty
PropertiesList — Empty: properties.items.length === 0
PropertyDetail — useQuery(api.poster.properties.get), tabs switch between Jobs/Documents/Maintenance sections
AddPropertyStep1 — Google Places autocomplete, placeId + lat/lng captured on selection
AddPropertyStep2 — Property type chips use IconCircle E (unselected) / C (selected), validation on continue
AddPropertyStep3 — useMutation(api.poster.properties.create), photos stored via Convex storage, Street View auto-fetched
PropertyJobs — Filtered from api.poster.properties.get response, toggle between active/completed via pills
PropertyDocuments — Category filter tabs, file stored via Convex storage, doc viewer opens on tap
MaintenanceSchedule — Calendar with event dots, frequency badges (quarterly/biannual/annual), reminder notifications via push
ServiceHistory — Timeline from completed jobs, filter by service type, before/after photos open in ImageViewer
PropertySettings — useMutation(api.poster.properties.update) for edits, api.poster.properties.remove for delete, setPrimary toggle
posterId: v.id("posterUsers")
address: v.string()
city: v.string()
state: v.string()
zipCode: v.string()
propertyType: v.string()
nickname: v.optional(v.string())
latitude: v.optional(v.number())
longitude: v.optional(v.number())
placeId: v.optional(v.string())
isPrimary: v.optional(v.boolean())
streetViewUrl: v.optional(v.string())
propertyNotes: v.optional(v.object({
gateCode, accessInstructions,
dogWarning, parkingInstructions,
specialNotes, contactOnArrival
}))
createdAt: v.number()
updatedAt: v.number()
Index: by_poster_id [posterId]
api.poster.properties.list
args: { limit?, offset? }
returns: { items, total, hasMore }
api.poster.properties.get
args: { propertyId }
returns: { property, jobs }
api.poster.properties.create
args: { address, city, state,
zipCode, propertyType?, nickname?,
sqFt?, yearBuilt?, bedrooms?,
bathrooms?, lat?, lng?, placeId? }
api.poster.properties.update
args: { propertyId, ...partial }
api.poster.properties.remove
args: { propertyId }
guards: no active jobs
api.poster.properties.setPrimary
args: { propertyId }
useProperties()
wraps api.poster.properties.list
returns paginated property list
usePropertyDetail(propertyId)
wraps api.poster.properties.get
returns property + associated jobs
usePropertyJobs(propertyId)
filters jobs by property from get
splits active vs completed
usePropertyDocuments(propertyId)
future: propertyDocuments table
useMaintenanceSchedule(propertyId)
future: maintenanceSchedule table
Google Places Autocomplete
address input + placeId capture
Google Street View
auto-fetched on property create
via googleStreetView.fetchUrl
stored as streetViewUrl on record
Future Schema (not yet built):
propertyDocuments table
propertyId, category, fileId,
fileName, fileType, fileSize
maintenanceSchedule table
propertyId, taskName, frequency,
lastCompleted, nextDue, reminder
Auth: requirePoster(ctx) on all
Validation: string/number ranges
Security: explicit field mapping