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
@@ -0,0 +1,99 @@
<script lang="ts">
import { accordion } from '$lib/actions/accordion';
import Icon from '$lib/components/ui/Icon.svelte';
import type { FaqItem } from '$lib/types';
export let title = 'FAQs';
export let intro: string | undefined = undefined;
export let faqs: FaqItem[];
export let emitSchema = true;
export let variant: 'panel' | 'plain' = 'panel';
$: schemaJson = JSON.stringify({
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqs.map((faq) => ({
'@type': 'Question',
name: faq.question,
acceptedAnswer: {
'@type': 'Answer',
text: faq.answer
}
}))
});
</script>
<svelte:head>
{#if emitSchema && faqs.length}
{@html `<script type="application/ld+json">${schemaJson}<` + `/script>`}
{/if}
</svelte:head>
<div class:faq-section-plain={variant === 'plain'} class="faq-section">
<h2 class="faq-section-heading">
<span class="faq-section-icon"><Icon name="fas fa-circle-question" /></span>
{title}
</h2>
{#if intro}
<p class="faq-section-intro">{intro}</p>
{/if}
<div use:accordion class="faq">
{#each faqs as faq}
<details>
<summary>{faq.question}</summary>
<p>{faq.answer}</p>
</details>
{/each}
</div>
</div>
<style>
.faq-section-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
margin-right: 10px;
border-radius: 12px;
background: var(--gw-green);
box-shadow: 0 10px 22px rgba(33, 48, 33, 0.16);
vertical-align: middle;
}
.faq-section-icon :global(.icon) {
color: var(--yellow);
font-size: 16px;
}
.faq-section-heading {
margin: 0 0 14px;
}
.faq-section-intro {
margin: 0 0 18px;
color: #5b6067;
font-size: 16px;
line-height: 1.6;
}
@media (max-width: 768px) {
.faq-section-icon {
display: none;
}
.faq-section-heading {
text-align: left;
white-space: normal;
overflow-wrap: anywhere;
text-wrap: balance;
font-size: clamp(22px, 5.6vw, 26px);
line-height: 1.18;
}
.faq summary,
.faq details p {
text-align: left;
}
}
</style>