Component architecture
- Reorganise src/lib/components into pages/, sections/, ui/ subdirectories and
  update all imports across routes and tests.

Owner admin dashboard (+2k lines)
- Scheduled welcome-pack emails: durable queue with cancel/reschedule and a
  background sender loop (SCHEDULED_CHECK_INTERVAL_SECONDS, default 60s).
- Custom welcome-pack subject line, preview recipients, and client BCC.
- Add-client, edit client profile, and reset-onboarding flows.
- "View as client" onboarding preview (owner impersonation, dry-run submit).
- Tabbed owner welcome route (/owner/welcome/[[tab]]).

MYOB integration
- New mail_api/myob.py: create new clients as MYOB AccountRight customer
  contacts. Disabled until all MYOB_* env vars are set (no-op otherwise).

Onboarding
- New "Does your dog resource guard?" Yes/No question in the Behaviour step.
- Persist vetAddress, flea/tick, and pet-insurance fields server-side.

Misc
- vite config, new hooks.ts, responsive CSS tweaks, deploy/docker config,
  mail-api README and start-dev.ps1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 21:51:29 +12:00
co-authored by Claude Opus 4.8
parent 7d5b72e1d3
commit 0e88e7bdac
82 changed files with 4543 additions and 914 deletions
+68
View File
@@ -0,0 +1,68 @@
import { fireEvent, render } from '@testing-library/svelte';
import { describe, expect, it } from 'vitest';
import Header from './Header.svelte';
import { homepageContent } from '$lib/content/homepage';
import { setMockPage } from '../../../test/mocks/app-stores';
describe('Header', () => {
it('marks the services link active for service detail pages', () => {
setMockPage('https://www.goodwalk.co.nz/pack-walks');
const { container } = render(Header, {
navigation: homepageContent.navigation
});
expect(container.querySelector('a.nav-link-active[href="#services"]')).toBeTruthy();
});
it('opens and closes the mobile menu', async () => {
Object.defineProperty(window, 'innerWidth', {
configurable: true,
writable: true,
value: 390
});
const { container } = render(Header, {
navigation: homepageContent.navigation
});
const menuToggle = container.querySelector('.hamburger') as HTMLButtonElement;
const mobileMenu = container.querySelector('.mobile-menu') as HTMLDivElement;
const mobileMenuShell = container.querySelector('.mobile-menu-shell') as HTMLDivElement;
const firstMobileLink = mobileMenu.querySelector('a') as HTMLAnchorElement;
expect(menuToggle).toHaveAttribute('aria-expanded', 'false');
await fireEvent.click(menuToggle);
expect(menuToggle).toHaveAttribute('aria-expanded', 'true');
expect(mobileMenuShell.classList.contains('open')).toBe(true);
await fireEvent.click(firstMobileLink);
expect(menuToggle).toHaveAttribute('aria-expanded', 'false');
expect(mobileMenuShell.classList.contains('open')).toBe(false);
});
it('closes the mobile menu when tapping outside the menu panel', async () => {
Object.defineProperty(window, 'innerWidth', {
configurable: true,
writable: true,
value: 390
});
const { container } = render(Header, {
navigation: homepageContent.navigation
});
const menuToggle = container.querySelector('.hamburger') as HTMLButtonElement;
const mobileMenuShell = container.querySelector('.mobile-menu-shell') as HTMLDivElement;
const mobileMenuBackdrop = container.querySelector('.mobile-menu-backdrop') as HTMLButtonElement;
await fireEvent.click(menuToggle);
expect(menuToggle).toHaveAttribute('aria-expanded', 'true');
expect(mobileMenuShell.classList.contains('open')).toBe(true);
await fireEvent.click(mobileMenuBackdrop);
expect(menuToggle).toHaveAttribute('aria-expanded', 'false');
expect(mobileMenuShell.classList.contains('open')).toBe(false);
});
});