# VR Tour Links (Shared · Project Catalogue)

> 📍 Part of the project catalogue doc set — the map and routing table is
> [start-here.md](/docs/modules_handbook/shared/project-catalogue/start-here.md).

**Context:** a sub-module of [Project Catalogue](/docs/modules_handbook/shared/project-catalogue/readMe.md) · **Nav:** the **VR Tours** tab on a catalogue project's Show page (`/manage/property/catalog/{uuid}?tab=vr-tours`); buyers see it as the **VR360 Tour** tab on the public Malaysian project page (`/my/projects/{slug}#vr-tours`) · **Used by:** Project Catalogue (admin), the public project detail page and the Area Guide drawer (same props).

## What it does

Lets an admin attach **links to on-site / in-building 360° VR tours** — a Matterport, Kuula or
developer-hosted walkthrough of the lobby, a show unit, the facilities — to a catalogue project.
The admin types a **title** and a **URL**; nothing is uploaded or rendered here.

On the public page each tour is one row: the title and a **View** button that opens the link in a
new browser tab (`target="_blank" rel="noopener noreferrer"`). The raw URL is never printed to a
buyer. A project with no links shows no tab. Guests see it — like the 360° Aerial tab, it is
marketing, not a member benefit.

⚠️ **Not the same thing as [VR360 Aerial Panoramas](/docs/modules_handbook/shared/project-catalogue/vr360/readMe.md).**
That sub-module RENDERS an aerial view from Google 3D Tiles and stores the image. This one stores
someone else's link to the INSIDE of the building. Separate table, separate tab, separate code.

## How it works

- **Table `catalog_vr_tours`** — `catalog_project_id` + `catalog_project_uuid`, `title` (191),
  `url` (2048), `position`, blame columns, soft deletes. Model `Src\Analysis\Reference\CatalogVrTour`,
  relation `CatalogProject::vrTours()`.
- **It is a CATALOGUE table** (owner's ruling, 2026-09-26 — a project's tours are the same on every
  platform), so its migration lives in
  [`database/migrations/catalogue/`](/database/migrations/catalogue/README.md)
  (`2026_09_26_120000_create_catalog_vr_tours.php`) and is applied to both the master and each
  site's own copy. ⚠️ **The Hub must run `php artisan migrate --path=database/migrations/catalogue
  --database=catalogue` before the tab works against the master** — until then every read of a
  master project fails with "table not found".
- **Children follow their project.** Always read through `CatalogVrTour::forProject($catalogue)`,
  which asks on the project's own connection (master, or the site DB for a platform-created
  project) and returns display order (`position`, then `id`). It also matches
  `catalog_project_uuid IS NULL OR = the project's uuid`, so a stale integer after a master re-key
  cannot show another building's tours — the same guard `approvedVrBake()` uses.
- **Writes** go through `Src\Analysis\Repositories\CatalogVrTourRepository` (`create` appends at the
  end, `update`, `reorder`, `delete` = soft delete), each in a transaction on the project's
  connection. A tour is always resolved INSIDE its project (`findTour()` in the controller), so a
  tour id from another project 404s; `reorder` silently drops ids the project does not own.
- **Routes** (`routes/web.php`, inside `manage.property.catalog.*`), all
  `permission:manage-projects` + **`catalogue.edit`** (the domain gate — writes to a master row are
  allowed only from `CATALOGUE_EDIT_DOMAINS`):

  | Method | URI | Name |
  |---|---|---|
  | POST | `{id}/vr-tours` | `vr-tours.store` |
  | PUT | `{id}/vr-tours/order` (declared before `{tourId}`) | `vr-tours.order` |
  | PUT | `{id}/vr-tours/{tourId}` | `vr-tours.update` |
  | DELETE | `{id}/vr-tours/{tourId}` | `vr-tours.destroy` |

- **Validation** — `StoreCatalogVrTourRequest` (Update extends it): title required ≤191, url
  required ≤2048 and **`url:http,https`** — the value becomes a link buyers click, so a
  `javascript:` URL must never be stored.
- **Public page** — `ProjectDetailController::malaysiaPageProps()` sends `vrTours`
  (`[{ title, url }]`, nothing else) from the private `vrTourLinks()`. The prop is declared in
  `Components/ProjectDetail/projectDetailProps.js` — **an undeclared prop is silently dropped** by
  the page's `v-bind="props"` — and rendered by the `#tab-vr-tours` slot in
  `ProjectDetailContent.vue`, placed right after 360° Aerial.
- **Delete / cleanup** — `CatalogProjectRepository::delete()` soft-deletes the project's tours;
  `ProjectCatalogueMergeService::retainedDependencies()` lists "VR tour links" as a blocker, so the
  orphan sweep never deletes a canonical an admin has curated tours on.
- **Not covered (yet):** the Hong Kong and UAE detail pages do not show tours; `catalogue:export`
  packages do not carry the table (a COPY site's mirror does, since it copies every master table).

## Reference usage

```php
use Src\Analysis\Reference\CatalogVrTour;
use Src\Analysis\Repositories\CatalogVrTourRepository;

// Read — always on the project's own connection, in display order.
$tours = CatalogVrTour::forProject($catalogue)->get();

// Write — nested input keyed by the model, per GUIDELINES.
$data['catalog_vr_tour']['title'] = 'Show unit Type A';
$data['catalog_vr_tour']['url'] = 'https://my.matterport.com/show/?m=abc';
$tour = app(CatalogVrTourRepository::class)->create($catalogue, $data);
```

## Related files

- `database/migrations/catalogue/2026_09_26_120000_create_catalog_vr_tours.php`
- `src/Analysis/Reference/CatalogVrTour.php`, `CatalogProject::vrTours()`
- `src/Analysis/Repositories/CatalogVrTourRepository.php`
- `app/Http/Controllers/Manage/Property/CatalogVrTourController.php`
- `app/Http/Requests/Manage/Property/{Store,Update}CatalogVrTourRequest.php`, `ReorderCatalogVrToursRequest.php`
- `app/Http/Controllers/Manage/Property/CatalogController.php` — `vrTours` Show prop
- `app/Http/Controllers/Main/Site/ProjectDetailController.php` — `vrTours` page prop, `vrTourLinks()`
- `resources/js/Pages/Manage/Property/Catalog/Partials/Tabs/VrToursTab.vue`, `Show.vue`
- `resources/js/Components/ProjectDetail/ProjectDetailContent.vue`, `projectDetailProps.js`
- `tests/Feature/Property/CatalogVrTourTest.php`
