Landscapt (CRM)

Job Photos

Field photo documentation, annotation, and before/after comparisons — attached to a job site, not a person.

What it is, and where to find it

Job Photos is a paid add-on (job_photos in the billing catalog) that gives crews and office staff a place to capture, tag, and mark up photos (and other files) tied to a job site. It's the same underlying feature everywhere it shows up — one component, JobPhotosPage, is rendered at two different routes:

  • Its own app shell at /photos/jobs, with a dedicated dark sidebar — this is what crew accounts land on. The Home dashboard surfaces it as a "Job Photos" tile ("Field photo documentation & field forms") whenever the org has the add-on, both for crew accounts and everyone else.
  • Inside Landscapt/CRM, under Scheduling → Job Photos (route at /crm/scheduling/job-photos).

A Photo Job can optionally link to an Equipt Project (for cost tracking) or a Landscapt Client — both links are visible and editable from the job detail pane. It is not tied to a specific CMMS Work Order, PM Schedule, or CRM Job/visit — see Known limitations below for what that means in practice.

Who can access it

Access is role-based, computed client-side in usePhotoAccess(). Admins and Crew accounts always have access; everyone else needs an explicit photo_module_access flag on their profile. This is not a crew-only tool — managers, admins, and technicians can all be granted access, and in practice the full gallery/detail view (edit, archive, delete, project/client linking) is only shown to non-crew roles, while crew gets a simplified mobile-first view.

RoleCan accessCan uploadCan annotateCan delete
AdminYesYesYesYes
Manager (“Sales”)Only with the access flagYesYesYes
CrewYes — auto-grantedYesNoNo
TechnicianOnly with the access flagYesNoNo
Viewer / PurchaserOnly with the access flagNoNoNo
Crew sees a different screen, not a locked-down one. When the signed-in user is Crew, the job detail page renders CrewPhotoView instead of the full PhotoGallery: a two-column grid, Before/After/All filters, a big sticky "Upload Progress Photo" button, and a read-only lightbox. Crew cannot open the annotation editor — the route itself redirects them away if they land on it directly.

Photo Jobs

Photos aren't uploaded loose — every photo belongs to a Photo Job, a lightweight record (name, customer name, address, notes, status) that exists specifically for organizing photo documentation by job site. Photo Jobs are distinct from CMMS Work Orders and CRM Jobs — they're their own object.

  • Statusactive, complete, or pending, changed with one-click action buttons on the detail pane.
  • Archive — a separate axis from status; archived jobs drop out of the default list but can still be found under the "Archived" view filter.
  • Optional Project link — ties the Photo Job to an Equipt/PO Project for cost tracking; the linked project's status badge and full detail sheet are reachable directly from the Photo Job.
  • Optional Client link — ties the Photo Job to a Landscapt Client record. If left unlinked, the job is only associated with a customer by free-text name — see Known limitations.

Both List and Table view modes are available for browsing Photo Jobs, and each job has its own Comments and Audit Trail tabs, same as other record types in the app.

Uploading photos, videos, and files

The uploader offers four entry points: Camera (opens the device's rear camera directly via capture="environment" on mobile), Library (photo picker), Videos, and Files (PDF, Word, Excel, PowerPoint, text, CSV). Multiple files can be queued at once.

  • Image compression — any image over 500 KB is compressed client-side to a 500 KB target (max dimension 2048px, JPEG/PNG, quality 0.85) before upload, explicitly to keep uploads workable on poor cell coverage in the field. EXIF is preserved through compression. If compression fails for any reason, the original file is uploaded unmodified rather than blocking the upload.
  • Location tagging — GPS is captured from the device's live Geolocation API at the moment of upload, not parsed from the photo's EXIF data. The code is explicit about why: mobile browsers routinely strip EXIF GPS tags before JavaScript can read them, so "where the device is right now" is the more reliable signal for a photo being taken live in the field. If location permission is denied, the photo still uploads — it's just untagged.
  • iCloud proxy warning — the uploader flags any image under 100 KB with a visible warning that it may be an undownloaded iCloud thumbnail rather than the real photo, and suggests opening it in the Photos app first.
  • Each queued file can be given a display name, a Before/During/After/Neither flag, one or more tags, and a note before submitting — set individually per file or as a default applied to the whole batch.
Storage: originals go to the job-photos-original Supabase bucket, annotated composites to job-photos-annotated. Only the storage path is stored in the job_photos table — signed URLs (1-hour TTL) are generated fresh at read time, never persisted, matching how attachments work elsewhere in the app. Deleting a photo soft-deletes the database row only; the underlying storage object is left in place intentionally, with storage cleanup treated as a separate admin task.

Tagging, before/after, and comparisons

Every photo (or video) can carry a Before/During/After/Neither flag and any number of the fixed tag set:

Tree RemovalTrimmingMulchLawnDrainageHardscapeCleanupOther

The gallery (office/full view) can filter by file type (Photos / Videos / Docs) and by the Before/During/After/Annotated tabs. A "Select" mode lets you multi-select photos to bulk-apply a Before/After/During tag, or to clear tags.

Before/After comparisons are a separate, explicit pairing — not just two photos that happen to share the Before/After tag. Select exactly two photos in Select mode and choose "Pair as Before/After"; a dialog lets you preview each photo, swap which one is "before" vs. "after", and give the pair an optional label (e.g. "Front bed", "Retaining wall"). A job can have several such pairs. Comparisons render with a slider view (BeforeAfterSlider), and any comparison can be deleted independently of the photos it references.

Annotating a photo

The annotation editor is a Fabric.js canvas layered over the original photo, reachable from any photo's "Annotate" button (Admin/Manager only). It exists to mark up areas of interest for the crew to see — the page's own subheading says exactly that: "Mark up areas of interest for the crew".

ToolWhat it does
SelectClick an existing shape or text label to select, move, resize, or rotate it. Delete key (or the toolbar trash icon) removes what's selected.
ArrowClick and drag to draw a straight arrow with a filled head — the standard “this, right here” marker.
CircleClick to drop a fixed 40px-radius outlined circle centered on the click point.
TextClick to place an editable label (“Label” by default) with a semi-transparent black background so it reads over any photo. Double-click to rename it.
Draw (freehand)Switches the canvas into freehand pencil mode — draw any freeform line, 4px wide.

Four colors are available — Red, Yellow, Green, White — applied to whichever tool is active. Undo removes the most recently added shape; Delete removes whatever is currently selected.

Saving renders the canvas (photo + all markup) to a flattened PNG, uploads it to the job-photos-annotated bucket, and stores the raw Fabric.js JSON alongside it so the annotation can be reopened and edited later — not just viewed as a flat image. Once a photo has annotations, both the office gallery and the crew view display the annotated composite instead of the plain original by default, with a small pencil-icon badge marking which photos have markup.

Re-opening a previously annotated photo on a different screen size re-scales every saved shape to match, rather than leaving them positioned for the original canvas dimensions — this is handled explicitly in the load path so an annotation made on a desktop still lines up correctly when reviewed on a crew tablet.

Worked example: documenting a mulch job

  1. Office creates a Photo Job — "123 Main St — Lawn & Mulch" — optionally linking it to the Client and/or the Equipt Project for cost tracking.
  2. Before the crew starts, someone (crew or office) uploads a "Before" photo of the bed from the Camera picker. It's automatically compressed and GPS- tagged from the device's current location.
  3. A manager opens that Before photo and annotates it — a red circle around a low spot that needs extra mulch depth, plus a text label reading "2″ here" — and saves. The circle and label are now baked into a composite PNG the crew sees when they open the photo, without needing to be in the office to hear the instruction.
  4. After the crew finishes, they upload an "After" photo of the same bed from the field — a two-tap flow (Camera → Upload) that doesn't require touching a keyboard.
  5. Office selects both photos in Select mode, chooses "Pair as Before/After", confirms which is which, labels the pair "Front bed", and saves. The comparison now appears with a slider anyone reviewing the job can drag between before and after.

Known limitations

  • Client linking is manual and optional. A Photo Job stores a free-text customerName and an optional clientId FK. If no client is explicitly linked, the job is associated with a customer by name string only — there's no automatic matching against the Clients table. Per CLAUDE.md, this kind of auto-match/client-linking work for photo jobs and damage cases is intentionally deferred until after the Landscapt dev/prod split.
  • No link to a specific CRM Job, visit, or CMMS Work Order. A Photo Job can only be linked to a Project (Equipt/PO) or a Client — not to an individual scheduled visit or work order. Photos document a job site, not a specific dispatch.
  • Deleted storage objects are not cleaned up automatically. Soft-deleting a photo removes the database row but intentionally leaves the file sitting in Supabase Storage; a real cleanup pass is a separate, unbuilt admin task.
  • The Draw and Circle annotation tools are fixed-size/simple. Circles are always a 40px radius regardless of drag distance, and there's no rectangle, highlighter, or measurement tool — only Arrow, Circle, Text, and freehand Draw.

More in Landscapt (CRM)

Clients, Properties & Leads
Client accounts, commercial hierarchies, service properties, and the activity timeline that ties it all together.
Estimates & the Budget Engine
How an estimate is built, how its numbers are actually calculated, and how it becomes a job.
Jobs & Packages
The six job types, how a job's status differs from a visit's status, and how a Package template turns into a billed job.
Services & Pricing
The service catalog, bulk catalog price changes, and Price Adjustment runs — which prices seed new work and which ones actually bill.
The Dispatch Board
The daily scheduling screen crews and dispatchers live in — visits, crews, status, and how actual hours get calculated.
The Crew App
What a crew sees on their phone — the day's stops, clocking on and off, breaks, photos, and sending work back to the office.
Sales Meetings
Booking appointments per sales rep, the double-booking warning, and the split between a rep's direct reminder and the client-facing automation trigger.
The Waiting List
Jobs with a date range instead of a date, held for opportunistic scheduling — and how to actually get them dispatched.
Snow Jobs & Storm Dispatch
Storm events, priority dispatch, and the invoicing flow built specifically for snow.
Invoicing & Payments
How an invoice is born, how its status moves, and where the client's own PO number actually goes.
Contracts
Ongoing billing agreements — monthly amounts, seasonal overrides, sub-properties, and how signing and invoicing actually work.
Communication Automations
How sequences, triggers, and events work — and how Automations differ from Sales Campaigns.
Forms & Lead Capture
Building, publishing, and sharing public forms — and what happens to a submission once it lands.
Tickets
Support and service tickets — where they come from, how they're worked, and how they connect to the rest of a client's record.
Damage Cases
Tracking property damage and warranty claims tied to a job — what a case captures, how cost rolls up, and a real current limitation in how it connects to a client record.
Online Payments & Stripe
How a client actually pays an invoice online, how Stripe Connect keeps every org's money in their own account, and how the platform takes zero cut.
Browse all guides
© 2026 Landscapt. All rights reserved.