feat(onboarding): welcome dashboard with first-steps cards
Some checks failed
Some checks failed
Дефолтная страница после логина (/) — OnboardingPage по образу
МойСклад «Первые шаги». Старый DashboardPage с KPI и графиком
переехал на /dashboard, в меню «Главная» теперь два пункта:
«Главная» (онбординг) и «Аналитика» (KPI/графики).
useOnboardingProgress() — хук, считает 4 шага:
- orgConfigured: country + defaultCurrency установлены
- hasEmployees: > 1 сотрудник (помимо админа)
- hasProducts: > 0 товаров
- hasSupplies: > 0 приёмок
OnboardingPage:
- Прогресс-бар «N из 4 шагов» с процентом
- 4 карточки задач: Настройки → Сотрудники → Каталог/Импорт → Приёмка
- Каждая показывает иконку (CheckCircle2 если done) + бэйдж
категории + заголовок + описание + CTA-кнопка с ArrowRight,
меняющая текст и ссылку в зависимости от done.
- Когда все 4 шага сделаны — плашка «🎉 Готово!» + переход на
/dashboard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
033f20e215
commit
8fb55993a1
|
|
@ -2,6 +2,7 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
|||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { LoginPage } from '@/pages/LoginPage'
|
||||
import { DashboardPage } from '@/pages/DashboardPage'
|
||||
import { OnboardingPage } from '@/pages/OnboardingPage'
|
||||
import { CountriesPage } from '@/pages/CountriesPage'
|
||||
import { UnitsOfMeasurePage } from '@/pages/UnitsOfMeasurePage'
|
||||
import { PriceTypesPage } from '@/pages/PriceTypesPage'
|
||||
|
|
@ -41,7 +42,8 @@ export default function App() {
|
|||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route element={<ProtectedRoute />}>
|
||||
<Route element={<AppLayout />}>
|
||||
<Route path="/" element={<DashboardPage />} />
|
||||
<Route path="/" element={<OnboardingPage />} />
|
||||
<Route path="/dashboard" element={<DashboardPage />} />
|
||||
<Route path="/catalog/products" element={<ProductsPage />} />
|
||||
<Route path="/catalog/products/new" element={<ProductEditPage />} />
|
||||
<Route path="/catalog/products/:id" element={<ProductEditPage />} />
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@ function buildNav(): NavSection[] {
|
|||
]
|
||||
return [
|
||||
{ group: 'Главное', items: [
|
||||
{ to: '/', icon: LayoutDashboard, label: 'Dashboard', end: true },
|
||||
{ to: '/', icon: LayoutDashboard, label: 'Главная', end: true },
|
||||
{ to: '/dashboard', icon: LayoutDashboard, label: 'Аналитика' },
|
||||
]},
|
||||
{ group: 'Каталог', items: catalog },
|
||||
{ group: 'Контрагенты', items: [
|
||||
|
|
|
|||
44
src/food-market.web/src/lib/useOnboardingProgress.ts
Normal file
44
src/food-market.web/src/lib/useOnboardingProgress.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { useQuery } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api'
|
||||
import type { PagedResult } from '@/lib/types'
|
||||
import { useOrgSettings } from '@/lib/useOrgSettings'
|
||||
|
||||
interface EmployeeRow { id: string; userId: string | null }
|
||||
|
||||
/** Прогресс по «первым шагам» новой организации: настройки + сотрудники +
|
||||
* товары + первая приёмка. Используется в OnboardingPage. */
|
||||
export function useOnboardingProgress() {
|
||||
const org = useOrgSettings()
|
||||
const employees = useQuery({
|
||||
queryKey: ['onboarding:employees-count'],
|
||||
queryFn: async () => (await api.get<PagedResult<EmployeeRow>>('/api/organization/employees?pageSize=1')).data.total,
|
||||
staleTime: 60_000,
|
||||
})
|
||||
const products = useQuery({
|
||||
queryKey: ['onboarding:products-count'],
|
||||
queryFn: async () => (await api.get<PagedResult<unknown>>('/api/catalog/products?pageSize=1')).data.total,
|
||||
staleTime: 60_000,
|
||||
})
|
||||
const supplies = useQuery({
|
||||
queryKey: ['onboarding:supplies-count'],
|
||||
queryFn: async () => (await api.get<PagedResult<unknown>>('/api/purchases/supplies?pageSize=1')).data.total,
|
||||
staleTime: 60_000,
|
||||
})
|
||||
|
||||
const orgConfigured = !!org.data?.defaultCurrencyId && !!org.data?.countryCode
|
||||
const hasEmployees = (employees.data ?? 0) > 1 // больше одного admin'а
|
||||
const hasProducts = (products.data ?? 0) > 0
|
||||
const hasSupplies = (supplies.data ?? 0) > 0
|
||||
|
||||
const steps = { orgConfigured, hasEmployees, hasProducts, hasSupplies }
|
||||
const done = Object.values(steps).filter(Boolean).length
|
||||
const total = Object.keys(steps).length
|
||||
return {
|
||||
isLoading: org.isLoading || employees.isLoading || products.isLoading || supplies.isLoading,
|
||||
...steps,
|
||||
doneCount: done,
|
||||
totalCount: total,
|
||||
overall: total === 0 ? 0 : Math.round((done / total) * 100),
|
||||
allDone: done === total,
|
||||
}
|
||||
}
|
||||
143
src/food-market.web/src/pages/OnboardingPage.tsx
Normal file
143
src/food-market.web/src/pages/OnboardingPage.tsx
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import { Link } from 'react-router-dom'
|
||||
import { Settings, UserCog, Package, ShoppingCart, ArrowRight, CheckCircle2, BarChart3 } from 'lucide-react'
|
||||
import { PageHeader } from '@/components/PageHeader'
|
||||
import { useOnboardingProgress } from '@/lib/useOnboardingProgress'
|
||||
|
||||
interface StepCard {
|
||||
done: boolean
|
||||
badge: string
|
||||
title: string
|
||||
description: string
|
||||
ctaLabel: string
|
||||
ctaTo: string
|
||||
ctaDisabled?: boolean
|
||||
icon: React.ComponentType<{ className?: string }>
|
||||
}
|
||||
|
||||
export function OnboardingPage() {
|
||||
const p = useOnboardingProgress()
|
||||
|
||||
const cards: StepCard[] = [
|
||||
{
|
||||
done: p.orgConfigured,
|
||||
badge: 'Запуск магазина',
|
||||
title: 'Основные настройки бизнеса',
|
||||
description: 'Укажите название, страну и валюту. От этого зависят валюта цен и ставка НДС в карточках товаров.',
|
||||
ctaLabel: p.orgConfigured ? 'Изменить настройки' : 'Перейти к настройкам',
|
||||
ctaTo: '/settings/organization',
|
||||
icon: Settings,
|
||||
},
|
||||
{
|
||||
done: p.hasEmployees,
|
||||
badge: 'Команда магазина',
|
||||
title: 'Добавьте кассира, кладовщика и других сотрудников',
|
||||
description: 'Каждому сотруднику — своя роль с набором прав. Кассирам можно привязать конкретные кассы.',
|
||||
ctaLabel: p.hasEmployees ? 'Управление сотрудниками' : 'Добавить сотрудников',
|
||||
ctaTo: '/settings/employees',
|
||||
icon: UserCog,
|
||||
},
|
||||
{
|
||||
done: p.hasProducts,
|
||||
badge: 'Каталог',
|
||||
title: 'Заведите товары вручную или импортируйте из МойСклад',
|
||||
description: 'Товары — основа всего. Импорт подтянет наименования, штрихкоды, цены и остатки одним нажатием.',
|
||||
ctaLabel: p.hasProducts ? 'Перейти в каталог' : 'Добавить товары',
|
||||
ctaTo: p.hasProducts ? '/catalog/products' : '/admin/import/moysklad',
|
||||
icon: Package,
|
||||
},
|
||||
{
|
||||
done: p.hasSupplies,
|
||||
badge: 'Закупки',
|
||||
title: 'Создайте первую приёмку товара',
|
||||
description: 'Проведённая приёмка кладёт товар на склад и обновляет себестоимость. Сканируйте штрихкоды или ищите вручную.',
|
||||
ctaLabel: p.hasSupplies ? 'К приёмкам' : 'Создать приёмку',
|
||||
ctaTo: p.hasSupplies ? '/purchases/supplies' : '/purchases/supplies/new',
|
||||
icon: ShoppingCart,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto">
|
||||
<div className="max-w-5xl mx-auto p-4 sm:p-6 space-y-5 sm:space-y-6">
|
||||
<PageHeader
|
||||
title="Первые шаги в Food Market"
|
||||
description="Складской учёт, закупки и продажи в одной системе. Пройдите 4 шага — и можно работать."
|
||||
/>
|
||||
|
||||
{/* Прогресс-плашка */}
|
||||
<section className="rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 p-5">
|
||||
<div className="flex items-center justify-between gap-3 mb-3">
|
||||
<div>
|
||||
<div className="text-sm text-slate-500">Готовность системы</div>
|
||||
<div className="text-xl font-semibold">{p.doneCount} из {p.totalCount} шагов</div>
|
||||
</div>
|
||||
{p.allDone ? (
|
||||
<Link
|
||||
to="/dashboard"
|
||||
className="inline-flex items-center gap-2 px-3 py-2 rounded-md bg-[var(--color-brand)] text-white text-sm font-medium hover:bg-[var(--color-brand-hover)]"
|
||||
>
|
||||
<BarChart3 className="w-4 h-4" /> Открыть дашборд
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-2xl font-bold text-[var(--color-brand)]">{p.overall}%</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="h-2 rounded-full bg-slate-100 dark:bg-slate-800 overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-[var(--color-brand)] transition-all"
|
||||
style={{ width: `${p.overall}%` }}
|
||||
/>
|
||||
</div>
|
||||
{p.allDone && (
|
||||
<div className="mt-3 text-sm text-emerald-600 inline-flex items-center gap-1.5">
|
||||
<CheckCircle2 className="w-4 h-4" /> 🎉 Готово! Система настроена и готова к работе.
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Карточки шагов */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-5">
|
||||
{cards.map((c) => (
|
||||
<article
|
||||
key={c.title}
|
||||
className="rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 p-5 flex flex-col gap-3"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`w-11 h-11 rounded-lg flex items-center justify-center flex-shrink-0
|
||||
${c.done ? 'bg-[var(--color-brand-light)] text-[var(--color-brand)]' : 'bg-slate-100 dark:bg-slate-800 text-slate-500'}`}>
|
||||
{c.done ? <CheckCircle2 className="w-6 h-6" /> : <c.icon className="w-6 h-6" />}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs uppercase tracking-wide text-slate-500">{c.badge}</div>
|
||||
<h3 className="text-base font-semibold mt-0.5">{c.title}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400 flex-1">{c.description}</p>
|
||||
{c.ctaDisabled ? (
|
||||
<button disabled className="inline-flex items-center gap-1.5 self-start px-3 py-1.5 rounded-md border border-slate-200 dark:border-slate-700 text-sm text-slate-400 cursor-not-allowed">
|
||||
{c.ctaLabel}
|
||||
</button>
|
||||
) : (
|
||||
<Link
|
||||
to={c.ctaTo}
|
||||
className="inline-flex items-center gap-1.5 self-start px-3 py-1.5 rounded-md border border-slate-200 dark:border-slate-700 text-sm font-medium hover:bg-slate-50 dark:hover:bg-slate-800"
|
||||
>
|
||||
{c.ctaLabel} <ArrowRight className="w-3.5 h-3.5" />
|
||||
</Link>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Поддержка */}
|
||||
<section className="rounded-xl border border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-800/30 p-5">
|
||||
<h3 className="text-sm font-semibold mb-1">Помощь и поддержка</h3>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Возникли вопросы по настройке или работе системы? Напишите администратору
|
||||
или загляните в документацию проекта на Forgejo.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in a new issue