feat(receipts): upload guide page + scanner head-tag fix
Adds /invoices/upload-receipts as the dedicated explainer for the mobile scanner PWA: install instructions for iOS/Android, direct deep-link button, and a walkthrough of the scan -> verify -> save flow. Sidebar entry replaces the old "Scan receipt" tab so the desktop side picks up the install steps before sending users to the mobile-only surface. Scanner layout moves PWA manifest + apple-* meta tags from inline JSX into Next.js's metadata/viewport exports so the App Router doesn't try to render a second <head>, fixing a hydration error that surfaced as two console warnings on the scan page. Scanner shell gains a centered Port Nimara logo header so the standalone PWA looks branded when launched from the home screen without the dashboard chrome. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
248
src/components/invoices/upload-receipts-guide.tsx
Normal file
248
src/components/invoices/upload-receipts-guide.tsx
Normal file
@@ -0,0 +1,248 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* Explainer page that lives under /<portSlug>/invoices/upload-receipts.
|
||||
*
|
||||
* The actual scanner UI is mobile-only and lives at /<portSlug>/scan as a
|
||||
* standalone PWA surface. This page tells operators how to install it on
|
||||
* their phone's home screen and walks through the workflow. It also
|
||||
* surfaces a direct "Open scanner" button as a fallback for users who
|
||||
* just want to navigate without installing.
|
||||
*/
|
||||
|
||||
import Link from 'next/link';
|
||||
import { Camera, Smartphone, Apple, Globe, ArrowRight, Sparkles } from 'lucide-react';
|
||||
|
||||
import { PageHeader } from '@/components/shared/page-header';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface Props {
|
||||
portSlug: string;
|
||||
}
|
||||
|
||||
export function UploadReceiptsGuide({ portSlug }: Props) {
|
||||
const scannerUrl = `/${portSlug}/scan`;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="How to upload receipts for reimbursement"
|
||||
eyebrow="Business expenses"
|
||||
description="When you spend your own money on a business expense for the marina, use this to log it. Snap a photo of the receipt with your phone, the system reads it for you, and finance approves it on the parent company's side."
|
||||
variant="gradient"
|
||||
actions={
|
||||
<Button asChild>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
||||
<Link href={scannerUrl as any}>
|
||||
<Camera className="mr-2 h-4 w-4" />
|
||||
Open scanner
|
||||
</Link>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* What it does, in plain English */}
|
||||
<section className="rounded-xl border border-border bg-card p-5 shadow-xs">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-brand/10 text-brand">
|
||||
<Sparkles className="h-5 w-5" aria-hidden />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="text-base font-semibold">What does it actually do?</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
You paid out of pocket for something the marina needs (fuel, hardware, a part run,
|
||||
lunch with a broker). Snap a photo of the receipt and this tool turns it into a
|
||||
reimbursement request. It pulls out the vendor, the date, the total, and the currency,
|
||||
drops them into the expense form, and queues the whole thing for the parent
|
||||
company's finance team to approve and pay you back.
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
The behind-the-scenes part is called OCR (short for “optical character
|
||||
recognition”). Think of it as a fancy phone camera that knows how to read
|
||||
printed text. Combined with a bit of AI to figure out which number is the total and
|
||||
which is the tax, it turns a paper receipt into a ready-to-save expense in about five
|
||||
seconds. No typing. No spreadsheets. No chasing finance for the form.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Two-step explainer: install first, then use */}
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<Step
|
||||
number={1}
|
||||
icon={<Smartphone className="h-5 w-5" />}
|
||||
title="Add the scanner to your phone"
|
||||
description="One-time setup. After this, the scanner opens like a normal app from your home screen."
|
||||
>
|
||||
<PlatformBlock
|
||||
icon={<Apple className="h-4 w-4" />}
|
||||
label="iPhone or iPad (Safari)"
|
||||
steps={[
|
||||
<>
|
||||
Open{' '}
|
||||
<Link
|
||||
href={scannerUrl as never}
|
||||
className="text-brand underline-offset-2 hover:underline"
|
||||
>
|
||||
this link
|
||||
</Link>{' '}
|
||||
in Safari on your phone.
|
||||
</>,
|
||||
'Tap the Share button at the bottom of the screen (the square with the arrow pointing up).',
|
||||
<>
|
||||
Scroll down and tap <span className="font-medium">Add to Home Screen</span>.
|
||||
</>,
|
||||
'Confirm the name "Scanner" and tap Add. The icon now sits on your home screen.',
|
||||
]}
|
||||
/>
|
||||
<PlatformBlock
|
||||
icon={<Globe className="h-4 w-4" />}
|
||||
label="Android phone (Chrome)"
|
||||
steps={[
|
||||
<>
|
||||
Open{' '}
|
||||
<Link
|
||||
href={scannerUrl as never}
|
||||
className="text-brand underline-offset-2 hover:underline"
|
||||
>
|
||||
this link
|
||||
</Link>{' '}
|
||||
in Chrome on your phone.
|
||||
</>,
|
||||
'Tap the three-dot menu in the top-right corner.',
|
||||
<>
|
||||
Tap <span className="font-medium">Install app</span> (older versions of Chrome say{' '}
|
||||
<span className="font-medium">Add to Home screen</span>).
|
||||
</>,
|
||||
'Confirm to install. The icon now sits on your home screen.',
|
||||
]}
|
||||
/>
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
number={2}
|
||||
icon={<Camera className="h-5 w-5" />}
|
||||
title="Snap a photo of a receipt"
|
||||
description="Open the scanner from your home screen and follow the prompts. The whole thing takes about ten seconds."
|
||||
>
|
||||
<ol className="space-y-3 pl-4 text-sm text-muted-foreground list-decimal">
|
||||
<li>
|
||||
<span className="font-medium text-foreground">Tap the camera tile.</span> Your phone
|
||||
opens its camera. Hold the receipt flat, get the whole thing in the frame, and snap.
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-medium text-foreground">Wait a few seconds.</span> The system
|
||||
reads the receipt and fills in the merchant, date, total, and currency for you. A
|
||||
loading spinner shows while this happens.
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-medium text-foreground">Glance over the numbers.</span> Most of
|
||||
the time everything is correct. If something looks off (wrong total, wrong category),
|
||||
tap the field and fix it. The category is the field you most often need to set
|
||||
yourself.
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-medium text-foreground">Tap Save.</span> The receipt becomes a
|
||||
pending expense ready for reimbursement. The parent company's finance team will
|
||||
review it on the{' '}
|
||||
<Link
|
||||
href={`/${portSlug}/expenses` as never}
|
||||
className="text-brand underline-offset-2 hover:underline"
|
||||
>
|
||||
Expenses page
|
||||
</Link>{' '}
|
||||
and approve it for payback. You can check the status of any expense you submitted from
|
||||
there too.
|
||||
</li>
|
||||
</ol>
|
||||
</Step>
|
||||
</div>
|
||||
|
||||
{/* Tips */}
|
||||
<div className="rounded-xl border border-dashed border-border bg-muted/30 p-5">
|
||||
<h3 className="text-sm font-semibold">Tips for the best results</h3>
|
||||
<ul className="mt-2 space-y-1.5 text-sm text-muted-foreground list-disc pl-5">
|
||||
<li>
|
||||
Get the whole receipt in the frame. If the edges are cut off, the total or date might be
|
||||
missed and finance might bounce it back to you.
|
||||
</li>
|
||||
<li>Hold the camera steady. Blurry photos are harder to read. Retake if needed.</li>
|
||||
<li>
|
||||
Receipts in foreign currencies are fine. The scanner picks up the currency code if it is
|
||||
printed on the slip. The parent company handles the conversion when they reimburse you.
|
||||
</li>
|
||||
<li>If the camera looks dim, just turn on a light. Bright, even lighting works best.</li>
|
||||
<li>
|
||||
Add a quick note in the description if the expense needs context (who you met, what the
|
||||
part was for, etc.). Saves finance from having to ask.
|
||||
</li>
|
||||
<li>
|
||||
Lost the home-screen icon? Open this page on your phone again and tap the{' '}
|
||||
<span className="font-medium">Open scanner</span> button at the top.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Step({
|
||||
number,
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
}: {
|
||||
number: number;
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section className="rounded-xl border border-border bg-card p-5 shadow-xs">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-brand/10 text-brand">
|
||||
{icon}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Step {number}
|
||||
</div>
|
||||
<h2 className="mt-0.5 text-base font-semibold">{title}</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 space-y-4">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function PlatformBlock({
|
||||
icon,
|
||||
label,
|
||||
steps,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
steps: React.ReactNode[];
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-lg border border-border/60 bg-background/50 p-3">
|
||||
<div className="flex items-center gap-2 text-sm font-medium">
|
||||
{icon}
|
||||
{label}
|
||||
</div>
|
||||
<ol className="mt-2 space-y-1.5 pl-5 text-sm text-muted-foreground list-decimal">
|
||||
{steps.map((s, i) => (
|
||||
<li key={i}>{s}</li>
|
||||
))}
|
||||
</ol>
|
||||
<div className="mt-2 flex items-center text-xs text-muted-foreground/80">
|
||||
<ArrowRight className="mr-1 h-3 w-3" aria-hidden />
|
||||
Done. The scanner now opens from your home screen like a normal app.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user