# Global Command Bar & Deep Links — Implementation Plan (Phase 1)

> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
> **Concept/visual:** [`global-command-bar-plan.html`](global-command-bar-plan.html) (the dark-theme mockup — ⌘K is interactive there).
> **Survives `/clear`:** this file is the canonical execution record. Update the checkboxes as you go.

**Goal:** Let a user summon any sales invoice / sales order / customer from ANY screen via a ⌘K command bar, and jump straight to it through a shareable deep-link URL.

**Architecture:** A new BE `GET /api/core/search` endpoint returns ranked, company-scoped, permission-filtered hits, each carrying the exact FE `route` to open it. A new FE `CommandBarService` + `command-bar` overlay (mounted in the topbar) calls it on ⌘K, lets the user keyboard-navigate, and `router.navigateByUrl(hit.route)`. Deep-linking reuses the **existing** `?viewId=<id>` query-param open pattern already present on sales invoices — we extend the same pattern to sales orders and purchase bills.

**Tech Stack:** Laravel 12 (nwidart Modules, Pest 3) · Angular 21 standalone + signals · PrimeNG · the real Moon dark/amber theme (`styles.scss` tokens: `--brand #080c1a`, `--accent #f5a623`).

## Global Constraints

- **Language:** product UI is **English-default**, Arabic secondary. All new user-facing strings: English primary + Arabic. Code identifiers in English.
- **Auth:** API uses header **`X-Authorization: Bearer <token>`** (not `Authorization`). FE services follow the existing `core/services/*` pattern (`inject(HttpClient)`, base `${environment.apiUrl}/...`).
- **Company scope + permissions:** every search hit MUST be scoped to the active company AND filtered by the caller's permissions — never return a document the user can't open. Mirror an existing controller's scoping (e.g. `SalesInvoiceController@index`).
- **FE has NO unit-test runner** (`skipTests: true` in `angular.json`). FE task verification = `ng build` passes (zero errors) **+** an explicit runtime check (curl the endpoint / load the page / open the palette). BE tasks use **Pest** (real TDD).
- **🔴 CHANGELOG IS MANDATORY PER FEATURE — AND IT MUST BE PUSHED.** Every shippable change adds a bullet to `moon-erp-be/docs/moonstack/CHANGELOG.md` under `## [Unreleased]` (English primary, then `{{ar}}` Arabic). The bullet only reaches a release when it is **committed AND pushed to `hazemdev2` AND merged to `main`** — `moonstack:ship` promotes `[Unreleased]` → a dated version from `main`. **An un-pushed changelog never ships.** See Task 8.
- **🔴 PER-TASK REVIEW IS MANDATORY.** No task is "done" until its **Verify** step has been run and its output observed, and the **Review gate** checkbox is ticked. "It should work" is not allowed — run it.
- **Host rules (moonui2):** after editing BE files as root → `chown moonui2:moonui2 <file>` then `bash local-deploy.sh`. FE build via `node_modules/.bin/ng build --base-href /app/`; deploy with `\cp -rf dist/moon-erp/browser/* /home/moonui2/public_html/app/` (clean old chunks first) + chown. `config.json` (apiUrl→moonui2) must stay.

---

## File Structure

**Backend (`/home/moonui2/moon-erp-be`):**
- Create `Modules/Core/app/Http/Controllers/GlobalSearchController.php` — the `/search` endpoint.
- Create `Modules/Core/app/Services/GlobalSearchService.php` — runs per-type queries, builds hits.
- Modify `Modules/Core/routes/api.php` — register the route.
- Create `Modules/Core/tests/Feature/GlobalSearchTest.php` — Pest feature tests.

**Frontend (`/home/moonui2/public_html/moon-erp/src/app`):**
- Create `core/models/search-hit.model.ts` — the hit interface.
- Create `core/services/global-search.service.ts` — calls the endpoint.
- Create `core/services/command-bar.service.ts` — open state + ⌘K + recent.
- Create `layout/command-bar/command-bar.component.{ts,html,scss}` — the overlay UI.
- Modify `layout/topbar/topbar.component.{ts,html}` — trigger button + mount overlay.
- Modify `features/sales/orders/orders.component.ts` — add `?viewId=` open (mirror invoices).
- Modify `features/purchases/bills/bills.component.ts` — add `?viewId=` open (mirror invoices).
- Modify `features/sales/invoices/invoices.component.html` — add Copy-link / New-tab buttons to the view dialog header.

**Search hit contract (BE ⇄ FE — both sides MUST match):**
```
GET /api/core/search?q=<term>&types=invoices,orders,customers&limit=8
200 → { "data": SearchHit[] }
SearchHit = {
  type:    'sales_invoice' | 'sales_order' | 'customer',
  id:       number,
  number:   string,        // "INV-2025-1042" (or customer code)
  title:    string,        // partner / customer name
  subtitle: string,        // e.g. "Sales · 2026-06-21"
  amount:   string | null, // formatted total, e.g. "12,400.00"
  status:   string | null, // localized label, e.g. "Due"
  group:    string,        // localized group header, e.g. "Sales Invoices"
  route:    string         // FE route to open, e.g. "/sales/invoices?viewId=1042"
}
```

---

## Task 1: BE — `GlobalSearchService` returns sales-invoice hits

**Files:**
- Create: `Modules/Core/app/Services/GlobalSearchService.php`
- Test: `Modules/Core/tests/Feature/GlobalSearchTest.php`

**Interfaces:**
- Produces: `GlobalSearchService::search(string $q, array $types, int $companyId, int $limit = 8): array` → array of hit arrays matching the SearchHit contract.

- [ ] **Step 1 — Write the failing test.** Create `GlobalSearchTest.php`:

```php
<?php
use Modules\Core\Services\GlobalSearchService;
use Modules\Sales\Models\SalesInvoice; // confirm the FQCN with: grep -rn "class SalesInvoice" Modules/Sales/app/Models

it('finds a sales invoice by its number', function () {
    $company = \Modules\Core\Models\Company::factory()->create();
    $inv = SalesInvoice::factory()->create([
        'company_id' => $company->id,
        'invoice_number' => 'INV-2025-1042',
    ]);

    $hits = app(GlobalSearchService::class)->search('1042', ['invoices'], $company->id);

    expect($hits)->toHaveCount(1)
        ->and($hits[0]['type'])->toBe('sales_invoice')
        ->and($hits[0]['id'])->toBe($inv->id)
        ->and($hits[0]['number'])->toBe('INV-2025-1042')
        ->and($hits[0]['route'])->toBe("/sales/invoices?viewId={$inv->id}");
});
```

- [ ] **Step 2 — Run it, confirm it fails.** Run: `cd /home/moonui2/moon-erp-be && php artisan test --filter=GlobalSearchTest` → Expected: FAIL ("Class GlobalSearchService not found"). *(First confirm the real `SalesInvoice` model FQCN, number column, and factory exist — adjust the test to the actual names before proceeding.)*

- [ ] **Step 3 — Implement `GlobalSearchService` (invoices only for now).**

```php
<?php
namespace Modules\Core\Services;

use Modules\Sales\Models\SalesInvoice;

class GlobalSearchService
{
    /** @return array<int,array<string,mixed>> */
    public function search(string $q, array $types, int $companyId, int $limit = 8): array
    {
        $q = trim($q);
        if ($q === '') {
            return [];
        }
        $hits = [];
        if (in_array('invoices', $types, true)) {
            $hits = array_merge($hits, $this->salesInvoices($q, $companyId, $limit));
        }
        return $hits;
    }

    /** @return array<int,array<string,mixed>> */
    private function salesInvoices(string $q, int $companyId, int $limit): array
    {
        $like = '%'.$q.'%';
        return SalesInvoice::query()
            ->where('company_id', $companyId)
            ->where(fn ($w) => $w->where('invoice_number', 'like', $like)
                ->orWhereHas('customer', fn ($c) => $c->where('name', 'like', $like)))
            ->with('customer:id,name')
            ->latest('id')->limit($limit)->get()
            ->map(fn ($inv) => [
                'type' => 'sales_invoice',
                'id' => $inv->id,
                'number' => (string) $inv->invoice_number,
                'title' => (string) ($inv->customer->name ?? ''),
                'subtitle' => 'Sales · '.optional($inv->invoice_date)->format('Y-m-d'),
                'amount' => number_format((float) $inv->total, 2),
                'status' => (string) ($inv->status?->value ?? $inv->status ?? ''),
                'group' => 'Sales Invoices',
                'route' => "/sales/invoices?viewId={$inv->id}",
            ])->all();
    }
}
```
*(Adjust column names — `invoice_number`, `invoice_date`, `total`, `customer` relation — to the real schema; verify with `php artisan db:table sales_invoices` or the model.)*

- [ ] **Step 4 — VERIFY: run the test, confirm PASS.** Run: `php artisan test --filter=GlobalSearchTest` → Expected: PASS (1 passed).
- [ ] **Step 5 — Review gate.** A reviewer confirms: test green, query is company-scoped, `route` exactly matches the `?viewId=` contract. ✅ before continuing.
- [ ] **Step 6 — Commit.** `chown moonui2:moonui2` the new files, then `git add Modules/Core/... && git commit -m "feat(core): global search service — sales invoices"`.

---

## Task 2: BE — search endpoint + permission/company scoping + orders & customers

**Files:**
- Create: `Modules/Core/app/Http/Controllers/GlobalSearchController.php`
- Modify: `Modules/Core/routes/api.php` (add the route near the other `Route::get` Core routes)
- Modify: `Modules/Core/app/Services/GlobalSearchService.php` (add `salesOrders()` + `customers()`)
- Test: `Modules/Core/tests/Feature/GlobalSearchTest.php` (add cases)

**Interfaces:**
- Consumes: `GlobalSearchService::search()` from Task 1.
- Produces: `GET /api/core/search?q=&types=&limit=` → `{ data: SearchHit[] }`.

- [ ] **Step 1 — Failing test for the HTTP endpoint + permission filtering.**

```php
it('returns hits over http, company-scoped and permission-filtered', function () {
    $user = loginUser(); // use the project's existing auth test helper
    SalesInvoice::factory()->create(['company_id' => $user->company_id, 'invoice_number' => 'INV-9001']);
    SalesInvoice::factory()->create(['company_id' => 9999, 'invoice_number' => 'INV-9001']); // other company

    $res = $this->getJson('/api/core/search?q=INV-9001&types=invoices');

    $res->assertOk()->assertJsonCount(1, 'data');
    expect($res->json('data.0.number'))->toBe('INV-9001');
});
```
*(Find the real login helper: `grep -rn "function loginUser\|actingAs\|Sanctum::actingAs" Modules/*/tests tests`.)*

- [ ] **Step 2 — Run, confirm FAIL** (`404` / route missing). `php artisan test --filter=GlobalSearchTest`.

- [ ] **Step 3 — Controller** `GlobalSearchController.php`:

```php
<?php
namespace Modules\Core\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Modules\Core\Services\GlobalSearchService;

class GlobalSearchController extends Controller
{
    public function __construct(private readonly GlobalSearchService $service) {}

    public function index(Request $request): \Illuminate\Http\JsonResponse
    {
        $data = $request->validate([
            'q' => ['required', 'string', 'max:120'],
            'types' => ['nullable', 'string'],
            'limit' => ['nullable', 'integer', 'min:1', 'max:25'],
        ]);
        $types = $data['types'] ?? null
            ? explode(',', $data['types'])
            : ['invoices', 'orders', 'customers'];

        // permission filter: drop a type the user can't view (mirror how other
        // controllers read the user's permissions — Spatie `hasPermissionTo`).
        $user = $request->user();
        $allowed = array_values(array_filter($types, fn ($t) => match ($t) {
            'invoices' => $user->can('sales.invoices'),
            'orders'   => $user->can('sales.orders'),
            'customers'=> $user->can('core.partners') || $user->can('sales.'),
            default    => false,
        }));

        $hits = $this->service->search(
            $data['q'], $allowed, $this->companyId($request), (int) ($data['limit'] ?? 8),
        );
        return response()->json(['data' => $hits]);
    }

    private function companyId(Request $request): int
    {
        // reuse the project's active-company resolution (mirror an existing
        // controller — e.g. how SalesInvoiceController gets company_id).
        return (int) ($request->user()->company_id);
    }
}
```
*(Verify `$user->can('sales.invoices')` is how Spatie permissions read here; confirm with an existing controller. Adjust `companyId()` to the real active-company resolver if multi-company.)*

- [ ] **Step 4 — Register the route** in `Modules/Core/routes/api.php` (add with the other `Route::get` lines, and the `use` import at top):

```php
use Modules\Core\Http\Controllers\GlobalSearchController;
// ...
Route::get('search', [GlobalSearchController::class, 'index'])->name('search');
```

- [ ] **Step 5 — Add `salesOrders()` + `customers()`** to `GlobalSearchService` (same shape as `salesInvoices`, with routes `"/sales/orders?viewId={$id}"` and `"/core/partners?viewId={$id}"`; group labels "Sales Orders" / "Customers"). Wire them in `search()` under `in_array('orders'...)` / `in_array('customers'...)`.

- [ ] **Step 6 — VERIFY (test).** `php artisan test --filter=GlobalSearchTest` → all PASS.
- [ ] **Step 7 — VERIFY (live curl).** After `chown` + `bash local-deploy.sh`:
```bash
TOKEN=<a valid moonui2 token>
curl -s "https://moonui2.elbaset.com/moon-erp-be/api/core/search?q=INV&types=invoices,orders,customers" \
  -H "X-Authorization: Bearer $TOKEN" | head -c 600
```
Expected: JSON `{"data":[{"type":"sales_invoice",...,"route":"/sales/invoices?viewId=..."}]}`.
- [ ] **Step 8 — Review gate.** Reviewer confirms: tests green, the live curl returns real hits, a cross-company row is NOT returned, a type the token lacks permission for is absent. ✅
- [ ] **Step 9 — Commit** (chown first): `git commit -m "feat(core): /api/core/search endpoint (invoices, orders, customers)"`.

---

## Task 3: FE — search model + `GlobalSearchService`

**Files:**
- Create: `core/models/search-hit.model.ts`
- Create: `core/services/global-search.service.ts`

**Interfaces:**
- Produces: `GlobalSearchService.search(q: string, types?: string[]): Observable<SearchHit[]>`; `SearchHit` interface (mirrors the BE contract exactly).

- [ ] **Step 1 — Model** `search-hit.model.ts`:

```typescript
export interface SearchHit {
  type: 'sales_invoice' | 'sales_order' | 'customer';
  id: number;
  number: string;
  title: string;
  subtitle: string;
  amount: string | null;
  status: string | null;
  group: string;
  route: string;
}
```

- [ ] **Step 2 — Service** `global-search.service.ts`:

```typescript
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { environment } from '../../../environments/environment';
import { SearchHit } from '../models/search-hit.model';

@Injectable({ providedIn: 'root' })
export class GlobalSearchService {
  private http = inject(HttpClient);
  private url = `${environment.apiUrl}/core/search`;

  search(q: string, types?: string[]): Observable<SearchHit[]> {
    let params = new HttpParams().set('q', q);
    if (types?.length) params = params.set('types', types.join(','));
    return this.http.get<{ data: SearchHit[] }>(this.url, { params })
      .pipe(map((r) => r.data ?? []));
  }
}
```

- [ ] **Step 3 — VERIFY (build).** `cd /home/moonui2/public_html/moon-erp && node_modules/.bin/ng build --base-href /app/` → Expected: "Application bundle generation complete", zero errors.
- [ ] **Step 4 — Review gate.** Reviewer confirms `SearchHit` matches the BE contract field-for-field. ✅
- [ ] **Step 5 — Commit.** `git commit -m "feat(search): FE search model + service"`.

---

## Task 4: FE — `CommandBarService` (open state, ⌘K, debounced search, recent)

**Files:**
- Create: `core/services/command-bar.service.ts`

**Interfaces:**
- Consumes: `GlobalSearchService.search`.
- Produces: signals `open` (`WritableSignal<boolean>`), `query`, `results` (`Signal<SearchHit[]>`), `selectedIndex`; methods `openBar()`, `close()`, `setQuery(q)`, `move(delta)`, `recents(): SearchHit[]`, `pushRecent(hit)`.

- [ ] **Step 1 — Implement** `command-bar.service.ts`:

```typescript
import { Injectable, inject, signal, computed } from '@angular/core';
import { Subject } from 'rxjs';
import { debounceTime, switchMap } from 'rxjs/operators';
import { GlobalSearchService } from './global-search.service';
import { SearchHit } from '../models/search-hit.model';

const RECENT_KEY = 'cmdbar.recent';

@Injectable({ providedIn: 'root' })
export class CommandBarService {
  private search = inject(GlobalSearchService);
  open = signal(false);
  query = signal('');
  results = signal<SearchHit[]>([]);
  selectedIndex = signal(0);
  private q$ = new Subject<string>();

  constructor() {
    this.q$.pipe(
      debounceTime(200),
      switchMap((q) => q.trim() ? this.search.search(q) : Promise.resolve([] as SearchHit[])),
    ).subscribe((hits) => { this.results.set(hits as SearchHit[]); this.selectedIndex.set(0); });
  }

  openBar(): void { this.open.set(true); this.query.set(''); this.results.set(this.recents()); this.selectedIndex.set(0); }
  close(): void { this.open.set(false); }
  setQuery(q: string): void { this.query.set(q); this.q$.next(q); }
  move(delta: number): void {
    const n = this.results().length; if (!n) return;
    this.selectedIndex.set((this.selectedIndex() + delta + n) % n);
  }
  recents(): SearchHit[] { try { return JSON.parse(localStorage.getItem(RECENT_KEY) || '[]'); } catch { return []; } }
  pushRecent(hit: SearchHit): void {
    const list = [hit, ...this.recents().filter((h) => !(h.type === hit.type && h.id === hit.id))].slice(0, 6);
    localStorage.setItem(RECENT_KEY, JSON.stringify(list));
  }
}
```

- [ ] **Step 2 — VERIFY (build).** `ng build` → zero errors.
- [ ] **Step 3 — Review gate.** Reviewer confirms debounce + recents persistence logic. ✅
- [ ] **Step 4 — Commit.** `git commit -m "feat(search): command-bar service (state, debounce, recents)"`.

---

## Task 5: FE — `command-bar` overlay component (dark/amber theme)

**Files:**
- Create: `layout/command-bar/command-bar.component.ts` `.html` `.scss`

**Interfaces:**
- Consumes: `CommandBarService` (signals + methods), `Router`.
- Produces: `<app-command-bar>` standalone component; opens on `CommandBarService.open()`; Enter/click → `router.navigateByUrl(hit.route)` + `pushRecent` + `close`.

- [ ] **Step 1 — Component `.ts`** (skeleton — full keyboard handling + grouping):

```typescript
import { Component, inject, HostListener, computed } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router } from '@angular/router';
import { CommandBarService } from '../../core/services/command-bar.service';
import { SearchHit } from '../../core/models/search-hit.model';

@Component({
  selector: 'app-command-bar',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './command-bar.component.html',
  styleUrl: './command-bar.component.scss',
})
export class CommandBarComponent {
  bar = inject(CommandBarService);
  private router = inject(Router);

  grouped = computed(() => {
    const g: Record<string, SearchHit[]> = {};
    for (const h of this.bar.results()) (g[h.group] ??= []).push(h);
    return Object.entries(g).map(([group, items]) => ({ group, items }));
  });

  @HostListener('document:keydown', ['$event'])
  onKey(e: KeyboardEvent): void {
    if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); this.bar.openBar(); return; }
    if (!this.bar.open()) return;
    if (e.key === 'Escape') this.bar.close();
    else if (e.key === 'ArrowDown') { e.preventDefault(); this.bar.move(1); }
    else if (e.key === 'ArrowUp') { e.preventDefault(); this.bar.move(-1); }
    else if (e.key === 'Enter') { e.preventDefault(); this.go(this.bar.results()[this.bar.selectedIndex()]); }
  }

  go(hit?: SearchHit): void {
    if (!hit) return;
    this.bar.pushRecent(hit); this.bar.close();
    this.router.navigateByUrl(hit.route);
  }
}
```

- [ ] **Step 2 — Template `.html`** — overlay + input (`(input)="bar.setQuery($any($event.target).value)"`), grouped results (`@for` over `grouped()`), highlight `bar.selectedIndex()`, footer hints. **Reuse the exact markup + classes from the mockup** `knowledge-base/plans/global-command-bar-plan.html` (the `.pal*` / `.res` styles) so the look matches what was approved.
- [ ] **Step 3 — Styles `.scss`** — copy the palette tokens + `.pal`/`.res`/`.sel` rules from the mockup; use the app's `--accent`/`--brand` CSS vars.
- [ ] **Step 4 — VERIFY (build).** `ng build` → zero errors.
- [ ] **Step 5 — Review gate.** Reviewer confirms the component renders grouped results and Enter navigates. (Visual check after Task 6 mounts it.) ✅
- [ ] **Step 6 — Commit.** `git commit -m "feat(search): command-bar overlay component"`.

---

## Task 6: FE — mount the command bar in the topbar (trigger + ⌘K)

**Files:**
- Modify: `layout/topbar/topbar.component.ts` (import + add to `imports:`)
- Modify: `layout/topbar/topbar.component.html` (search trigger button + `<app-command-bar>`)

**Interfaces:**
- Consumes: `CommandBarComponent`, `CommandBarService`.

- [ ] **Step 1** — In `topbar.component.ts`: `import { CommandBarComponent } from '../command-bar/command-bar.component';` and add `CommandBarComponent` to the standalone `imports:` array; `bar = inject(CommandBarService);`.
- [ ] **Step 2** — In `topbar.component.html`: add the trigger (mirrors the mockup `.search-trigger`) calling `(click)="bar.openBar()"`, and mount `<app-command-bar />` once.

```html
<button class="topbar-search" type="button" (click)="bar.openBar()">
  <i class="pi pi-search"></i>
  <span>{{ langService.isRtl() ? 'ابحث عن أي مستند…' : 'Search any document…' }}</span>
  <kbd>⌘K</kbd>
</button>
<app-command-bar />
```

- [ ] **Step 3 — VERIFY (build + deploy + manual).** `ng build` → deploy to `/app` → on **moonui2.elbaset.com** press **Ctrl+K**: the palette opens; type an invoice number → a hit appears → Enter → the sales-invoices page opens with that invoice's view dialog (`?viewId=`). Hard-refresh first.
- [ ] **Step 4 — Review gate.** Reviewer confirms END-TO-END on the live site: ⌘K → type → Enter → invoice opens. ✅ (This is the headline acceptance test.)
- [ ] **Step 5 — Commit.** `git commit -m "feat(search): mount command bar in topbar (Ctrl+K)"`.

---

## Task 7: FE — extend `?viewId=` deep-link to sales orders & purchase bills + copy-link affordance

**Files:**
- Modify: `features/sales/orders/orders.component.ts`
- Modify: `features/purchases/bills/bills.component.ts`
- Modify: `features/sales/invoices/invoices.component.html`

**Interfaces:**
- Consumes: existing per-component `viewX()` open method + `service.getById`.

- [ ] **Step 1 — Sales orders:** in `orders.component.ts`, in `ngOnInit`, mirror the invoices pattern exactly:

```typescript
this.route.queryParams.subscribe((p) => {
  const id = parseInt(p['viewId'], 10);
  if (!isNaN(id)) this.service.getById(id).subscribe({ next: (r) => { if (r.data) this.viewOrder(r.data); } });
});
```
*(Use the order component's real view method name — confirm with `grep -n "viewDialogVisible\|view" orders.component.ts`.)*

- [ ] **Step 2 — Purchase bills:** same pattern in `bills.component.ts` (real `viewBill`/`view` method).
- [ ] **Step 3 — Copy-link / new-tab** in the invoice view-dialog header (`invoices.component.html`): two buttons —
  - Copy: `navigator.clipboard.writeText(location.origin + '/sales/invoices?viewId=' + viewingInvoice()!.id)`
  - New tab: `<a [href]="'/sales/invoices?viewId=' + viewingInvoice()!.id" target="_blank">`
- [ ] **Step 4 — VERIFY (build + manual).** `ng build` → deploy → open `…/sales/orders?viewId=<real id>` directly in the browser → the order opens. Same for bills. Click "Copy link" on an invoice → paste in a new tab → it opens. ✅
- [ ] **Step 5 — Review gate.** Reviewer confirms all three deep-links open the right document from a cold URL. ✅
- [ ] **Step 6 — Commit.** `git commit -m "feat(search): viewId deep-links for orders + bills, copy-link on invoice"`.

---

## Task 8: 🔴 Changelog (What's New) + push + merge — SO IT SHIPS

**Files:**
- Modify: `moon-erp-be/docs/moonstack/CHANGELOG.md`

- [ ] **Step 1 — Add a bullet under `## [Unreleased]`** (English primary, then `{{ar}}` Arabic), e.g.:

```markdown
- **Find any document from anywhere with the new ⌘K command bar — and open it by its own link.** A search box in the top bar (or Ctrl/⌘+K) finds any sales invoice, sales order or customer by number or name from any screen and jumps straight to it; documents now open via a shareable link you can bookmark or open in a new tab. {{ar}} **لاقِ أي مستند من أي مكان عبر شريط الأوامر الجديد ⌘K — وافتحه بلينكه الخاص.** صندوق بحث في الشريط العلوي (أو Ctrl/⌘+K) بيلاقي أي فاتورة بيع أو أمر بيع أو عميل بالرقم أو الاسم من أي شاشة ويوديك له على طول؛ والمستندات بقت تتفتح بلينك تقدر تحفظه أو تفتحه في تاب جديد.
```

- [ ] **Step 2 — VERIFY the file** is owned by `moonui2` (`chown moonui2:moonui2 docs/moonstack/CHANGELOG.md`) so `ship` can later rewrite it. Commit: `git commit -m "docs(changelog): note global command bar + deep links"`.
- [ ] **Step 3 — 🔴 PUSH so it accumulates for the release.** Push BOTH repos' `hazemdev2`, then fast-forward `main`:

```bash
for D in /home/moonui2/moon-erp-be /home/moonui2/public_html/moon-erp; do
  git -C "$D" push origin hazemdev2
  git -C "$D" fetch origin main --quiet
  [ "$(git -C "$D" rev-list --count hazemdev2..origin/main)" = 0 ] \
    && git -C "$D" push origin hazemdev2:main || echo "main diverged — merge worktree needed"
done
```

- [ ] **Step 4 — VERIFY pushed.** `git -C <repo> rev-parse --short origin/main hazemdev2` → equal for both repos. Confirm the bullet is in `origin/main`'s `CHANGELOG.md`. ✅ **Until this passes, the feature is NOT in the next release.**
- [ ] **Step 5 — Review gate.** Reviewer confirms `[Unreleased]` carries the bullet on `origin/main`, ready for `moonstack:ship` to promote.

---

## Definition of Done (whole plan)
- [ ] All 8 tasks' Review gates ticked.
- [ ] BE: `php artisan test --filter=GlobalSearchTest` green.
- [ ] FE: `ng build` zero errors; deployed to moonui2 `/app`.
- [ ] Live: Ctrl+K → type a number → Enter → the document opens (the headline test).
- [ ] Deep-links open from a cold URL for invoices, orders, bills.
- [ ] Changelog bullet present on `origin/main` `[Unreleased]`.

## Out of scope (Phase 2 — separate plan)
Expand search to inventory/lab/accounting/products/suppliers · recent & pinned in the topbar · scopes + shorthand (`#1042`, `@customer`) · quick actions ("New invoice") from the bar. See the concept HTML §④.

## Self-review notes (author)
- Spec coverage: concept §①–③ → Tasks 1–7; §④ Phase-1 items (deep-link + command bar + `GET /search`) all covered; Phase-2 explicitly deferred.
- Reality check baked in: `?viewId=` already exists on invoices (`invoices.component.ts:471`) → Task 7 reuses it, not reinvents. FE has no test runner → FE tasks verify by build + live check, BE by Pest.
- Open assumptions the implementer MUST confirm before coding: exact `SalesInvoice` model FQCN/columns, the auth test helper, Spatie `->can()` permission names, and the active-company resolver — each flagged inline.
