Add Help page with full User Guide

Covers: overview, sheets, AI commands, brief extraction, export CSV,
export templates, admin (clients, dropdowns, users), and login/emergency
access. Admin-only sections are hidden for regular users.
Accessible via sidebar "Help" link at /help.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-03-23 19:32:17 +00:00
parent 6c93915768
commit 8f57c657fa
3 changed files with 440 additions and 0 deletions

View file

@ -15,6 +15,7 @@ import AdminUsersPage from './pages/admin/AdminUsersPage'
import AdminDropdownsPage from './pages/admin/AdminDropdownsPage'
import AdminClientsPage from './pages/admin/AdminClientsPage'
import LoginPage from './pages/LoginPage'
import HelpPage from './pages/HelpPage'
function AuthGate({ children }: { children: React.ReactNode }) {
const { instance, inProgress, accounts } = useMsal()
@ -109,6 +110,7 @@ export default function App() {
<Route path="/admin/users" element={<AdminRoute><AdminUsersPage /></AdminRoute>} />
<Route path="/admin/dropdowns" element={<AdminRoute><AdminDropdownsPage /></AdminRoute>} />
<Route path="/admin/clients" element={<AdminRoute><AdminClientsPage /></AdminRoute>} />
<Route path="/help" element={<HelpPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</AppShell>

View file

@ -34,6 +34,13 @@ const IconPlus = () => (
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
</svg>
)
const IconHelp = () => (
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10"/>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>
)
export default function Sidebar() {
const navigate = useNavigate()
@ -125,6 +132,18 @@ export default function Sidebar() {
<IconUpload />
Upload Brief
</button>
<button
onClick={() => navigate('/help')}
className="w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm text-left transition-colors"
style={{
background: location.pathname === '/help' ? 'var(--accent-dim)' : 'transparent',
color: location.pathname === '/help' ? 'var(--accent)' : 'var(--text-secondary)',
}}
>
<IconHelp />
Help
</button>
</div>
{/* ── Divider ── */}

View file

@ -0,0 +1,419 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useAuthStore } from '../stores/useAuthStore'
// ── Section types ──────────────────────────────────────────────────────────────
interface Section {
id: string
icon: string
title: string
adminOnly?: boolean
content: React.ReactNode
}
// ── Reusable helpers ───────────────────────────────────────────────────────────
const H2 = ({ children }: { children: React.ReactNode }) => (
<h2 style={{ color: 'var(--text-primary)', fontSize: 16, fontWeight: 700, marginBottom: 12, marginTop: 0 }}>{children}</h2>
)
const H3 = ({ children }: { children: React.ReactNode }) => (
<h3 style={{ color: 'var(--accent)', fontSize: 13, fontWeight: 600, marginBottom: 8, marginTop: 20 }}>{children}</h3>
)
const P = ({ children }: { children: React.ReactNode }) => (
<p style={{ color: 'var(--text-secondary)', fontSize: 13, lineHeight: 1.7, marginBottom: 10 }}>{children}</p>
)
const Li = ({ children }: { children: React.ReactNode }) => (
<li style={{ color: 'var(--text-secondary)', fontSize: 13, lineHeight: 1.7, marginBottom: 4 }}>{children}</li>
)
const Code = ({ children }: { children: React.ReactNode }) => (
<code style={{
background: 'rgba(255,196,7,0.12)', color: 'var(--accent)',
padding: '1px 6px', borderRadius: 4, fontSize: 12, fontFamily: 'monospace',
}}>{children}</code>
)
const Tip = ({ children }: { children: React.ReactNode }) => (
<div style={{
background: 'rgba(255,196,7,0.07)', border: '1px solid rgba(255,196,7,0.2)',
borderRadius: 8, padding: '10px 14px', marginBottom: 12,
}}>
<span style={{ color: 'var(--accent)', fontWeight: 600, fontSize: 12 }}>Tip: </span>
<span style={{ color: 'var(--text-secondary)', fontSize: 13 }}>{children}</span>
</div>
)
const Steps = ({ items }: { items: string[] }) => (
<ol style={{ paddingLeft: 20, margin: '8px 0 12px' }}>
{items.map((item, i) => (
<li key={i} style={{ color: 'var(--text-secondary)', fontSize: 13, lineHeight: 1.7, marginBottom: 6 }}
dangerouslySetInnerHTML={{ __html: item }} />
))}
</ol>
)
// ── Sections ───────────────────────────────────────────────────────────────────
const SECTIONS: Section[] = [
{
id: 'overview',
icon: '📋',
title: 'Overview',
content: (
<>
<H2>What is AC Helper?</H2>
<P>
AC Helper is an Activation Calendar management tool for Oliver Agency.
It lets you create and manage deliverable sheets, extract deliverables from briefs using AI,
and export them in a format tailored to each client.
</P>
<H3>Core concepts</H3>
<ul style={{ paddingLeft: 20, margin: '0 0 12px' }}>
<Li><strong style={{ color: 'var(--text-primary)' }}>Sheet</strong> a spreadsheet of deliverables (rows). Each row is one ad/asset.</Li>
<Li><strong style={{ color: 'var(--text-primary)' }}>Brief extraction</strong> upload a client brief (PDF, PPTX, DOCX, XLSX) and AI reads it and populates a sheet automatically.</Li>
<Li><strong style={{ color: 'var(--text-primary)' }}>AI command</strong> type a plain-English instruction to add or modify rows in the sheet.</Li>
<Li><strong style={{ color: 'var(--text-primary)' }}>Client</strong> each client can have its own Category/Media hierarchy and CSV export format.</Li>
<Li><strong style={{ color: 'var(--text-primary)' }}>Export template</strong> define which columns appear in the exported CSV and what they're called.</Li>
</ul>
<H3>Navigation</H3>
<ul style={{ paddingLeft: 20, margin: '0 0 12px' }}>
<Li>The <strong style={{ color: 'var(--text-primary)' }}>sidebar</strong> (left) lists all your sheets. Click any sheet to open it.</Li>
<Li><strong style={{ color: 'var(--text-primary)' }}>Dashboard</strong> quick access to recent sheets and extractions.</Li>
<Li><strong style={{ color: 'var(--text-primary)' }}>Upload Brief</strong> start a new AI extraction.</Li>
<Li>Admins see <strong style={{ color: 'var(--text-primary)' }}>Users / Clients / Dropdowns</strong> links at the bottom of the sidebar.</Li>
</ul>
</>
),
},
{
id: 'sheets',
icon: '📄',
title: 'Sheets',
content: (
<>
<H2>Working with Sheets</H2>
<H3>Create a new sheet</H3>
<Steps items={[
'Click the <strong>+</strong> button next to "Sheets" in the sidebar, or click <strong>New Sheet</strong> on the Dashboard.',
'A blank sheet opens immediately. The name defaults to today\'s date — you can rename it.',
]} />
<H3>Rename, duplicate, delete</H3>
<P>Right-click any sheet in the sidebar to open the context menu with Rename, Duplicate and Delete options.</P>
<H3>Sheet columns</H3>
<ul style={{ paddingLeft: 20, margin: '0 0 12px' }}>
<Li><Code>Title</Code> deliverable name / description</Li>
<Li><Code>Status</Code> Booked / To-do / In Progress / Done</Li>
<Li><Code>Category</Code> task type (e.g. Social (Static), OOH Print). Dropdown values depend on the selected client.</Li>
<Li><Code>Media</Code> media channel. Dependent on Category.</Li>
<Li><Code>Sub-media</Code> sub-channel</Li>
<Li><Code>Format</Code> size/spec (e.g. 1080x1080)</Li>
<Li><Code>Supply date</Code> / <Code>Live date</Code> key dates</Li>
<Li><Code>Language</Code> / <Code>Country</Code> market</Li>
<Li><Code>Quantity</Code> number of units</Li>
</ul>
<H3>Editing cells</H3>
<P>Click any cell to edit. Category and Media are dropdowns the available values come from the selected client's hierarchy (or the global list if no client is set).</P>
<Tip>Changes are auto-saved a few seconds after you stop typing. The "Saving…" indicator appears in the header.</Tip>
<H3>Assign a client to a sheet</H3>
<P>In the sheet header, select a client from the <strong>Client</strong> dropdown. This loads that client's Category/Media hierarchy and their export template.</P>
<H3>Clear sheet</H3>
<P>Click <strong>Clear</strong> in the sheet header to remove all rows and reset the AI activity log.</P>
</>
),
},
{
id: 'ai',
icon: '🤖',
title: 'AI Commands',
content: (
<>
<H2>AI Commands</H2>
<P>
The command bar at the bottom of every sheet lets you add or modify deliverables using plain English.
The AI understands the client's Category/Media hierarchy and will only use valid values.
</P>
<H3>Example commands</H3>
<ul style={{ paddingLeft: 20, margin: '0 0 12px' }}>
<Li><Code>Add 5 social media banners for UK</Code></Li>
<Li><Code>Add 3 email newsletters for DE, FR, ES</Code></Li>
<Li><Code>Create 10 OOH Print A4 deliverables for Germany</Code></Li>
<Li><Code>Add 2 TVC 30s adaptations for France, supply 15 May, live 1 June</Code></Li>
<Li><Code>Change all UK rows to "In Progress"</Code></Li>
</ul>
<H3>YOLO mode</H3>
<P>
Toggle the <strong>YOLO</strong> switch to stop the AI from asking clarifying questions.
It will make its best guess and apply changes immediately. Useful when you're in a hurry.
</P>
<H3>Voice input</H3>
<P>
Click the <strong>microphone button</strong> to start dictating. Click again to stop.
The transcript appears in the command field in real-time edit it if needed, then press Enter or click Send.
</P>
<Tip>Voice works best in Chrome / Edge. Make sure microphone permission is granted for this site.</Tip>
<H3>Quick starters</H3>
<P>Below the command bar there are pre-filled example commands. Click one to load it into the input, then customise and send.</P>
<H3>Activity log</H3>
<P>Every AI action is recorded in the activity log (visible below the sheet). It shows what the AI did, which rows were affected, and any messages from the AI.</P>
</>
),
},
{
id: 'brief',
icon: '📁',
title: 'Brief Extraction',
content: (
<>
<H2>Extracting Deliverables from a Brief</H2>
<P>
The AI can read a client brief document and automatically populate a sheet with the deliverables it finds.
Supported formats: <Code>.pdf</Code>, <Code>.pptx</Code>, <Code>.docx</Code>, <Code>.xlsx</Code>, <Code>.ppt</Code>, <Code>.doc</Code>, <Code>.xls</Code>
</P>
<H3>Step-by-step</H3>
<Steps items={[
'Click <strong>Upload Brief</strong> in the sidebar or on the Dashboard.',
'(Optional) Select a <strong>Client</strong> from the dropdown — this helps the AI use the right categories.',
'Drag & drop your file(s) or click to browse. You can upload multiple files at once.',
'The extraction runs in the background. You\'ll see a progress card appear.',
'When complete, click <strong>Review</strong> to inspect the extracted deliverables.',
'On the review page, adjust rows if needed, then click <strong>Import to Sheet</strong> to add them to an existing sheet or create a new one.',
]} />
<Tip>You can upload multiple briefs at once. Each one becomes a separate extraction job.</Tip>
<H3>Review page</H3>
<P>
The review page shows all extracted rows in a table. You can edit any cell before importing.
Use <strong>Append</strong> to add rows to an existing sheet, or <strong>Replace</strong> to overwrite it.
</P>
</>
),
},
{
id: 'export',
icon: '⬇️',
title: 'Export CSV',
content: (
<>
<H2>Exporting to CSV</H2>
<P>
Click <strong>Export CSV</strong> in the sheet header to download the current sheet as a CSV file.
The column names and order depend on the active export template.
</P>
<H3>Export template priority</H3>
<P>The system uses the first template it finds, in this order:</P>
<ol style={{ paddingLeft: 20, margin: '0 0 12px' }}>
<Li><strong style={{ color: 'var(--text-primary)' }}>Client template</strong> set by admin for the selected client</Li>
<Li><strong style={{ color: 'var(--text-primary)' }}>Your personal template</strong> set by you using the button</Li>
<Li><strong style={{ color: 'var(--text-primary)' }}>Global template</strong> set by admin in the Dropdowns admin page</Li>
<Li><strong style={{ color: 'var(--text-primary)' }}>Built-in default</strong> standard AC columns</Li>
</ol>
<H3>Set your personal export template</H3>
<Steps items={[
'Click the <strong>⚙</strong> button next to "Export CSV" in the sheet header.',
'A panel opens. Drag & drop a sample <Code>.csv</Code> file — it only needs the header row.',
'The system detects your column names and suggests which internal field each one maps to.',
'Adjust the mapping if needed using the dropdowns.',
'Click <strong>Save template</strong>. From now on every export will use your column names.',
]} />
<Tip>To reset to the default format, open the panel and click <strong>Remove</strong>.</Tip>
</>
),
},
{
id: 'admin-clients',
icon: '🏢',
title: 'Admin: Clients',
adminOnly: true,
content: (
<>
<H2>Managing Clients</H2>
<P>Go to <strong>Clients</strong> in the sidebar (admin only). Each client can have its own Category/Media hierarchy and CSV export template.</P>
<H3>Create a client</H3>
<Steps items={[
'Type the client name in the <strong>Add Client</strong> field and press Enter or click Create.',
'The client now appears in the list and in the Client dropdown on every sheet.',
]} />
<H3>Upload a custom Category/Media hierarchy</H3>
<P>
By default, sheets use the global dropdown hierarchy. You can override this per client with an <Code>.xlsx</Code> file.
</P>
<Steps items={[
'In the client card, drag & drop an <Code>.xlsx</Code> file onto the dropdown zone (or click to browse).',
'The system reads the file headers and shows you the detected column mapping.',
'<strong>Confirm mapping</strong>: verify which column is "Category name", which is "Status", which is "Media types".',
'A full preview of parsed categories appears. Click <strong>Apply</strong> to save.',
'The client card shows a <strong>Custom hierarchy</strong> badge.',
]} />
<Tip>Expected Excel format: Column A = Category name, Column E = Status (Active/Archived), Column G = Media types (comma-separated). The mapping step lets you correct this if your file differs.</Tip>
<H3>Upload a custom export CSV template</H3>
<Steps items={[
'In the client card, scroll to the <strong>Export CSV template</strong> section.',
'Drop a sample <Code>.csv</Code> file. Only the header row matters.',
'Map each of your column names to the internal field it should contain.',
'Click <strong>Save template</strong>.',
]} />
<H3>Reset to global</H3>
<P>Click <strong>Reset to global</strong> on a client card to remove the custom hierarchy and revert to the global dropdown list.</P>
<H3>Delete a client</H3>
<P>Click <strong>Delete</strong> on a client card. This also removes their custom dropdown and export template files.</P>
</>
),
},
{
id: 'admin-dropdowns',
icon: '🗂️',
title: 'Admin: Dropdowns',
adminOnly: true,
content: (
<>
<H2>Global Dropdown Data</H2>
<P>
Go to <strong>Dropdowns</strong> in the sidebar. This page manages the <em>global</em> Category/Media hierarchy
used by sheets that have no client assigned (or whose client has no custom file).
</P>
<H3>Upload a new hierarchy</H3>
<Steps items={[
'Drag & drop an <Code>.xlsx</Code> file onto the upload zone.',
'Confirm the detected column mapping (Category name / Status / Media types).',
'Review the full list of parsed categories.',
'Click <strong>Apply Changes</strong> to replace the global data.',
]} />
<P>The current global data is always shown at the bottom of the page.</P>
<Tip>Uploading a new file replaces the entire global list. Clients with custom hierarchies are not affected.</Tip>
</>
),
},
{
id: 'admin-users',
icon: '👥',
title: 'Admin: Users',
adminOnly: true,
content: (
<>
<H2>User Management</H2>
<P>Go to <strong>Users</strong> in the sidebar. Every user who has signed in appears here.</P>
<H3>Roles</H3>
<ul style={{ paddingLeft: 20, margin: '0 0 12px' }}>
<Li><strong style={{ color: 'var(--text-primary)' }}>user</strong> can create and manage their own sheets, upload briefs, export CSV.</Li>
<Li><strong style={{ color: 'var(--text-primary)' }}>admin</strong> everything above, plus access to the admin panel (Users, Clients, Dropdowns).</Li>
</ul>
<H3>Change a user's role</H3>
<P>Click the role badge next to a user and select <strong>admin</strong> or <strong>user</strong> from the dropdown.</P>
<H3>Deactivate a user</H3>
<P>Toggle the <strong>Active</strong> switch off to prevent a user from logging in. Their data is preserved.</P>
<H3>Admin bootstrap</H3>
<P>
The emails <Code>daveporter@oliver.agency</Code> and <Code>vadymsamoilenko@oliver.agency</Code> automatically
receive admin role on first login and cannot be downgraded through the UI.
</P>
</>
),
},
{
id: 'login',
icon: '🔐',
title: 'Login & Access',
content: (
<>
<H2>Signing In</H2>
<H3>Standard login (Microsoft SSO)</H3>
<P>Click <strong>Sign in with Microsoft</strong> on the login page. You'll be redirected to Microsoft for authentication. Use your Oliver Agency account.</P>
<H3>Emergency access</H3>
<P>
If Microsoft SSO / 2FA is unavailable, use the emergency bypass:
</P>
<Steps items={[
'On the login page, click the small <strong>Emergency access</strong> link at the bottom.',
'Enter the emergency token (ask an admin).',
'Click <strong>Sign in</strong>.',
]} />
<P>The emergency token is configured in the server <Code>.env</Code> file as <Code>EMERGENCY_TOKEN=</Code>. If not set, emergency access is disabled.</P>
<H3>Signing out</H3>
<P>There is no explicit sign-out button closing the browser tab ends the session. The Microsoft SSO token expires and you'll be asked to sign in again on your next visit.</P>
</>
),
},
]
// ── Page ───────────────────────────────────────────────────────────────────────
export default function HelpPage() {
const navigate = useNavigate()
const user = useAuthStore(s => s.user)
const isAdmin = user?.role === 'admin'
const [activeId, setActiveId] = useState('overview')
const visible = SECTIONS.filter(s => !s.adminOnly || isAdmin)
const active = visible.find(s => s.id === activeId) ?? visible[0]
return (
<div className="flex h-full min-h-0" style={{ maxWidth: 1100, margin: '0 auto', width: '100%' }}>
{/* Left nav */}
<div
className="flex-shrink-0 py-2 pr-4"
style={{ width: 200, borderRight: '1px solid var(--border)', overflowY: 'auto' }}
>
<div className="text-xs font-bold uppercase tracking-widest mb-4 px-2" style={{ color: 'var(--text-muted)' }}>
User Guide
</div>
<nav className="space-y-0.5">
{visible.map(section => (
<button
key={section.id}
onClick={() => setActiveId(section.id)}
className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-left transition-colors"
style={{
background: activeId === section.id ? 'var(--accent-dim)' : 'transparent',
color: activeId === section.id ? 'var(--accent)' : 'var(--text-secondary)',
border: 'none',
cursor: 'pointer',
}}
>
<span style={{ fontSize: 14 }}>{section.icon}</span>
<span className="text-xs font-medium">{section.title}</span>
{section.adminOnly && (
<span className="ml-auto text-xs px-1 rounded" style={{ background: 'rgba(255,196,7,0.15)', color: 'var(--accent)', fontSize: 9 }}>
Admin
</span>
)}
</button>
))}
</nav>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto px-8 py-6">
<div style={{ maxWidth: 680 }}>
{active.content}
</div>
</div>
</div>
)
}