# RIO React App — Project Overview

## ⚠️ IMPORTANT: Git Workflow

**DO NOT commit changes to git from your local machine. DO NOT RUN GIT COMMANDS.** The workflow is:
1. Make code changes locally
2. Push changes to the dev server (via rsync or SCP)
3. Test on the dev server
4. Check in on the dev server (NOT from local)
5. Then migrate to production

The developer will handle all git commits themselves to ensure nothing reaches production prematurely.

**Important:** Never run `git status`, `git log`, `git diff`, or any other git commands locally — these may show stale information and the dev server is the source of truth. All version control is managed remotely.

### ℹ️ IMPORTANT: File Changes in Summaries

At the end of each prompt, **always provide a list of ONLY the files changed in that specific prompt**, not files from earlier in the session. This tells you exactly which files to push to the React/dev server after each response.

**Format:**
```
Files Changed (This Prompt):
- src/containers/Property/actions.js
- src/containers/VaultOffers/VaultOffersCommon.css
```

**Critical:** Do NOT include files you already pushed in previous prompts. Each response's file list is independent — only the files changed *right now* go in this list. This prevents you from re-pushing files unnecessarily.

### ℹ️ IMPORTANT: Viewing Full Conversation Context

The Claude Code terminal has limited scroll history. **At the start of EVERY session, provide the current transcript file path.** If not provided, request it with: "Give me the transcript path."

Open the transcript in VS Code to scroll freely through the entire conversation. The transcript file is your complete record of all messages, decisions, and context for the session.

---

## What It Is

RIO is a real estate management platform. It provides tools for managing properties, clients, tasks, expenses, documents, offers, communications, campaigns, and more. The UI is a single-page React application backed by a proprietary API server.

## Local API Server

The backend API for this project lives in a folder named `riowww`. On this machine it is at:

```
/mnt/c/Users/mcqueary/Documents/Work/RIO/Upgrade/riowww
```

On other developers' local builds it will be in a differently rooted path but the folder will always be named `riowww` — search for that to locate it.

## Tech Stack

- **React** (class components throughout)
- **Redux** with `redux-thunk` for async actions
- **react-router** (v3-style, using `Router`/`Route`/`IndexRoute` from `react-router`)
- **redux-devtools-extension** for dev tooling
- **react-redux-loading-bar** for loading indicators
- **redux-reset** to support full store resets (e.g., on logout)

## Project Structure

All application code lives under `src/`. Key directories:

- `src/containers/` — Feature modules. Each has its own `actions.js`, `reducer.js`, and one or more component files. Examples: `Property`, `Expenses`, `Tasks`, `Documents`, `Notes`, `CRM`, `Offers`, `Users`, `Campaigns`, `Flyers`, `Communications`.
- `src/components/` — Shared/reusable UI components: `Input`, `Button`, `ListItem`, `AddItemList`, `ListWrapper`, `Expander`, `LoadingBar`, `Message`, `Calendars`, etc.
- `src/utilityFunctions/` — API call utilities (see API section below).
- `src/selectors/` — Redux selectors (e.g., `propertySelectors.js`).
- `src/store.js` — Central Redux store. All reducers are registered here via `combineReducers`.
- `src/index.js` — App entry point, router configuration.

## API Calling Convention

There are two utilities for making API requests:

- **`xapi`** — The current, supported method. Constructs requests using `.add(key, value)` to set parameters, then `.fetch()` to execute. All new and refactored code should use this.
- **`postApi`** — Deprecated for regular API calls. **EXCEPTION: Do NOT convert file uploads to xapi.** File uploads (with `documentobj` parameter containing file data) must remain on `postApi`. It properly handles the FormData structure that file uploads require. Examples: property imports, user imports, CRM contact imports.

Typical action pattern using `xapi`:

```js
import xapi from '../../utilityFunctions/xapi';

export function getSomething(loginauth, id) {
    return function(dispatch) {
        let x = new xapi('api_action_name');
        x.add('auth', loginauth);
        x.add('id', id);
        x.fetch()
            .then(resp => resp.json())
            .then(data => {
                if (data.result && data.result.success) {
                    dispatch({ type: 'RECEIVE_SOMETHING', payload: data.result.data });
                }
            })
            .catch(err => console.log(err));
    };
}
```

`loginauth` is always read from `localStorage.loginauth` at call time and passed as the `auth` parameter.

## User Feedback (Messages)

The `Message` component provides app-wide success/error banners. Use `setMessage` from `components/Message/actions`:

```js
import { setMessage } from '../../components/Message/actions';

dispatch(setMessage('alert-success', 'Thing saved successfully'));
dispatch(setMessage('alert-danger', 'Something went wrong'));
```

## Redux Store

All reducers are registered in `src/store.js`. When adding a new feature with its own reducer, both the import and the entry in `combineReducers` must be added there.

---

## Expenses Module — Fix History

### Background

The Expenses module (`src/containers/Expenses/`) was one of the earliest parts of the project. Over time the API calling convention changed, and the module was left behind. It stopped working correctly because:

1. It was still using `postApi` (deprecated) instead of `xapi`.
2. API calls were made directly inside the component rather than through Redux actions.

### Files

- `containers/Expenses/Expenses.js` — Main list/add component
- `containers/Expenses/ExpenseOverview.js` — Edit/detail view for a single expense (rendered inline via `Expander`)
- `containers/Expenses/actions.js` — Redux action creators
- `containers/Expenses/reducer.js` — Redux reducer (`state.expenses`)

### What Was Broken and How It Was Fixed

**`store.js`**
The `expenses` reducer was never registered. Added import and entry in `combineReducers`.

**`actions.js`**
- Refactored `addExpense` from `postApi`/FormData to `xapi`.
- Fixed a bug where after a successful add, the list re-fetch was called with `payload.data.propertyId || payload.data.taskId` — both always `undefined`. Changed to `payload.id`, which is where the ID is actually set.
- Added `setMessage` dispatches for success and error feedback.
- Added an `onSuccess` callback parameter so the calling component can react to a confirmed save.

**`Expenses.js`**
- `componentDidMount` was calling the imported action functions directly (e.g., `getExpenseList(loginauth, ...)`), bypassing Redux dispatch entirely. Fixed to `this.props.getExpenseList(...)`.
- `componentDidUpdate` and `removeExpense` both called `this.xapiGetExpenseList`, a method that had been commented out during the earlier refactor, causing a runtime crash. Fixed to use `this.props.getExpenseList`.
- The render was reading `expensesList`, `categories`, and `fetchingExpenses` from local component state, which was never updated from Redux. Fixed to read from `this.props` (which are mapped from `state.expenses`).
- `addExpenses` now passes an `onSuccess` callback to `this.props.addExpense` that closes the form and resets `formValues` on confirmed success.
- Local state no longer holds `expensesList` or `categories` — those live entirely in Redux.

---

## Backend Cron Jobs

Scheduled background tasks live entirely on the PHP backend (`riowww`), not in the React app. Key locations:

- **`riowww/schedule/`** — All cron PHP scripts (billing, notifications, cleanup, etc.)
- **`riowww/cron.schedule.*`** — Config files listing scripts and their schedules
- **`riowww/scheduled.php`** — Web UI to view and trigger cron jobs

See the "Cron Jobs" section in `claude_RIO_WWW.md` for full details.

---

## Docker

This project **does not use Docker**. The `docker/` directory in `riowww` is unused legacy config — ignore it.

---

## White Label / Multi-Domain Deployment — Known Issue

When deploying the React app on a domain other than `riogenesis.com` (e.g., `www.vault-offers.com`, test servers), all POST requests — including every xapi call and file upload — will be silently intercepted by a legacy redirect mechanism in `_inc/_config.php` on the PHP backend and bounced to `riogenesis.com`.

**Symptoms in the React app:**
- File uploads appear to succeed but return no filename or temp ID
- xapi calls return HTML instead of JSON, causing all response fields to be `undefined`
- Actions that depend on temp files (e.g., photo add) fail with "file does not exist"

**The fix is in the PHP backend (`riowww`), not in the React code.** See the "AWS Post Redirect Issue" section in `claude_RIO_WWW.md` for the full explanation and how to add a new domain to the whitelist.

---

## Utilities Module — Add Utility

### Files

- `components/GeneralTab/UtilitiesParser.js` — Renders the Utilities tab content; now includes the Add Utility form
- `containers/DetailsTab/actions.js` — Contains `addUtility` action
- `xapi/utility_add.php` — PHP xapi endpoint for adding a utility provider

### How It Works

The Utilities tab (`containers/DetailsTab` → `components/GeneralTab/GeneralTab.js` → `components/GeneralTab/UtilitiesParser.js`) shows dropdowns for each utility type (Electric, Gas, Water, etc.) where a utility company is selected per type.

A **+ Add Utility** (green button) was added at the top of `UtilitiesParser`. Clicking it shows an inline form with fields:
- **Name** (required)
- **Type** dropdown (required — populated from the existing utility type sections already in the component data)
- Phone, Email, Contact (optional)

On save, the `addUtility` action (`containers/DetailsTab/actions.js`) calls the `utility_add` xapi endpoint, dispatches a success/error message, and fires an `onUtilityAdded` callback that re-runs `getGeneralFieldData` to refresh the utility dropdowns so the new provider appears immediately.

### PHP endpoint — `utility_add`

File: `riowww/xapi/utility_add.php`

Required params: `auth`, `utilityname`, `utilitytype`
Optional params: `utilityphone`, `utilityemail`, `utilitycontact`, `utilitywebsite`

Inserts into `tblutility` scoped to the authenticated user's `companyid`. Returns `{ success: 1, data: { utilityid: <new id> } }`.

### Wiring in GeneralTab

`addUtility` and `onUtilityAdded` are passed as props to `UtilitiesParser` from `GeneralTab`. `addUtility` is added to `mapDispatchToProps` in `GeneralTab.js` and imported from `containers/DetailsTab/actions`.

---

## Details Tab — Loading Bar Behavior

### Problem

Every field save on any Details tab (Overview, Staff, Listing, Occupancy, Closing, HOA, Utilities) triggered `getGeneralFieldData`, which dispatched `FETCHING_GENERAL_PROPERTY_DATA: true`. The reducer was **clearing `propertyGeneralDataObj` to empty objects** on that dispatch, and the render was showing `<LoadingBar/>` instead of the content — causing the entire form to disappear for several seconds between edits.

### Fix (two files)

**`containers/DetailsTab/reducer.js`**
Removed the data-clearing block from the `FETCHING_GENERAL_PROPERTY_DATA: true` case. The reducer now only toggles the `fetchingGeneralPropertyData` flag; existing data stays in the store during a refresh and is overwritten when `SET_GENERAL_PROPERTY_DATA` arrives.

**`components/GeneralTab/GeneralTab.js`**
Changed the render from a ternary (`LoadingBar` OR content) to showing both simultaneously. The `LoadingBar` is wrapped in a `position: fixed; top: 0; left: 0; right: 0; z-index: 9999` container so it floats at the very top of the screen during a save without obscuring or replacing the form below.

---

## Google Calendar Integration

### Overview

RIO supports syncing events between the app's internal calendar and a user's Google Calendar (and Outlook Calendar). The feature requires Google OAuth 2.0 authorization. The Google Cloud project is named **RIO-Genesis**.

### Key Files

- `containers/CalendarSync/CalendarOAuth.js` — Rendered inside the Profile page; handles linking/unlinking a Google account and processing the OAuth callback
- `containers/CalendarSync/CalendarSync.js` — Standalone calendar sync page (`/admin/calendarsync`); lists available Google calendars and lets the user sync or unsync each one
- `containers/CalendarSync/actions.js` — All Redux action creators for Google/Outlook OAuth and calendar sync
- `containers/MyProfile/MyProfile.js` — Imports and renders `CalendarOAuth` inside the "Calendar Sync Setup" section
- `components/Calendars/Calendars.js` — Main calendar view using FullCalendar; displays synced events; clicking an event opens the EventModal for editing
- `components/GoogleDriveDocuments/GoogleDriveDocuments.js` — Separate Google Drive integration (file picker); uses scope `https://www.googleapis.com/auth/drive.file`

### OAuth Flow

1. User goes to **Profile → Calendar Sync Setup**, selects the **Google** tab
2. Clicks **"Link Account"** — the backend returns a Google OAuth URL (`calendargoogle_oauth` xapi action); the app redirects to it (`window.open(url, "_self")`)
3. User approves permissions on Google's consent screen
4. Google redirects back to the app with `?code=...&state=...` query params
5. `CalendarOAuth` detects those params on mount and calls `processOAuthCallback(code, state)` → xapi action `calendarsync_oauth_save` → backend exchanges the code for tokens and stores them
6. `googleOAuthGet()` and `outlookOAuthGet()` are called again to refresh the displayed linked account info

The OAuth scope requested is determined server-side by the PHP backend when it constructs the Google authorization URL. It is not set in the React code.

### Calendar Sync Flow

After linking:
1. Navigate to **Calendar Sync** (`/admin/calendarsync`) — there is a "Go to Calendar Sync" link on the Profile page
2. The page fetches the user's Google calendar list (`calendargooglelists_get`) and displays each calendar with its name and last sync timestamp
3. **The `fa-calendar` icon next to each calendar name is decorative only — not clickable**
4. The **`fa-refresh` (Sync) icon** triggers `syncGoogleCalendar(calendarid)` → xapi `calendargoogle_syncevents` — pulls events from Google into RIO
5. The **`fa-unlink` (Remove) icon** appears after a calendar is synced; clicking it calls `unsyncCalendar(calendarid, 'google')`
6. After any event is added or updated in the app (`calendarevent_add` / `calendarevent_update`), the app automatically calls both `syncGoogleCalendar` and `syncOutlookCalendar` to push the change back

### xapi Actions (Calendar)

| xapi action | Purpose |
|---|---|
| `calendargoogle_oauth` | Get OAuth status and auth URL for Google |
| `calendaroutlook_oauth` | Get OAuth status and auth URL for Outlook |
| `calendarsync_oauth_save` | Process OAuth callback (code + state), save tokens |
| `calendarsync_oauth_remove` | Unlink a Google/Outlook account |
| `calendargooglelists_get` | List user's Google calendars |
| `calendaroutlooklists_get` | List user's Outlook calendars |
| `calendargoogle_syncevents` | Sync a Google calendar's events into RIO |
| `calendaroutlook_syncevents` | Sync an Outlook calendar's events into RIO |
| `calendarsync_unsync` | Remove a calendar from sync |
| `calendareventlist_get` | Get all calendar events for the app view |
| `calendarevent_add` | Create an event (triggers auto-sync after) |
| `calendarevent_update` | Update an event (triggers auto-sync after) |
| `calendarevent_remove` | Delete an event |

### Google Cloud Console — RIO-Genesis

The OAuth app is configured in Google Cloud Console under project **RIO-Genesis**.

- The OAuth consent screen was renamed to **"Google Auth Platform"** in the Google Cloud Console UI (as of early 2025). Navigate to **APIs & Services → Google Auth Platform**.
- The **"Audience"** tab within Google Auth Platform is where the publishing status and test users are managed.
- The app is currently set to **"In Production"** status. In Production mode, the Test Users section does not appear — that section only exists in "Testing" status.

### Testing the OAuth Scopes (for Google Verification Reviewers)

Since the app is In Production but not yet verified, all users (including Google's testers) will see the **"Google hasn't verified this app"** warning during the OAuth flow. This is expected. Testers should:

1. Proceed past the warning by clicking **"Advanced"** → **"Go to [App Name] (unsafe)"**
2. Follow the full OAuth consent flow
3. After linking, go to Calendar Sync and sync a calendar
4. Create or edit a calendar event in the app to confirm write-back works

Include this note in any testing instructions submitted to Google:
> *"The app is pending verification. On the OAuth warning screen, click 'Advanced' then 'Go to [app name] (unsafe)' to proceed. All scope functionality works normally past this screen."*

### Google OAuth Token Format Fix (May 2026)

Older stored Google OAuth tokens in `tbloauthuser.oauthuserkey` were plain access token strings. The Google PHP client library's `setAccessToken()` expects a JSON object (`{"access_token":"...","token_type":"Bearer",...}`). Passing a plain string throws "Invalid token format" — a catch error logged in `xapi_calendargooglelists_get`.

**Fix in `riowww/_inc/functions_xapi.php` inside `getClient()`:** Before calling `setAccessToken()`, the stored value is now JSON-decoded and validated. If it is not a valid JSON token object, `setAccessToken()` is skipped. The code then falls through to the existing refresh/re-auth flow:
- If `oauthusersecret` holds a valid refresh token, a new token is fetched silently
- If not, `getClient()` returns the auth URL and the React app prompts the user to re-link their Google account — no manual "unlink and relink" instruction needed

Users with properly stored JSON tokens are unaffected.

---

### Google App Verification — Drive Scope Change (May 2026)

During the Google app verification process for calendar sync, Google rejected the restricted `https://www.googleapis.com/auth/drive` scope and requested the app switch to the narrower `drive.file` scope.

**What was changed:**

- `riowww/_inc/functions_xapi.php` — Removed `$client->addScope('https://www.googleapis.com/auth/drive')` from `getClient()`. This scope was present but never used — the calendar client only needs calendar scopes.
- Google Cloud Console — Removed `auth/drive` from registered scopes; added `https://www.googleapis.com/auth/drive.file`.

**Current scopes registered in Cloud Console:**
- `https://www.googleapis.com/auth/calendar.events`
- `https://www.googleapis.com/auth/userinfo.email`
- `https://www.googleapis.com/auth/userinfo.profile`
- `https://www.googleapis.com/auth/drive.file` (non-sensitive — no verification required)

**Note on Drive scopes:** The Google Drive file picker (`GoogleDriveDocuments.js`) has always used `drive.file` — that was never the problem. The restricted `auth/drive` scope was incorrectly added to the PHP calendar OAuth client (`getClient()`) and was dead code. The reply sent to Google was **"Confirming narrower scopes"**.

---

### Google App Verification — Re-submission and Two-Project Discovery (May 2026)

After the May 2026 scope-narrowing reply, Google asked for a new demo video showing:
- The end-to-end app flow including the OAuth grant
- The complete OAuth Consent Screen, in English, with the exact scopes being requested
- Each requested scope being exercised by an app feature

**The OAuth Consent Screen itself is rendered by Google, not by RIO.** RIO does not control its layout or wording. What RIO controls is the *content* via Google Cloud Console → APIs & Services → Google Auth Platform (formerly "OAuth Consent Screen") under the **RIO-Genesis** project — app name, logo, support email, homepage/privacy/ToS links, registered scopes, authorized domains. Whatever scopes are registered there is what appears on the consent screen during the live OAuth flow.

### Two-Project Discovery

While preparing the video, we discovered the app's two Google OAuth flows are configured under **two different Google Cloud projects**:

| Flow | Project | Client ID | Config file |
|---|---|---|---|
| Calendar OAuth (server-side, PHP `getClient()` in `functions_xapi.php`) | **`rio-genesis`** (the project being verified) | `637410131656-m7bp6jlr7e6peaksfflre5siaood1i2j.apps.googleusercontent.com` | `riowww/_inc/googleapp_credentials.json` |
| Drive Picker (`GoogleDriveDocuments.js` + PHP `attachgoogledocs_*`) | **`rio-ui-287415`** (separate, unrelated to the verification submission) | `224808519689-mr45qvnm3q5lcv49jnk5makk23ht46a8.apps.googleusercontent.com` | `riowww/client_secret.json` (loaded by `riowww/_inc/functions_google.php`) |

The Drive Picker's consent screen shows `rio-ui-287415`'s branding — not RIO-Genesis. And `drive.file`, which we'd re-registered under RIO-Genesis as part of the earlier scope-narrowing, is never actually requested by RIO-Genesis at runtime. The picker requests it against `rio-ui-287415` instead.

### Where the Drive Picker is Surfaced in the UI

- Trigger button: `src/containers/AttachmentControls/AttachmentControls.js` lines 67-72 — the **"Google Docs"** button in the Communications attachment toolbar. Clicking it dispatches `openGoogleDocuments()`, which flips `state.communications.googleDocumentsListOpen` true.
- Render gate: `src/containers/Communications/Communications.js:986-990` — when true, renders `<GoogleDriveDocuments/>`, which fires the Google OAuth popup and then the Picker.
- After a file is picked, `src/containers/Communications/AttachmentActions.js:29` POSTs to `attachgoogledocs_add` on the PHP side.

### Options Considered

**Option 1 — Consolidate into RIO-Genesis (NOT taken)**

Reuse the existing RIO-Genesis Calendar Client ID for the Drive Picker too. A single Web Client ID supports both server-side auth-code (Calendar) and client-side popup (Picker) flows — scopes are requested at grant time, not bound to the Client ID. Creating new credentials inside an in-review project does **not** restart Google's verification; verification lives at the project/consent-screen level, not per Client ID.

Steps if we ever revisit:
- Cloud Console (rio-genesis project):
  - Enable **Google Picker API** and **Google Drive API** (Calendar API already on)
  - Edit the existing Calendar Client ID → add JavaScript origins for every domain the React app runs on (riogenesis.com, vault-offers.com, dev/staging, localhost)
  - Create a new **API key** restricted to Picker + Drive (this is a separate credential type from Client IDs)
- Code:
  - `src/components/GoogleDriveDocuments/GoogleDriveDocuments.js`:
    - line 13 `clientId` → `637410131656-m7bp6jlr7e6peaksfflre5siaood1i2j.apps.googleusercontent.com`
    - line 10 `developerKey` → new RIO-Genesis API key
    - line 17 `appId` → RIO-Genesis project number
  - `riowww/_inc/functions_google.php`:
    - line 22 → load `/_inc/googleapp_credentials.json` instead of `/client_secret.json`
    - line 24 → remove the hardcoded `setClientId('224808519689-...')`
  - Delete the now-unused `riowww/client_secret.json`

**Option 2 — Drop the Drive Picker entirely (CHOSEN)**

Client decided to **hide the Drive upload option in Communications**. With the picker out of the user-visible flow, the verification submission no longer needs `drive.file`, and `rio-ui-287415` is no longer referenced by anything users interact with.

### Decision and Resume Checklist (Checkpoint — pick up here)

The following work has NOT been done yet. This is the checkpoint to resume from:

1. **Hide the "Google Docs" attachment button** — ✅ DONE (2026-05-13)
   - File: `src/containers/AttachmentControls/AttachmentControls.js` — the `<Button>` block (formerly lines 67-72) is now wrapped in a JSX comment (`{/* ... */}`)
   - Picker code left in place as harmless dead code: `GoogleDriveDocuments.js`, `AttachmentActions.js`, PHP `attachgoogledocs_*`, `functions_google.php`
   - Not yet deployed — confirm zero traffic in `rio-ui-287415` API dashboard after deploy before proceeding with Step 3

2. **Remove `drive.file` from RIO-Genesis registered scopes** — ✅ DONE (2026-05-13)
   - Removed `https://www.googleapis.com/auth/drive.file` in Google Cloud Console → rio-genesis project → APIs & Services → Google Auth Platform → Data Access
   - Final registered scope list is now: `calendar.events`, `userinfo.email`, `userinfo.profile`

3. **Retire the `rio-ui-287415` Cloud project** — ✅ MOOT (2026-05-13)
   - The client accidentally deleted `rio-ui-287415` on their own. No retirement work needed; the project (and its `224808519689-...` OAuth client) is already gone.
   - Implication: the Drive Picker button in `AttachmentControls.js` is doubly dead — UI hidden in Step 1, and the OAuth client it pointed at no longer exists. Leaving the picker code as harmless dead code is still fine; if it were ever un-hidden it would simply fail at the OAuth step.

4. **Re-record the Google verification demo video**
   - Show only the Calendar OAuth grant with the three remaining scopes
   - Confirm consent screen language toggle (bottom-left) is **English**
   - Show the unverified-app interstitial path: Advanced → "Go to RIO-Genesis (unsafe)"
   - Demonstrate each scope:
     - `userinfo.email` / `userinfo.profile` → linked account info on the Profile page
     - `calendar.events` → sync a Google calendar at `/admin/calendarsync`, view synced events in the Calendar view, create/edit an event in RIO and show it pushing back to Google
   - Record in one continuous take through the OAuth grant (reviewers reject videos that splice past the consent screen)
   - Use a fresh Google test account that has never linked before for cleanest first-time consent footage

5. **Reply to Google** with the new video, noting the reduced scope set (`drive.file` removed since the corresponding feature is no longer exposed in the UI).

---

## Vault-Offers Counter Offer Feature

### Overview

Agents can send a counter offer to a buyer from the Offers list inside a property. The feature lives entirely in the vault-offers side of the React app and is triggered from `PropertyOfferList`.

### Flow

1. Agent clicks the **counter icon** (`fa-exchange`) on an offer row in `PropertyOfferList`.
2. The modal opens in `'counter'` mode. `handleCounter` fires:
   - Clears `counterOfferPrefill` and `currentNotification` from Redux.
   - Dispatches `xapiCounterOfferGet(offerid)` → pre-fills the form with the most recent counter (or the original offer if no counter exists yet).
   - Dispatches `xapiPropertyOfferNotificationGet({ propertyid, notificationtype: 'counter' })` → loads the notification email template into `currentNotification`.
3. The modal renders `CounterOfferForm` (shows a loading state until `counterOfferPrefill` is ready).
4. Agent fills out the form and clicks **Send Counter Offer**.
5. `handleCounterSend` fires:
   - Calls `xapiCounterOfferAdd(...)` → saves the counter to `tbloffercounter` and updates extended terms on `tbloffer`.
   - On success, calls `xapiSendPropertyOfferNotification(...)` using `currentNotification.emailtext` → sends the email and saves a note on the offer.
   - Closes the modal.
   - Calls `xapiGetOffer(offerid, propertyid)` → dispatches `SET_OFFER_DATA` to update that row in all three Redux offer lists (`singleOfferData`, `searchedPropertyOfferListData`, `allOffersData`).

### Key Files

| File | Role |
|---|---|
| `src/containers/PropertyOfferList/PropertyOfferList.js` | Houses `handleCounter`, `handleCounterSend`, and the modal rendering; renders `OfferChangeHistory` inside the counter/H&B modal |
| `src/containers/Offers/CounterOfferForm.js` | The counter offer form component (12 fields, 2-column grid) |
| `src/containers/Offers/OfferChangeHistory.js` | Scrollable card-based change history; reads `offerAuditList` from Redux; shown in counter modal and vault-offers `OfferOverview` |
| `src/containers/Property/actions.js` | `xapiCounterOfferGet`, `xapiCounterOfferAdd`, `clearCounterOfferPrefill`, `xapiOfferAuditListGet`, `clearOfferAuditList` |
| `src/containers/Property/reducer.js` | `counterOfferPrefill`, `counterOfferPrefillLoading`; `offerAuditList`, `offerAuditListLoading` state and reducer cases |
| `riowww/xapi/propertyoffercounter_get.php` | Returns most recent `tbloffercounter` row (or falls back to `tbloffer`) to pre-fill the form. Dates returned as `Y-m-d` for HTML date inputs. |
| `riowww/xapi/propertyoffercounter_add.php` | Inserts into `tbloffercounter` (no explicit `offercountercreateddate` — uses PostgreSQL DEFAULT/UTC); updates extended terms on `tbloffer`; writes audit rows to `tblofferaudit`. SMALLINT boolean fields use `1`/`0`. VARCHAR dollar fields use `db_tick()`. Dates saved as ISO `Y-m-d`. |
| `riowww/xapi/offerauditlist_get.php` | Returns `tblofferaudit` rows grouped by `offercounterid`, newest first. Detects H&B via `tblnote` BETWEEN check. Overrides "Counter Offer Amount" label to "Highest & Best Amount" for H&B groups. |

### Redux State (property reducer)

```
counterOfferPrefill: null | { ...formFields }
counterOfferPrefillLoading: bool
```

### CounterOfferForm Fields

Offer Amount (required), Earnest Money, Pre-Qualified (checkbox), Lender, Loan Type, Copy of Earnest Check (checkbox), Seller Paid Closing Costs, Seller Paid Home Warranty Costs, Seller Paid Repair Costs, Interest Rate %, Close of Escrow Date, Purchase Type, Notes to Listing Agent (full-width textarea).

### PHP Type Notes (tbloffer / tbloffercounter)

- `offerisprequalified`, `offeriscopyearnestincluded` — **SMALLINT** columns. SQL must use `1`/`0`, not `true`/`false`.
- `offerbuyerclosingcosts`, `offersellerpaidwarrantycosts`, `offersellerpaidrepaircosts` — **VARCHAR(50)** columns. Must use `db_tick()`, not `db_number()`.

### Notification Email / Note

After the counter is saved, `propertyoffernotification_send.php` sends the email and saves a note on the offer via `track_note`. On vault-offers domains (where `str_contains($_SERVER['HTTP_HOST'], 'vault-offers')` is true), the email/note body is overridden with the canonical vault-offers counter offer template defined in `propertyoffernotification_get.php` under `case 'counteroffernotification'`. This ensures the note matches the outgoing email and includes `@@offerlink@@` (the buyer's public offer link), which is resolved by `email_replace_arr` before saving.

### Offer List Update After Send

`SET_OFFER_DATA` (via `xapiGetOffer`) is used instead of `SAVE_OFFERS_DATA` (via `xapiGetOffers`) because it surgically updates the specific offer row in all three Redux lists. The re-fetched offer row returns `offerstatus` as `'In Negotiation'` (computed via the CASE WHEN subquery in `propertyoffer_get.php`).

### Promise Chaining / Error Handling

`handleCounterSend` returns the promise from `xapiCounterOfferAdd`. `CounterOfferForm.handleSend` chains `.catch()` on the returned promise to reset `sending: false` and display `'Failed to send. Please try again.'` if the PHP call errors.

### Offer Audit Log

Every counter offer save writes field-level change records to `tblofferaudit` (one row per changed field). The PHP endpoint snapshots current values before any writes using the same fallback logic as `propertyoffercounter_get.php`, then diffs each of the 14 form fields after saving. Numeric fields are compared as floats to avoid false positives from formatting differences.

`offerauditchangedbytype` is `'Seller'` for changes made by the logged-in listing agent. Buyer-side counter/H&B submissions (on `inc_propertyoffers_form.php`) write `'Buyer'`.

See `riowww/tblofferaudit_create.sql` for the table DDL — run this against the database before deploying.

### OfferChangeHistory Component

`src/containers/Offers/OfferChangeHistory.js` — renders a scrollable horizontal row of cards, one per counter event (`offercounterid`), newest on the left.

**Where it's used:**
- `PropertyOfferList.js` — shown inside the counter modal (`modalMode === 'counter'` or `'highestandbest'`) below the `CounterOfferForm`. Keyed on `offerid` so it re-fetches when a different offer is selected.
- `OfferOverview.js` — shown in the vault-offers offer detail view (vault-offers only, guarded by `profileFeatures.whitelabel_offers_only`).

**Redux state (property reducer):**

```
offerAuditList: null | Array<{
    offercounterid: number,
    countertype: 'counter' | 'highestandbest',
    changedbytype: 'Seller' | 'Buyer',
    amount: string,
    date: string,          // UTC ISO timestamp
    rows: Array<{ label, prevvalue, newvalue }>
}>
offerAuditListLoading: bool
```

**Actions (in `containers/Property/actions.js`):**

```js
xapiOfferAuditListGet(offerid)  // fetches via offerauditlist_get → FETCHING_OFFER_AUDIT_LIST / SET_OFFER_AUDIT_LIST
clearOfferAuditList()           // dispatches CLEAR_OFFER_AUDIT_LIST
```

**Reducer cases (in `containers/Property/reducer.js`):**

```
FETCHING_OFFER_AUDIT_LIST → offerAuditListLoading: true
SET_OFFER_AUDIT_LIST      → offerAuditListLoading: false, offerAuditList: payload
CLEAR_OFFER_AUDIT_LIST    → offerAuditList: null, offerAuditListLoading: false
```

**Card layout:** Navy (`#004A91`) header for Seller, steel-blue (`#5b9bd5`) for Buyer. Header shows changedbytype, counter type label ("Counter" or "Highest & Best"), and date formatted with `moment.utc(date).local().format('MM/DD/YYYY h:mm:ss A')`. Body is a 3-column table: Field | Previous Value | New Value.

**xapi endpoint:** `riowww/xapi/offerauditlist_get.php` — params `auth`, `offerid`. Returns groups ordered newest first. At the API layer, the label `'Counter Offer Amount'` is overridden to `'Highest & Best Amount'` when `countertype === 'highestandbest'`.

### Offer Note Date Fix

Notes on the Offers page were displaying a time several hours ahead of the actual creation time. The `notecreateddate` column in PostgreSQL stores UTC, but `moment(date)` was treating the unzoned string as local time. Fixed in `OfferOverview.js` line 635:

```js
// Before (wrong — treats UTC string as local time):
moment(note.notecreateddate).format('MM-DD-YYYY hh:mm:ss A')

// After (correct — parses as UTC then converts to browser local time):
moment.utc(note.notecreateddate).local().format('MM-DD-YYYY hh:mm:ss A')
```

---

## Missing xapi Endpoint — propertystatus_get

`propertyStatusGet()` in `containers/PropertyList/actions.js` calls `propertystatus_get` but the PHP file never existed, causing a JSON error when opening the Offers page. Created `riowww/xapi/propertystatus_get.php`.

The endpoint queries `tblmlisttype JOIN tblmlist` where `mlisttypeshortname = 'propertystatus'` and returns a flat array of `mlistvalue` strings. `OfferOverview.js` maps over this array directly treating each item as a plain string for the Property Status dropdown.

---

## CompanyCam Integration

### Current State (Option A — Simple Link)

A "Go to CompanyCam" button has been added to `containers/VaultOffersDashboard/VaultOffersDashboard.js` that opens `https://app.companycam.com` in a new tab. No auth or backend work involved.

### Future Work (Option B — OAuth Integration)

CompanyCam uses OAuth 2.0 authorization code flow. Prerequisites: a registered CompanyCam developer app with a Client ID and Client Secret.

**PHP backend (`riowww`) — 3 new xapi actions needed:**

- `companycam_oauth_init` — Returns the authorization URL (with `client_id`, `redirect_uri`, `scope`)
- `companycam_oauth_callback` — Receives the `code`, exchanges it for `access_token`/`refresh_token`, stores both in DB
- `companycam_oauth_status` — Returns whether the current user/company is connected

Token storage requires a DB table (or columns on an existing user/company table) for `access_token`, `refresh_token`, and expiry. **Important:** CompanyCam rotates refresh tokens — every new access token request returns a new refresh token, so both must be updated together on every refresh.

Access tokens expire after 7200 seconds (2 hours).

**React frontend — additions to `VaultOffersDashboard`:**

- On mount: call `companycam_oauth_status` to check connection state
- If not connected: show "Connect CompanyCam" button → call `companycam_oauth_init` → redirect or popup to CompanyCam auth URL
- After OAuth: CompanyCam redirects to a callback URL on the PHP server → that endpoint calls `companycam_oauth_callback`, then redirects back to `/vault-offers-dashboard`
- If connected: show "Connected to CompanyCam" status with an "Open CompanyCam" link

**Open questions before starting Option B:**
- Does the client already have a CompanyCam developer account / registered app (Client ID + Secret)?
- Should this be per-user OAuth or a single company-level connection?
- What API capabilities are actually needed (photo sync, project creation, etc.)?

---

## Build Environment — JavaScript Syntax Restrictions

The React app's build environment does **not** support optional chaining (`?.`). Do not use it anywhere in the React source. Use a `|| {}` fallback instead:

```js
// Not allowed:
const name = propertyOptions.find(opt => opt.value == id)?.name || '';

// Use this instead:
const name = (propertyOptions.find(opt => opt.value == id) || {}).name || '';
```

---

## Vault-Offers Offer Action Status Updates

### Overview

When a vault-offers user performs an offer action (Counter, Highest & Best, Accept, Reject) and it succeeds, the `offerstatus` field on the affected offers is automatically updated in both the DB and the Redux store. Non-vault users are unaffected — behavior is identical to before.

The gate in `OfferNotification` is `profileFeatures.whitelabel_offers_only`. The gate in `PropertyOfferList.handleCounterSend` is `offerActionsEnabled()`. Both check the same underlying feature flag.

### Status Values

All status IDs are lowercase, matching the `id` fields in `STATUS_OPTIONS` (`containers/ConnectionCenter/CONSTANTS.js`):

| Action | Status written |
|---|---|
| Counter | `'countered'` |
| Highest & Best | `'countered-h&b'` |
| Accept | `'accepted'` (clicked offer); `'rejected'` (all other offers on the property) |
| Reject | `'rejected'` |

### Which Offers Are Updated

- **Counter**: only the offer the counter icon was clicked on
- **Highest & Best**: all checkbox-selected offers + the clicked offer (if none checked, just the clicked offer)
- **Accept**: the clicked offer → `'accepted'`; every other offer for the property → `'rejected'`
- **Reject**: all checkbox-selected offers + the clicked offer (if none checked, just the clicked offer)

The "checkbox-selected + clicked offer" logic is already handled by `OfferNotification`'s `toChecked` state (built from `preCheckedOfferId` + `selectedOfferIds` in `componentDidMount`). `updateOfferField` iterates over `toChecked` for H&B and Reject.

For Accept, `allOffersData` (from `state.property.allOffersData`) is used to find all other offers — this ensures offers hidden by an active search filter are still rejected.

### Immediate List Update (Optimistic Dispatch)

To avoid the list showing stale status while API calls are in flight, `updateOfferStatusesLocal(updates)` is dispatched synchronously before any API calls. This fires `UPDATE_OFFER_STATUSES` in the property reducer, which patches `offerstatus` in both `allOffersData` and `searchedPropertyOfferListData` immediately. The async `xapiOfferFieldUpdate` calls persist each status to the DB in the background; when they complete, `SET_OFFER_DATA` (via the internal `xapiGetOffer` call) overwrites each row with the server-confirmed data.

### PHP Notes

`propertyoffer_update.php` accepts `fieldkey='offerstatus'` and saves the value directly. `propertyoffer_get.php` computes `offerstatus` via a CASE WHEN that only overrides with `'In Negotiation'` when the stored status is `'New'` or `NULL` and a counter row exists in `tbloffercounter`. Writing any explicit status (`'countered'`, `'countered-h&b'`, `'accepted'`, `'rejected'`) bypasses this override and persists correctly.

`propertyoffer_update.php` also has server-side auto-reject logic: when `offerstatus = 'accepted'` is saved, it auto-rejects offers where `offerstatus` is empty/null. The React-side rejection of all other offers is broader (covers offers with status `'New'` etc.) and supersedes the PHP auto-reject.

### Key Files

| File | Change |
|---|---|
| `containers/PropertyOfferList/PropertyOfferList.js` | Imports `xapiOfferFieldUpdate` and `updateOfferStatusesLocal`; `handleCounterSend` dispatches local patch + field update (vault-only); non-vault falls back to `xapiGetOffer` |
| `containers/Offers/OfferNotification.js` | `send()` determines status from action type (vault-only via `profileFeatures.whitelabel_offers_only`); dispatches optimistic patch then DB persists; `allOffersData` and `profileFeatures` added to `mapStateToProps` |
| `containers/Property/actions.js` | Added `updateOfferStatusesLocal(updates)` — dispatches `UPDATE_OFFER_STATUSES` with `[{ offerid, offerstatus }, ...]` |
| `containers/Property/reducer.js` | Added `UPDATE_OFFER_STATUSES` case — patches `offerstatus` on matching offers in both `allOffersData` and `searchedPropertyOfferListData` |

---

## Vault-Offers Offer Amount Sync (Counter / H&B)

### Overview

For vault-offers companies, `tbloffer.offeramount` is kept in sync with the most recent counter or buyer-side H&B amount so that a later **Accept** uses the correct price. The DB-side writes live entirely in the PHP backend (see the matching section in `claude_RIO_WWW.md`). The React change is just a refresh so the list reflects the new amount immediately.

### Which Actions Write the Amount

| Action | Has an amount? | Writes `offeramount` |
|---|---|---|
| Seller counter (React → `xapi/propertyoffercounter_add.php`) | yes | yes |
| Seller H&B (React, email request only) | no | no |
| Buyer counter (public PHP, `inc_propertyoffers_form.php`) | yes | yes |
| Buyer H&B (public PHP, `inc_propertyoffers_form.php`) | yes | yes |

The seller's H&B from the React side is purely an email request (uses `OfferNotification`, not `CounterOfferForm`) — there is no amount form, so nothing to write back. The matching amount arrives when the buyer responds, which is handled on the public side.

### React Change — `PropertyOfferList.handleCounterSend`

Previously, the vault-offers branch only optimistically patched `offerstatus` via `updateOfferStatusesLocal` + `xapiOfferFieldUpdate`; `offeramount` in the Redux lists stayed stale until a manual refresh. `handleCounterSend` now always calls `xapiGetOffer(selectedOfferId, propertyid)` after a successful save (in addition to the status-update path for vault-offers). `SET_OFFER_DATA` patches all three offer lists, so the updated amount appears in `singleOfferData`, `searchedPropertyOfferListData`, and `allOffersData` immediately.

### Why "On Accept"

`OfferNotification.send()` with `customnotificationtype === 'accept'` (vault-offers branch) writes `offerstatus = 'accepted'` to the clicked offer without re-stating the amount — it relies on the value already on the row. Keeping `offeramount` current at counter/H&B time means the Accept doesn't need to know which counter row to read from.

### Preserving the Original — `offeroriginalamount`

Overwriting `tbloffer.offeramount` would clobber the "Original Offer Amount" displayed on the public form and elsewhere, so `tbloffer` gained a dedicated `offeroriginalamount` column (`NUMERIC(12,2) NOT NULL DEFAULT 0`). See `claude_RIO_WWW.md` for the SQL migration, backfill, and `offer_set` change that populates it at offer creation.

**React display — `VaultOfferEdit.js`:**

A read-only **Original Offer Amount** field renders above the editable **Offer Amount** field. It is sourced from `offer.offeroriginalamount` (which `propertyoffer_get`'s `select *` already returns once the column exists) and only renders when the value is greater than 0, so pre-backfill rows or rows that never had it set don't show an empty `$ 0.00`. Value is formatted with `Number(x).toFixed(2)`.

The field is added to both the constructor initializer and `componentDidUpdate` so it tracks `props.offer` changes like the other fields.

---

## Vault-Offers Review Offer Task — List Navigation, Highlight & Auto-Complete

### Overview

Vault-offers users can click a "Review Offer" task badge on the dashboard to navigate to the offer list for that property with the specific offer highlighted in green and auto-scrolled into view. When any offer action is performed (Counter, Accept, Reject, Custom Email, or Highest & Best), the corresponding "Review Offer" task is automatically marked complete.

### User Flow

1. **From Dashboard:** Agent clicks a "Review Offer" task count badge on the vault-offers dashboard.
2. **Navigate to List:** The app navigates to `/property/{id}/offers?highlightofferid={offerid}` — the offer list (not the single-offer detail view).
3. **Visual Highlight:** The correct offer row receives a light green background (`#c8f7c5`).
4. **Auto-Scroll:** The page smoothly scrolls to center the highlighted row in the viewport.
5. **Persistent Highlight:** The green highlight remains visible indefinitely until the user navigates away.
6. **Auto-Complete on Action:** When the agent performs any offer action on that offer, the linked "Review Offer" task is automatically marked complete in the background.

### Key Files Changed

| File | What Changed |
|---|---|
| `src/containers/VaultOffersTaskList/VaultOffersTaskList.js` | `navigateToTask` method now uses `?highlightofferid=` param for Review Offer tasks |
| `src/containers/PropertyOfferList/PropertyOfferList.js` | Parse `?highlightofferid=` URL param, set state, apply CSS class, add scroll-to logic, call task completion action after counter send |
| `src/containers/PropertyOfferList/PropertyOfferList.css` | Added highlight CSS classes |
| `src/containers/Property/actions.js` | New `xapiCompleteReviewOfferTask(offerid)` action |
| `src/containers/Offers/OfferNotification.js` | Call `xapiCompleteReviewOfferTask` for each affected offer after send |
| `riowww/xapi/reviewoffertask_complete.php` | **NEW** — PHP endpoint to mark tasks complete |

### PropertyOfferList Implementation Details

#### Constructor
- State: `highlightOfferId: null` (stores the offerid being highlighted)
- Instance variable: `this.offerRowRefs = {}` (tracks DOM refs for each offer row by offerid)

#### getQueryHighlightOfferId()
Helper method that parses the `?highlightofferid=` URL param:
```js
getQueryHighlightOfferId = () => {
    if (typeof window === 'undefined') return null;
    const match = window.location.search.match(/[?&]highlightofferid=(\d+)/);
    return match ? parseInt(match[1], 10) : null;
};
```

#### componentDidMount
After existing logic, detect highlight URL param on initial mount:
```js
const highlightOfferId = this.getQueryHighlightOfferId();
if (highlightOfferId) {
    this.setState({ highlightOfferId });
}
```

#### componentDidUpdate
Two blocks added:

1. **Detect URL param changes** — When the URL changes but component stays mounted (e.g., back navigation from task detail):
```js
const currentHighlightId = this.getQueryHighlightOfferId();
if (currentHighlightId && currentHighlightId !== this.state.highlightOfferId) {
    this.setState({ highlightOfferId: currentHighlightId });
}
```

2. **Scroll when data arrives** — When `searchedPropertyOfferListData` populates with rows while highlight is active:
```js
const { highlightOfferId } = this.state;
const hadData = prevProps.searchedPropertyOfferListData && prevProps.searchedPropertyOfferListData.length > 0;
const hasData = this.props.searchedPropertyOfferListData && this.props.searchedPropertyOfferListData.length > 0;
if (highlightOfferId && !hadData && hasData) {
    setTimeout(() => {
        const el = this.offerRowRefs[String(highlightOfferId)];
        if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
    }, 100);
}
```

#### Offer Row Rendering
Each offer row `<div>` includes:
- **ref callback** — stores the DOM element by `String(offerid)`
- **CSS class conditional** — adds `propertyofferlist_borders--highlight` when `highlightOfferId` matches
- **No inline styles** — the highlight CSS class handles all styling

```js
const offeridStr = String(row.offerid);
const isHighlighted = this.state.highlightOfferId && String(this.state.highlightOfferId) === offeridStr;
return (
    <div
    className={`propertyofferlist_grid propertyofferlist_borders ${this.offerActionsEnabled() ? 'prodsite' : 'legacysite'} ${isHighlighted ? 'propertyofferlist_borders--highlight' : ''}`}
    key={i}
    ref={(el) => {
        if (el) this.offerRowRefs[offeridStr] = el;
        else delete this.offerRowRefs[offeridStr];
    }}
    >
    {/* row cells */}
    </div>
);
```

#### handleCounterSend
After successful counter save, call `xapiCompleteReviewOfferTask`:
```js
if (this.props.xapiCompleteReviewOfferTask) {
    this.props.xapiCompleteReviewOfferTask(selectedOfferId);
}
```

### CSS Highlighting

**PropertyOfferList.css:**
```css
.propertyofferlist_grid.propertyofferlist_borders--highlight {
  background: #c8f7c5 !important;
}

.propertyofferlist_grid.propertyofferlist_borders--highlight > div {
  background-color: #c8f7c5 !important;
}
```

The second rule is **critical** — child `<div>` elements inside the grid row would otherwise cover the parent's background. Both selector levels must be styled with `!important` to override existing specificity rules (e.g., the `:nth-child(odd)` white background).

### PHP Endpoint — reviewoffertask_complete.php

**Location:** `riowww/xapi/reviewoffertask_complete.php`

**Parameters:**
- `auth` (required) — authentication token
- `offerid` (required) — the offer ID to complete tasks for

**Logic:**
1. Validate auth and retrieve the authenticated user's `companyid`
2. UPDATE `tbltask` to mark tasks complete:
   - WHERE `taskcompleteddate IS NULL` (not already complete)
   - AND `taskinactivateddate IS NULL` (not inactive)
   - AND `taskname = 'Review Offer'` (only Review Offer tasks)
   - AND `taskdescription LIKE 'Offerid: ' || offerid || '%'` (linked to this offer)
   - AND `propertyid IN (SELECT propertyid FROM tblproperty WHERE companyid = ...)` (scoped to user's company)
3. Set `taskcompleteddate = now()` and `taskcompletedby = loginid()`
4. Return `{ success: 1 }` (fire-and-forget — React doesn't wait for result)

### Redux Action — xapiCompleteReviewOfferTask

**Location:** `src/containers/Property/actions.js`

```js
export function xapiCompleteReviewOfferTask(offerid) {
    return function(dispatch) {
        let x = new xapi('reviewoffertask_complete');
        x.add('auth', loginauth);
        x.add('offerid', offerid);
        x.fetch().catch(err => console.log('reviewoffertask_complete error', err));
    };
}
```

No dispatch needed — this is a silent background operation. Errors are logged but don't block the user flow.

### Integration Points

#### OfferNotification.js
After `xapiSendPropertyOfferNotification(data)` succeeds, loop over all checked offers and complete their Review Offer tasks:
```js
const { xapiCompleteReviewOfferTask } = this.props;
if (xapiCompleteReviewOfferTask) {
    Object.keys(this.state.toChecked).forEach(offerid => {
        xapiCompleteReviewOfferTask(offerid);
    });
}
```

This handles: Accept, Reject, Custom Email, and Highest & Best actions.

Imports: `xapiCompleteReviewOfferTask` from `../Property/actions`
mapDispatchToProps: add `xapiCompleteReviewOfferTask`

#### PropertyOfferList.js
In `handleCounterSend`, after `xapiCounterOfferAdd` succeeds and the modal closes, call the task completion action for the specific counter offer:
```js
if (this.props.xapiCompleteReviewOfferTask) {
    this.props.xapiCompleteReviewOfferTask(selectedOfferId);
}
```

This handles: Counter action.

Imports: `xapiCompleteReviewOfferTask` from `../Property/actions`
mapDispatchToProps: add `xapiCompleteReviewOfferTask`

### Verification

1. **Dashboard → List Navigation**
   - From the vault-offers dashboard, click a "Review Offer" task count badge
   - Verify the offer list page loads (URL contains `?highlightofferid=...`)
   - Verify the correct offer row has a green background
   - Verify the page auto-scrolls so the highlighted row is centered

2. **Highlight Persistence**
   - Verify the green background stays visible indefinitely (no timeout)
   - Verify clicking outside the highlighted row or navigating away clears the highlight

3. **Counter Action → Task Complete**
   - On the offer list with the highlighted row, click the Counter icon
   - Fill out and send the counter
   - Go back to the vault-offers dashboard
   - Verify the "Review Offer" task count for that property decreased by 1 (after refresh or re-navigation if needed)

4. **Other Actions → Task Complete**
   - Repeat for Accept, Reject, Highest & Best, and Custom Email actions
   - Each should decrease the Review Offer task count

5. **Multiple Offers**
   - Test a property with multiple offers and multiple Review Offer tasks
   - Verify only the task for the selected offer is completed (others remain)

---

## Vault-Offers Styling System

### Overview

For detailed information on implementing and maintaining vault-offers styling across components and pages, **see `claude_vault_offers_styling.md`** in this directory.

This styling system provides compact, reduced-size UI for users with the `whitelabel_offers_only` feature flag enabled. All styling is conditional and non-vault users are completely unaffected.

### Implemented Pages

**Task Module:**
- AddTask, TaskOverview, CompleteTask, Tasks (list)

**Documents Module:**
- Documents (list/view), Document (card component), AddDocument (modal form)
- IconCounters (filter bar) — now horizontal layout: icon + name + count on one row

**Expenses Module:**
- Expenses (list), ExpenseOverview (detail/edit panel)

**Notes Module:**
- Notes (list), NotesOverview (detail/edit panel)

**Offers Module:**
- OffersList (add form), OfferOnline (Manage Online Offers), OpenHouseList (add/edit forms)
- Note: OfferEdit and CounterOfferForm were already styled (vault-offers only pages)

**Photos Module:**
- Photos (list with modal), Album (album container with add/zip/delete modes)

**Property History Module:**
- PropertyHistoryList (read-only table view of historical data)

**Filter Modals:**
- CRM Contact List filter (Communications page, ContactList.js)
- Email filter (Communications page, Communications.js)
- HOA List filter (HOA.js)
- Task filter (Tasks.js)
- Property filter (PropertyList.js)
- Property Attributes filter (PropertyAttributes.js)
- Users filter (Users.js)

### Quick Reference

- **CSS utilities:** `src/containers/VaultOffers/VaultOffersCommon.css` — centralized vault-offers styling
- **JavaScript styles:** `src/containers/VaultOffers/VAULT_OFFERS_STYLES.js` — reusable inline style objects
- **Feature flag:** `state.myprofile.features.whitelabel_offers_only`
- **Total pages styled:** 13 pages + 6 sub-components

### Common Styling Applied

All vault-offers styled pages follow this pattern:
- Buttons: `vault-offers-button` class (28px height, 0.85em font)
- Inputs: `vault-offers-input` class + `VAULT_OFFERS_STYLES.inputStyle` (22px height, 0.8em font, 2px 4px padding)
- Headers/Labels: `vault-offers-text-small` class (0.8em font)
- Spacing: Reduced margins (1px-2px instead of 1rem)
- Page wrapper: `vault-offers-[pagename]` class for scoped CSS overrides

### When to Read the Full Guide

Read `claude_vault_offers_styling.md` if you are:
- Adding vault-offers styling to a new page or component
- Troubleshooting placeholder positioning or input sizing
- Modifying existing vault-offers styled components
- Creating custom CSS rules for vault-offers features

### Filter Modal Styling Pattern

Filter modals on pages with search bars follow this pattern for vault-offers:

**CSS Class:** `vault-offers-filter-modal` (applies to `<Modal>` element)

**Component Pattern:**
```jsx
const isVaultOffers = profileFeatures && profileFeatures.whitelabel_offers_only;

const filterModal = (
    <Modal
        show={isFilterOpen}
        onHide={toggleFilter}
        className={isVaultOffers ? 'vault-offers-filter-modal' : ''}
    >
        <Modal.Header closeButton>
            <Modal.Title>Filter [List Name]</Modal.Title>
        </Modal.Header>
        <Modal.Body>
            <div className='input__wrapper'>
                <select className={`input primaryBorderWithFocus ${isVaultOffers ? 'vault-offers-input' : ''}`}>
                    {/* options */}
                </select>
                <div className="input__placeholder input__placeholder--datain">
                    Filter Label
                </div>
            </div>
        </Modal.Body>
        <Modal.Footer>
            <Button 
                className={`button__success ${isVaultOffers ? 'vault-offers-button' : ''}`}
                onClick={toggleFilter}>
                Close
            </Button>
        </Modal.Footer>
    </Modal>
);
```

**Styling includes:**
- Compact header/footer with reduced padding (4px 8px)
- Smaller font sizes (0.8-0.9em for titles, 0.8em for inputs)
- Input height reduced to 22px
- Button height reduced to 24px
- Reduced spacing between elements (6px margins)


---

## Known React Deprecation Warnings — To Fix

The following React warnings appear in the console and should be addressed:

### 1. **Invalid prop `placeholderTone` on DOM element**
**Location:** `src/components/Input/Input.js:308`  
**Issue:** `placeholderTone` is being passed to a DOM element as a prop; React doesn't recognize it. Should be either removed or spelled as lowercase `placeholdertone` if intentional custom attribute.  
**Fix:** Review Input component and remove invalid prop or convert to valid HTML attribute.

### 2. **Function components cannot be given refs**
**Locations:**
- `src/components/Input/Input.js` (OffersList render)
- `src/components/FileDrop/FileDrop.js` (DropUploadFile component)

**Issue:** Refs are being used on function components, which don't support refs directly. Components need to be wrapped with `React.forwardRef()` to accept refs.  
**Fix:** Convert function components to class components or wrap with `React.forwardRef()`.

### 3. **Deprecated lifecycle method `componentWillReceiveProps`**
**Location:** `src/components/Input/Input.js`  
**Issue:** `componentWillReceiveProps` is unsafe and deprecated in React 17.x. Should either be renamed to `UNSAFE_componentWillReceiveProps` (temporary) or refactored to use `componentDidUpdate` or `getDerivedStateFromProps`.  
**Fix:** Refactor Input component to use modern React patterns.

**Priority:** Low — these are dev warnings that don't affect functionality. Can be addressed after current postApi → xapi refactoring is complete.
