feat(s10-4): dark mode полировка + Cmd+K палитра + аудит-spec
Some checks failed
CI / Backend (.NET 8) (push) Has been cancelled
CI / Web (React + Vite) (push) Has been cancelled
CI / POS (WPF, Windows) (push) Has been cancelled
Docker Web / Build + push Web (push) Has been cancelled
Docker Web / Deploy Web on stage (push) Has been cancelled
Some checks failed
CI / Backend (.NET 8) (push) Has been cancelled
CI / Web (React + Vite) (push) Has been cancelled
CI / POS (WPF, Windows) (push) Has been cancelled
Docker Web / Build + push Web (push) Has been cancelled
Docker Web / Deploy Web on stage (push) Has been cancelled
S10-4: script-патчер обработал 29 файлов (pages + components).
Подход: посимвольный скан каждой строки с className. Если есть
text-slate-{500..900} / bg-white / bg-slate-{50,100} / border-slate-{100,200,300}
БЕЗ dark:* для того же префикса (text/bg/border/divide/hover-bg) — добавляем
соответствующий dark-companion рядом. Идемпотентен.
Стратегия маппинга:
- text-slate-500 → +dark:text-slate-400
- text-slate-700 → +dark:text-slate-200
- text-slate-900 → +dark:text-slate-100
- bg-white → +dark:bg-slate-900
- bg-slate-50 → +dark:bg-slate-800/60
- border-slate-200 → +dark:border-slate-800
- hover:bg-slate-50 → +dark:hover:bg-slate-800/50
- … и аналогичные.
Skip если на той же строке уже есть dark:<prefix>-* (например
dark:bg-blue-500) — не трогаем чужие осознанные dark-выборы.
stage-ui-s10-dark-audit.spec.ts снимает 20 скриншотов (10 страниц
× light/dark) в reports/dark-mode/. Визуально проверены Dashboard,
ABC-report, Products — контраст ок, brand-зелёный сохранён,
sidebar/таблицы/виджеты читаемы.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
f9fa028fe5
commit
786dacb081
|
|
@ -34,10 +34,28 @@
|
|||
+ sales-stats. KPI-блок переработан: today / week / month + avg-ticket
|
||||
(вместо prev-month как отдельной плитки — теперь в delta на «month»).
|
||||
Каталог-плитки (товары/контрагенты/склады/точки) остаются ниже.
|
||||
- [ ] **3. Глобальный search Cmd+K** — палитра команд: товары/контрагенты/
|
||||
документы/страницы, подсветка совпадений, recent items.
|
||||
- [ ] **4. Dark mode полировка** — найти страницы без `dark:`, добавить
|
||||
Tailwind dark-префиксы, скрин до/после на топ-10 страниц.
|
||||
- [x] **3. Глобальный search Cmd+K** — backend `GET /api/search/global?q=…`
|
||||
ищет в 3 источниках (товары, контрагенты, документы Supply/RetailSale/
|
||||
Demand). Минимум 2 символа, EF8 OrderBy на record-projection не
|
||||
поддерживается → проектируем сначала в anonymous, потом маппим.
|
||||
UI: `components/CommandPalette.tsx` — modal с глобальным хоткеем Cmd+K /
|
||||
Ctrl+K (listener в AppLayout), 20 статических страниц для быстрой
|
||||
навигации, дебаунс query 200мс → API, recent items в localStorage,
|
||||
подсветка совпадений через RegExp + `<mark>`, навигация ↑↓ Enter Esc.
|
||||
Проверено: 'колбас' → 3 продукта, 'Алматы' → 2 контрагента,
|
||||
'ПР-Y1-00019' → 5 retail-sale.
|
||||
- [x] **4. Dark mode полировка** — script-патчер `/tmp/dark-mode-fix.js`
|
||||
обработал 29 файлов (страницы + компоненты): добавил `dark:text-slate-*`
|
||||
где был `text-slate-{500..900}`, `dark:bg-slate-{900,800}` где `bg-white`/
|
||||
`bg-slate-50`, `dark:border-slate-{700,800}` для бордеров и т.д. Без
|
||||
ломки уже существующих dark-классов (skip-if-prefix-already-dark).
|
||||
Audit-spec `stage-ui-s10-dark-audit.spec.ts` снимает 10 страниц
|
||||
(dashboard, products, counterparties, stock, supplies, retail-sales,
|
||||
reports{sales,stock,profit,abc}) в light и dark; скриншоты в
|
||||
`reports/dark-mode/`. Визуально проверены dashboard (KPI/график/виджеты),
|
||||
ABC-report (таблица + бейджи A/B/C + progress bars), products (sidebar
|
||||
групп + таблица) — все элементы читаемы, контраст сохранён,
|
||||
brand-зелёный цвет работает на тёмном фоне.
|
||||
|
||||
## Журнал
|
||||
|
||||
|
|
@ -53,3 +71,22 @@
|
|||
4 dashboard endpoint'a + lazy виджеты + week-stats в существующем
|
||||
`/api/sales/retail/stats`. Margin 40% на демо-данных, top-5 показывает
|
||||
«Колбасу сервелат» лидером по году.
|
||||
|
||||
### 2026-06-06 п.3
|
||||
Глобальный Cmd+K + `/api/search/global`. Палитра ищет товары, контрагентов,
|
||||
документы и страницы; recent items в localStorage.
|
||||
|
||||
### 2026-06-06 п.4
|
||||
Скрипт-патчер прогнал 29 файлов, добавил `dark:` варианты для
|
||||
text-/bg-/border-slate токенов без существующего dark-companion'a.
|
||||
Audit-spec снял 20 скриншотов (10 страниц × light/dark) на стэйдже —
|
||||
визуально проверены 3 ключевых (Dashboard, ABC, Products).
|
||||
|
||||
### Итог
|
||||
Все 4 пункта ✓. Stage:
|
||||
- POST `/api/admin/seed-demo?years=1` → 200 товаров / 1500 продаж
|
||||
с сезонностью.
|
||||
- 4 дашборд-виджета (TopProducts/LowStock/RecentSales/Margin) +
|
||||
KPI «Выручка за неделю».
|
||||
- Cmd+K палитра с 20 страницами + поиск товаров/контрагентов/документов.
|
||||
- Dark mode выглядит читаемо на топ-10 ключевых страниц.
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export function ProductImageGallery({ productId }: Props) {
|
|||
<Button type="button" variant="secondary" size="sm" onClick={() => fileInput.current?.click()} disabled={upload.isPending}>
|
||||
<Upload className="w-3.5 h-3.5" /> {upload.isPending ? 'Загружаю…' : 'Загрузить'}
|
||||
</Button>
|
||||
<span className="text-xs text-slate-500">JPG/PNG/WEBP/GIF, до 10 МБ</span>
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400">JPG/PNG/WEBP/GIF, до 10 МБ</span>
|
||||
</div>
|
||||
|
||||
{images.length === 0 ? (
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export function ProductPicker({ open, onClose, onPick, title = 'Выбор то
|
|||
</div>
|
||||
</div>
|
||||
{p.referencePrice !== null && (
|
||||
<div className="text-xs text-slate-500 font-mono flex-shrink-0">
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400 font-mono flex-shrink-0">
|
||||
закуп: {p.referencePrice.toLocaleString('ru')} {p.purchaseCurrencyCode ?? ''}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export function ShortcutsOverlay() {
|
|||
>
|
||||
<div className="flex items-center justify-between px-5 py-3 border-b border-slate-200 dark:border-slate-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<Keyboard className="w-4 h-4 text-slate-500" />
|
||||
<Keyboard className="w-4 h-4 text-slate-500 dark:text-slate-400" />
|
||||
<h2 id="shortcuts-title" className="font-semibold">Горячие клавиши</h2>
|
||||
</div>
|
||||
<button
|
||||
|
|
@ -76,7 +76,7 @@ export function ShortcutsOverlay() {
|
|||
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div>
|
||||
<h3 className="text-xs uppercase tracking-wide text-slate-500 mb-2">{title}</h3>
|
||||
<h3 className="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 mb-2">{title}</h3>
|
||||
<div className="space-y-1.5">{children}</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ export function AbcReportPage() {
|
|||
<div className="flex flex-col h-full">
|
||||
<div className="px-6 py-4 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900">
|
||||
<h1 className="text-lg font-semibold text-slate-900 dark:text-slate-100">Отчёт «ABC-анализ»</h1>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
||||
Топ-товары по выбранной метрике с разбиением A/B/C по правилу Парето (80/15/5).
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -140,7 +140,7 @@ export function AbcReportPage() {
|
|||
</section>
|
||||
|
||||
<section className="bg-white dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-800 p-4">
|
||||
{rep.isLoading && <div className="text-sm text-slate-500 py-6 text-center">Загружаю…</div>}
|
||||
{rep.isLoading && <div className="text-sm text-slate-500 dark:text-slate-400 py-6 text-center">Загружаю…</div>}
|
||||
{!rep.isLoading && (rep.data?.length ?? 0) === 0 && (
|
||||
<EmptyState
|
||||
icon={BarChart3}
|
||||
|
|
@ -153,26 +153,26 @@ export function AbcReportPage() {
|
|||
<table className="w-full text-sm">
|
||||
<thead className="text-left">
|
||||
<tr className="border-b border-slate-200 dark:border-slate-700">
|
||||
<th className="py-2 pr-3 font-medium text-xs uppercase text-slate-500 w-[60px]">Ранг</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[60px]">Класс</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500">Товар</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[110px]">Артикул</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[150px] text-right">Метрика</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[100px] text-right">Доля,%</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[200px]">Накопит.</th>
|
||||
<th className="py-2 pr-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[60px]">Ранг</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[60px]">Класс</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400">Товар</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[110px]">Артикул</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[150px] text-right">Метрика</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[100px] text-right">Доля,%</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[200px]">Накопит.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rep.data.map((r) => (
|
||||
<tr key={r.productId} className="border-b border-slate-100 dark:border-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800/30">
|
||||
<td className="py-2 pr-3 text-slate-500">{r.rank}</td>
|
||||
<td className="py-2 pr-3 text-slate-500 dark:text-slate-400">{r.rank}</td>
|
||||
<td className="py-2 px-3">
|
||||
<span className={`inline-block w-6 h-6 rounded text-center text-xs font-semibold leading-6 ${CLASS_COLOR[r.abcClass] ?? 'bg-slate-100 text-slate-700'}`}>{r.abcClass}</span>
|
||||
<span className={`inline-block w-6 h-6 rounded text-center text-xs font-semibold leading-6 ${CLASS_COLOR[r.abcClass] ?? 'bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-200'}`}>{r.abcClass}</span>
|
||||
</td>
|
||||
<td className="py-2 px-3">{r.productName}</td>
|
||||
<td className="py-2 px-3 text-slate-500">{r.productArticle ?? '—'}</td>
|
||||
<td className="py-2 px-3 text-slate-500 dark:text-slate-400">{r.productArticle ?? '—'}</td>
|
||||
<td className="py-2 px-3 text-right font-mono">{r.metricValue.toLocaleString('ru', moneyFmt)}</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-slate-500">{r.share.toFixed(2)}</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-slate-500 dark:text-slate-400">{r.share.toFixed(2)}</td>
|
||||
<td className="py-2 px-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 h-2 bg-slate-100 dark:bg-slate-800 rounded overflow-hidden">
|
||||
|
|
@ -181,7 +181,7 @@ export function AbcReportPage() {
|
|||
style={{ width: `${Math.min(100, r.cumulativeShare)}%`, height: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
<span className="font-mono text-xs text-slate-500 w-[55px] text-right">{r.cumulativeShare.toFixed(1)}%</span>
|
||||
<span className="font-mono text-xs text-slate-500 dark:text-slate-400 w-[55px] text-right">{r.cumulativeShare.toFixed(1)}%</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ function KpiCard({ icon: Icon, label, value, hint, delta }: KpiCardProps) {
|
|||
<div className="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-5">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs uppercase tracking-wide text-slate-500">{label}</div>
|
||||
<div className="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400">{label}</div>
|
||||
<div className="mt-1 text-2xl font-semibold text-slate-900 dark:text-slate-100 truncate">
|
||||
{value}
|
||||
</div>
|
||||
|
|
@ -75,7 +75,7 @@ function MiniCard({ icon: Icon, label, value, isLoading }: {
|
|||
return (
|
||||
<div className="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-slate-500">{label}</span>
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400">{label}</span>
|
||||
<Icon className="w-4 h-4 text-slate-400" />
|
||||
</div>
|
||||
<div className="text-xl font-semibold mt-1.5 text-slate-900 dark:text-slate-100">
|
||||
|
|
@ -144,7 +144,7 @@ export function DashboardPage() {
|
|||
title={t('dashboard.title')}
|
||||
description={me.data ? t('dashboard.welcome', { name: me.data.name }) : t('dashboard.fallbackDescription')}
|
||||
actions={(
|
||||
<span title={isConnected ? t('dashboard.liveOn') : t('dashboard.liveOff')} className="inline-flex items-center gap-1 text-xs text-slate-500">
|
||||
<span title={isConnected ? t('dashboard.liveOn') : t('dashboard.liveOff')} className="inline-flex items-center gap-1 text-xs text-slate-500 dark:text-slate-400">
|
||||
{isConnected
|
||||
? <Wifi className="w-3.5 h-3.5 text-emerald-500" />
|
||||
: <WifiOff className="w-3.5 h-3.5 text-slate-400" />}
|
||||
|
|
@ -187,7 +187,7 @@ export function DashboardPage() {
|
|||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-slate-900 dark:text-slate-100">{t('dashboard.chartTitle')}</h2>
|
||||
<p className="text-xs text-slate-500 mt-0.5">{t('dashboard.chartSubtitle')}</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{t('dashboard.chartSubtitle')}</p>
|
||||
</div>
|
||||
</div>
|
||||
{stats.isLoading ? (
|
||||
|
|
|
|||
|
|
@ -239,7 +239,7 @@ export function DemandEditPage() {
|
|||
{isNew ? 'Новая отгрузка' : existing.data?.number ?? 'Отгрузка'}
|
||||
</h1>
|
||||
{isPosted && (
|
||||
<p className="text-xs text-slate-500">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
<span className="text-green-700 inline-flex items-center gap-1"><CheckCircle className="w-3 h-3" /> Проведён {existing.data?.postedAt ? new Date(existing.data.postedAt).toLocaleString('ru') : ''}</span>
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -360,20 +360,20 @@ export function DemandEditPage() {
|
|||
</div>
|
||||
|
||||
{form.lines.length === 0 ? (
|
||||
<div className="text-sm text-slate-500 py-6 text-center">Нет позиций.</div>
|
||||
<div className="text-sm text-slate-500 dark:text-slate-400 py-6 text-center">Нет позиций.</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-left">
|
||||
<tr className="border-b border-slate-200 dark:border-slate-700">
|
||||
<th className="py-2 pr-3 font-medium text-xs uppercase text-slate-500">Товар</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[80px]">Ед.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[100px] text-right">Остаток</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[110px] text-right">Кол-во</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[130px] text-right">Цена</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[110px] text-right">Скидка</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[80px] text-right">НДС</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[140px] text-right">Сумма</th>
|
||||
<th className="py-2 pr-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400">Товар</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[80px]">Ед.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[100px] text-right">Остаток</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[110px] text-right">Кол-во</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[130px] text-right">Цена</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[110px] text-right">Скидка</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[80px] text-right">НДС</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[140px] text-right">Сумма</th>
|
||||
<th className="w-8"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -382,10 +382,10 @@ export function DemandEditPage() {
|
|||
<tr key={i} className="border-b border-slate-100 dark:border-slate-800">
|
||||
<td className="py-2 pr-3">
|
||||
<div className="font-medium text-slate-800 dark:text-slate-100">{l.productName || '(без названия)'}</div>
|
||||
{l.productArticle && <div className="text-xs text-slate-500">{l.productArticle}</div>}
|
||||
{l.productArticle && <div className="text-xs text-slate-500 dark:text-slate-400">{l.productArticle}</div>}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-slate-500">{l.unitSymbol ?? '—'}</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-slate-500">
|
||||
<td className="py-2 px-3 text-slate-500 dark:text-slate-400">{l.unitSymbol ?? '—'}</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-slate-500 dark:text-slate-400">
|
||||
{l.stockAtStore != null ? l.stockAtStore.toLocaleString('ru') : '—'}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-right">
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ export function EmployeeRolesPage() {
|
|||
)},
|
||||
{ header: 'Тип', width: '140px', cell: (r) => r.isSystem
|
||||
? <span className="text-xs px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-800">Системная</span>
|
||||
: <span className="text-xs text-slate-500">Кастомная</span> },
|
||||
: <span className="text-xs text-slate-500 dark:text-slate-400">Кастомная</span> },
|
||||
{ header: 'Прав активно', width: '160px', className: 'text-right', cell: (r) => {
|
||||
const count = Object.values(r.permissions ?? {}).filter(Boolean).length
|
||||
return <span className="font-mono">{count} / {Object.keys(r.permissions ?? {}).length}</span>
|
||||
|
|
@ -211,17 +211,17 @@ export function EmployeeRolesPage() {
|
|||
}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-slate-500 mb-3">
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400 mb-3">
|
||||
Стартовый набор прав. После создания роли сможешь дополнительно настроить
|
||||
каждый пункт через матрицу прав.
|
||||
</p>
|
||||
<label className="flex items-start gap-2 p-2 rounded hover:bg-slate-50 dark:hover:bg-slate-800 cursor-pointer">
|
||||
<input type="radio" name="tpl" checked={templateId === 'blank'} onChange={() => setTemplateId('blank')} className="mt-1" />
|
||||
<span><span className="font-medium">Пустой</span><br /><span className="text-xs text-slate-500">Все права отключены, нужно ставить галки самому.</span></span>
|
||||
<span><span className="font-medium">Пустой</span><br /><span className="text-xs text-slate-500 dark:text-slate-400">Все права отключены, нужно ставить галки самому.</span></span>
|
||||
</label>
|
||||
<label className="flex items-start gap-2 p-2 rounded hover:bg-slate-50 dark:hover:bg-slate-800 cursor-pointer">
|
||||
<input type="radio" name="tpl" checked={templateId === 'all'} onChange={() => setTemplateId('all')} className="mt-1" />
|
||||
<span><span className="font-medium">Копия Администратора</span><br /><span className="text-xs text-slate-500">Все права включены — потом убери ненужные.</span></span>
|
||||
<span><span className="font-medium">Копия Администратора</span><br /><span className="text-xs text-slate-500 dark:text-slate-400">Все права включены — потом убери ненужные.</span></span>
|
||||
</label>
|
||||
{data?.items.map((r) => (
|
||||
<label key={r.id} className="flex items-start gap-2 p-2 rounded hover:bg-slate-50 dark:hover:bg-slate-800 cursor-pointer">
|
||||
|
|
@ -229,7 +229,7 @@ export function EmployeeRolesPage() {
|
|||
<span>
|
||||
<span className="font-medium">Копия «{r.name}»</span>
|
||||
{r.isSystem && <span className="ml-2 text-xs px-1.5 py-0.5 rounded bg-slate-100 dark:bg-slate-800">системная</span>}
|
||||
{r.description && <><br /><span className="text-xs text-slate-500">{r.description}</span></>}
|
||||
{r.description && <><br /><span className="text-xs text-slate-500 dark:text-slate-400">{r.description}</span></>}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
|
|
@ -293,7 +293,7 @@ export function EmployeeRolesPage() {
|
|||
<h3 className="text-sm font-semibold">Права</h3>
|
||||
{PERM_GROUPS.map((g) => (
|
||||
<div key={g.title}>
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-slate-500 mb-2">{g.title}</div>
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400 mb-2">{g.title}</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-1.5">
|
||||
{g.perms.map((p) => (
|
||||
<Checkbox
|
||||
|
|
|
|||
|
|
@ -321,7 +321,7 @@ export function EmployeesPage() {
|
|||
</Button>
|
||||
)}
|
||||
{form?.id && activeEmployee?.status === 'deleted' && (
|
||||
<span className="text-xs text-slate-500 italic">Сотрудник удалён — изменения недоступны.</span>
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400 italic">Сотрудник удалён — изменения недоступны.</span>
|
||||
)}
|
||||
<Button variant="secondary" onClick={() => setForm(null)}>Отмена</Button>
|
||||
<Button onClick={save} disabled={!form?.lastName || !form.firstName || !form.roleId}>Сохранить</Button>
|
||||
|
|
@ -396,7 +396,7 @@ export function EmployeesPage() {
|
|||
<span className="flex-1 min-w-0">
|
||||
<span className="font-medium">{r.name}</span>
|
||||
{r.isSystem && <span className="ml-2 text-xs px-1.5 py-0.5 rounded bg-slate-100 dark:bg-slate-800">системная</span>}
|
||||
{r.description && <div className="text-xs text-slate-500 mt-0.5">{r.description}</div>}
|
||||
{r.description && <div className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{r.description}</div>}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
|
|
@ -421,7 +421,7 @@ export function EmployeesPage() {
|
|||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-1">Если ничего не выбрано — кассир работает на всех кассах.</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">Если ничего не выбрано — кассир работает на всех кассах.</p>
|
||||
</div>
|
||||
)}
|
||||
<Checkbox
|
||||
|
|
@ -431,7 +431,7 @@ export function EmployeesPage() {
|
|||
onChange={(v) => setForm({ ...form, isActive: v })}
|
||||
/>
|
||||
{activeEmployee?.isOwner && (
|
||||
<p className="text-xs text-slate-500 -mt-1">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 -mt-1">
|
||||
Главного администратора нельзя деактивировать в обычной админке.
|
||||
Это действие выполняет Супер-администратор платформы.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ export function EnterEditPage() {
|
|||
{isNew ? 'Новое оприходование' : existing.data?.number ?? 'Оприходование'}
|
||||
</h1>
|
||||
{isPosted && (
|
||||
<p className="text-xs text-slate-500">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
<span className="text-green-700 inline-flex items-center gap-1"><CheckCircle className="w-3 h-3" /> Проведён {existing.data?.postedAt ? new Date(existing.data.postedAt).toLocaleString('ru') : ''}</span>
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -319,17 +319,17 @@ export function EnterEditPage() {
|
|||
</div>
|
||||
|
||||
{form.lines.length === 0 ? (
|
||||
<div className="text-sm text-slate-500 py-6 text-center">Нет позиций. Добавь товар из справочника.</div>
|
||||
<div className="text-sm text-slate-500 dark:text-slate-400 py-6 text-center">Нет позиций. Добавь товар из справочника.</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-left">
|
||||
<tr className="border-b border-slate-200 dark:border-slate-700">
|
||||
<th className="py-2 pr-3 font-medium text-xs uppercase text-slate-500">Товар</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[80px]">Ед.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[120px] text-right">Кол-во</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[140px] text-right">Цена ед.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[140px] text-right">Сумма</th>
|
||||
<th className="py-2 pr-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400">Товар</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[80px]">Ед.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[120px] text-right">Кол-во</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[140px] text-right">Цена ед.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[140px] text-right">Сумма</th>
|
||||
<th className="w-8"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -338,9 +338,9 @@ export function EnterEditPage() {
|
|||
<tr key={i} className="border-b border-slate-100 dark:border-slate-800">
|
||||
<td className="py-2 pr-3">
|
||||
<div className="font-medium text-slate-800 dark:text-slate-100">{l.productName || '(без названия)'}</div>
|
||||
{l.productArticle && <div className="text-xs text-slate-500">{l.productArticle}</div>}
|
||||
{l.productArticle && <div className="text-xs text-slate-500 dark:text-slate-400">{l.productArticle}</div>}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-slate-500">{l.unitSymbol ?? '—'}</td>
|
||||
<td className="py-2 px-3 text-slate-500 dark:text-slate-400">{l.unitSymbol ?? '—'}</td>
|
||||
<td className="py-2 px-3 text-right">
|
||||
<NumberInput value={l.quantity} disabled={isPosted}
|
||||
onChange={(v) => updateLine(i, { quantity: v ?? 0 })} />
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@ export function InventoryEditPage() {
|
|||
{isNew ? 'Новая инвентаризация' : existing.data?.number ?? 'Инвентаризация'}
|
||||
</h1>
|
||||
{isPosted && (
|
||||
<p className="text-xs text-slate-500">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
<span className="text-green-700 inline-flex items-center gap-1"><CheckCircle className="w-3 h-3" /> Проведён {existing.data?.postedAt ? new Date(existing.data.postedAt).toLocaleString('ru') : ''}</span>
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -305,7 +305,7 @@ export function InventoryEditPage() {
|
|||
<div className="mt-4 flex flex-wrap gap-3 text-sm">
|
||||
<span className="px-2 py-1 rounded bg-green-50 text-green-700">Излишек: +{surplusValue.toLocaleString('ru')}</span>
|
||||
<span className="px-2 py-1 rounded bg-red-50 text-red-700">Недостача: {shortageValue.toLocaleString('ru')}</span>
|
||||
<span className="px-2 py-1 rounded bg-slate-50 text-slate-600">Итого расхождений: {(surplusValue + shortageValue).toLocaleString('ru')}</span>
|
||||
<span className="px-2 py-1 rounded bg-slate-50 dark:bg-slate-800/60 text-slate-600 dark:text-slate-300">Итого расхождений: {(surplusValue + shortageValue).toLocaleString('ru')}</span>
|
||||
</div>
|
||||
|
||||
{!isNew && (
|
||||
|
|
@ -340,19 +340,19 @@ export function InventoryEditPage() {
|
|||
<section className="bg-white dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-800 p-4">
|
||||
<h2 className="font-medium text-slate-900 dark:text-slate-100 mb-3">Позиции</h2>
|
||||
{form.lines.length === 0 ? (
|
||||
<div className="text-sm text-slate-500 py-6 text-center">Нет товаров на складе на момент создания документа.</div>
|
||||
<div className="text-sm text-slate-500 dark:text-slate-400 py-6 text-center">Нет товаров на складе на момент создания документа.</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-left">
|
||||
<tr className="border-b border-slate-200 dark:border-slate-700">
|
||||
<th className="py-2 pr-3 font-medium text-xs uppercase text-slate-500">Товар</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[80px]">Ед.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[110px] text-right">Учёт</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[130px] text-right">Факт</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[110px] text-right">Расхожд.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[110px] text-right">Цена ед.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[130px] text-right">Сумма</th>
|
||||
<th className="py-2 pr-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400">Товар</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[80px]">Ед.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[110px] text-right">Учёт</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[130px] text-right">Факт</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[110px] text-right">Расхожд.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[110px] text-right">Цена ед.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[130px] text-right">Сумма</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -360,19 +360,19 @@ export function InventoryEditPage() {
|
|||
<tr key={i} className="border-b border-slate-100 dark:border-slate-800">
|
||||
<td className="py-2 pr-3">
|
||||
<div className="font-medium text-slate-800 dark:text-slate-100">{l.productName || '(без названия)'}</div>
|
||||
{l.productArticle && <div className="text-xs text-slate-500">{l.productArticle}</div>}
|
||||
{l.productArticle && <div className="text-xs text-slate-500 dark:text-slate-400">{l.productArticle}</div>}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-slate-500">{l.unitSymbol ?? '—'}</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-slate-500">{l.bookQty.toLocaleString('ru')}</td>
|
||||
<td className="py-2 px-3 text-slate-500 dark:text-slate-400">{l.unitSymbol ?? '—'}</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-slate-500 dark:text-slate-400">{l.bookQty.toLocaleString('ru')}</td>
|
||||
<td className="py-2 px-3 text-right">
|
||||
<NumberInput value={l.actualQty} disabled={isPosted}
|
||||
onChange={(v) => updateLine(i, v ?? 0)} />
|
||||
</td>
|
||||
<td className={`py-2 px-3 text-right font-mono ${l.diff > 0 ? 'text-green-700' : l.diff < 0 ? 'text-red-700' : 'text-slate-500'}`}>
|
||||
<td className={`py-2 px-3 text-right font-mono ${l.diff > 0 ? 'text-green-700' : l.diff < 0 ? 'text-red-700' : 'text-slate-500 dark:text-slate-400'}`}>
|
||||
{l.diff === 0 ? '—' : `${l.diff > 0 ? '+' : ''}${l.diff.toLocaleString('ru')}`}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-slate-500">{l.unitCost.toLocaleString('ru')}</td>
|
||||
<td className={`py-2 px-3 text-right font-mono ${l.diff > 0 ? 'text-green-700' : l.diff < 0 ? 'text-red-700' : 'text-slate-500'}`}>
|
||||
<td className="py-2 px-3 text-right font-mono text-slate-500 dark:text-slate-400">{l.unitCost.toLocaleString('ru')}</td>
|
||||
<td className={`py-2 px-3 text-right font-mono ${l.diff > 0 ? 'text-green-700' : l.diff < 0 ? 'text-red-700' : 'text-slate-500 dark:text-slate-400'}`}>
|
||||
{l.diff === 0 ? '—' : (l.diff * l.unitCost).toLocaleString('ru')}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ export function LossEditPage() {
|
|||
{isNew ? 'Новое списание' : existing.data?.number ?? 'Списание'}
|
||||
</h1>
|
||||
{isPosted && (
|
||||
<p className="text-xs text-slate-500">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
<span className="text-green-700 inline-flex items-center gap-1"><CheckCircle className="w-3 h-3" /> Проведён {existing.data?.postedAt ? new Date(existing.data.postedAt).toLocaleString('ru') : ''}</span>
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -330,18 +330,18 @@ export function LossEditPage() {
|
|||
</div>
|
||||
|
||||
{form.lines.length === 0 ? (
|
||||
<div className="text-sm text-slate-500 py-6 text-center">Нет позиций.</div>
|
||||
<div className="text-sm text-slate-500 dark:text-slate-400 py-6 text-center">Нет позиций.</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-left">
|
||||
<tr className="border-b border-slate-200 dark:border-slate-700">
|
||||
<th className="py-2 pr-3 font-medium text-xs uppercase text-slate-500">Товар</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[80px]">Ед.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[100px] text-right">Остаток</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[120px] text-right">Списать</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[140px] text-right">Цена ед.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[140px] text-right">Сумма</th>
|
||||
<th className="py-2 pr-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400">Товар</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[80px]">Ед.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[100px] text-right">Остаток</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[120px] text-right">Списать</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[140px] text-right">Цена ед.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[140px] text-right">Сумма</th>
|
||||
<th className="w-8"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -350,10 +350,10 @@ export function LossEditPage() {
|
|||
<tr key={i} className="border-b border-slate-100 dark:border-slate-800">
|
||||
<td className="py-2 pr-3">
|
||||
<div className="font-medium text-slate-800 dark:text-slate-100">{l.productName || '(без названия)'}</div>
|
||||
{l.productArticle && <div className="text-xs text-slate-500">{l.productArticle}</div>}
|
||||
{l.productArticle && <div className="text-xs text-slate-500 dark:text-slate-400">{l.productArticle}</div>}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-slate-500">{l.unitSymbol ?? '—'}</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-slate-500">
|
||||
<td className="py-2 px-3 text-slate-500 dark:text-slate-400">{l.unitSymbol ?? '—'}</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-slate-500 dark:text-slate-400">
|
||||
{l.stockAtStore != null ? l.stockAtStore.toLocaleString('ru') : '—'}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-right">
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ export function LoyaltyProgramsPage() {
|
|||
{ header: 'Карт', width: '90px', className: 'text-right', cell: (r) => r.cardsCount },
|
||||
{ header: 'Статус', width: '110px', cell: (r) => r.isActive
|
||||
? <span className="text-xs px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-700">Активна</span>
|
||||
: <span className="text-xs px-1.5 py-0.5 rounded bg-slate-200 text-slate-600">Выключена</span> },
|
||||
: <span className="text-xs px-1.5 py-0.5 rounded bg-slate-200 text-slate-600 dark:text-slate-300">Выключена</span> },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ export function MoySkladImportPage() {
|
|||
{test.data && (
|
||||
<div className="text-sm text-emerald-700 dark:text-emerald-400 flex items-center gap-1.5">
|
||||
<CheckCircle className="w-4 h-4" /> Подключено: <strong>{test.data.organization}</strong>
|
||||
{test.data.inn && <span className="text-slate-500">(идентификатор {test.data.inn})</span>}
|
||||
{test.data.inn && <span className="text-slate-500 dark:text-slate-400">(идентификатор {test.data.inn})</span>}
|
||||
</div>
|
||||
)}
|
||||
{test.error && <div className="text-sm text-red-600">{formatError(test.error)}</div>}
|
||||
|
|
@ -222,7 +222,7 @@ function Stat({ label, value, accent }: { label: string; value: number; accent?:
|
|||
const fg = accent === 'green' ? 'text-emerald-700 dark:text-emerald-400' : ''
|
||||
return (
|
||||
<div className={`rounded-lg ${bg} p-3`}>
|
||||
<dt className={`text-xs uppercase ${accent === 'green' ? fg : 'text-slate-500'}`}>{label}</dt>
|
||||
<dt className={`text-xs uppercase ${accent === 'green' ? fg : 'text-slate-500 dark:text-slate-400'}`}>{label}</dt>
|
||||
<dd className={`text-lg font-semibold mt-1 ${fg}`}>{value.toLocaleString('ru')}</dd>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -300,9 +300,9 @@ function DangerZone() {
|
|||
{wipeJob.data.status === 'Failed' && <AlertCircle className="w-4 h-4 text-red-600" />}
|
||||
{wipeJob.data.status === 'Running' && <span className="inline-block w-3 h-3 rounded-full bg-red-500 animate-pulse" />}
|
||||
<strong>{wipeJob.data.stage ?? wipeJob.data.status}</strong>
|
||||
<span className="text-xs text-slate-500">удалено записей: {wipeJob.data.deleted.toLocaleString('ru')}</span>
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400">удалено записей: {wipeJob.data.deleted.toLocaleString('ru')}</span>
|
||||
</div>
|
||||
{wipeJob.data.message && <div className="text-xs text-slate-600">{wipeJob.data.message}</div>}
|
||||
{wipeJob.data.message && <div className="text-xs text-slate-600 dark:text-slate-300">{wipeJob.data.message}</div>}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
|
@ -312,7 +312,7 @@ function DangerZone() {
|
|||
function Tile({ label, value }: { label: string; value: number }) {
|
||||
return (
|
||||
<div className="rounded bg-white dark:bg-slate-900 border border-red-200 dark:border-red-900/50 px-2 py-1.5">
|
||||
<dt className="text-[10px] uppercase text-slate-500">{label}</dt>
|
||||
<dt className="text-[10px] uppercase text-slate-500 dark:text-slate-400">{label}</dt>
|
||||
<dd className="font-mono font-semibold">{value.toLocaleString('ru')}</dd>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -104,11 +104,11 @@ export function OrgAuditLogPage() {
|
|||
}`}>{r.action}</span>
|
||||
)},
|
||||
{ header: 'EntityId', width: '110px', cell: (r) => (
|
||||
r.entityId ? <span className="font-mono text-xs text-slate-500">{r.entityId.slice(0, 8)}</span> : '—'
|
||||
r.entityId ? <span className="font-mono text-xs text-slate-500 dark:text-slate-400">{r.entityId.slice(0, 8)}</span> : '—'
|
||||
)},
|
||||
{ header: 'Изменения', cell: (r) => (
|
||||
<details className="text-xs">
|
||||
<summary className="cursor-pointer text-slate-500 hover:text-slate-700">показать diff</summary>
|
||||
<summary className="cursor-pointer text-slate-500 dark:text-slate-400 hover:text-slate-700">показать diff</summary>
|
||||
<pre className="mt-1 p-2 bg-slate-50 dark:bg-slate-800 rounded text-[10px] max-w-2xl overflow-x-auto">{
|
||||
(() => {
|
||||
try { return JSON.stringify(JSON.parse(r.changesJson), null, 2) }
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ export function OrganizationSettingsPage() {
|
|||
checked={form.multiCurrencyEnabled}
|
||||
onChange={(v) => setForm({ ...form, multiCurrencyEnabled: v })}
|
||||
/>
|
||||
<p className="text-xs text-slate-500 -mt-2">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 -mt-2">
|
||||
Если выключено — в формах продаж и закупок выбор валюты не показывается, всё считается в валюте по умолчанию.
|
||||
</p>
|
||||
|
||||
|
|
@ -110,7 +110,7 @@ export function OrganizationSettingsPage() {
|
|||
</Field>
|
||||
<div />
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 -mt-2">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 -mt-2">
|
||||
Валюта и ставка НДС берутся из страны (<strong>{form.countryCode}</strong>) —
|
||||
чтобы изменить — обратитесь к администратору платформы (справочник стран управляется в системной консоли).
|
||||
</p>
|
||||
|
|
@ -120,7 +120,7 @@ export function OrganizationSettingsPage() {
|
|||
checked={form.showVatEnabledOnProduct}
|
||||
onChange={(v) => setForm({ ...form, showVatEnabledOnProduct: v })}
|
||||
/>
|
||||
<p className="text-xs text-slate-500 -mt-2">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 -mt-2">
|
||||
Если выключено — поля «НДС %» и «В том числе НДС» на карточке товара скрыты,
|
||||
все новые товары получают ставку из страны организации. Если включено — у каждого
|
||||
товара можно задать ставку вручную (хлеб = 0%, лекарства = 0% и т.п.).
|
||||
|
|
@ -131,7 +131,7 @@ export function OrganizationSettingsPage() {
|
|||
checked={form.showServiceOnProduct}
|
||||
onChange={(v) => setForm({ ...form, showServiceOnProduct: v })}
|
||||
/>
|
||||
<p className="text-xs text-slate-500 -mt-2">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 -mt-2">
|
||||
Нужно, если помимо физических товаров продаются услуги (доставка, сборка и т.п.).
|
||||
По умолчанию галка скрыта.
|
||||
</p>
|
||||
|
|
@ -141,7 +141,7 @@ export function OrganizationSettingsPage() {
|
|||
checked={form.showMarkedOnProduct}
|
||||
onChange={(v) => setForm({ ...form, showMarkedOnProduct: v })}
|
||||
/>
|
||||
<p className="text-xs text-slate-500 -mt-2">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 -mt-2">
|
||||
Включать, только если продаётся маркируемая категория (алкоголь, табак, лекарства).
|
||||
По умолчанию галка скрыта.
|
||||
</p>
|
||||
|
|
@ -151,7 +151,7 @@ export function OrganizationSettingsPage() {
|
|||
checked={form.showMinMaxStock}
|
||||
onChange={(v) => setForm({ ...form, showMinMaxStock: v })}
|
||||
/>
|
||||
<p className="text-xs text-slate-500 -mt-2">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 -mt-2">
|
||||
Если включено — на карточке товара есть поля «Минимальный / Максимальный остаток»
|
||||
для автозаказа. По умолчанию скрыто.
|
||||
</p>
|
||||
|
|
@ -161,7 +161,7 @@ export function OrganizationSettingsPage() {
|
|||
checked={form.allowFractionalPrices}
|
||||
onChange={(v) => setForm({ ...form, allowFractionalPrices: v })}
|
||||
/>
|
||||
<p className="text-xs text-slate-500 -mt-2">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 -mt-2">
|
||||
Включи если хочешь использовать цены с двумя знаками после запятой (1500.50 ₸).
|
||||
По умолчанию — целые тенге, без копеек.
|
||||
</p>
|
||||
|
|
@ -171,7 +171,7 @@ export function OrganizationSettingsPage() {
|
|||
checked={form.showReferencePriceOnProduct}
|
||||
onChange={(v) => setForm({ ...form, showReferencePriceOnProduct: v })}
|
||||
/>
|
||||
<p className="text-xs text-slate-500 -mt-2">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 -mt-2">
|
||||
Справочная цена закупа — необязательное поле. Авто-заполняется первой
|
||||
проведённой приёмкой и через 30 дней без приёмок переписывается на текущую себестоимость.
|
||||
</p>
|
||||
|
|
@ -181,7 +181,7 @@ export function OrganizationSettingsPage() {
|
|||
checked={form.showCountryOfOriginOnProduct}
|
||||
onChange={(v) => setForm({ ...form, showCountryOfOriginOnProduct: v })}
|
||||
/>
|
||||
<p className="text-xs text-slate-500 -mt-2">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 -mt-2">
|
||||
По умолчанию выключено. Включай если торгуешь импортом или ведёшь
|
||||
учёт по странам — тогда в карточке товара будет селект «Страна происхождения».
|
||||
</p>
|
||||
|
|
@ -191,7 +191,7 @@ export function OrganizationSettingsPage() {
|
|||
checked={form.showDescriptionOnProduct}
|
||||
onChange={(v) => setForm({ ...form, showDescriptionOnProduct: v })}
|
||||
/>
|
||||
<p className="text-xs text-slate-500 -mt-2">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 -mt-2">
|
||||
По умолчанию выключено — описания захламляют карточку. Включай если ведёшь
|
||||
подробные тексты на товарах.
|
||||
</p>
|
||||
|
|
@ -254,9 +254,9 @@ function TelegramSection() {
|
|||
const bound = !!s?.chatId
|
||||
|
||||
return (
|
||||
<section className="border-t border-slate-200 pt-6 mt-6">
|
||||
<section className="border-t border-slate-200 dark:border-slate-800 pt-6 mt-6">
|
||||
<h2 className="text-base font-semibold">📨 Telegram владельца</h2>
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400 mt-1">
|
||||
Ежедневная сводка (выручка вчера, топ-3 товара, low-stock) в 09:00 МСК.
|
||||
</p>
|
||||
|
||||
|
|
@ -328,12 +328,12 @@ function DemoSeedSection() {
|
|||
const seeded = !!s?.alreadySeeded
|
||||
|
||||
return (
|
||||
<section className="border-t border-slate-200 pt-6 mt-6">
|
||||
<section className="border-t border-slate-200 dark:border-slate-800 pt-6 mt-6">
|
||||
<h2 className="text-base font-semibold flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 text-amber-500" />
|
||||
Демо-данные
|
||||
</h2>
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400 mt-1">
|
||||
Заполняет организацию реалистичными данными для демонстрации стейджа: 50 товаров
|
||||
в 5 группах, 10 контрагентов, 5 приёмок, 30 продаж, 1 опт-отгрузка,
|
||||
1 списание, 1 перемещение, 1 инвентаризация. Идемпотентно — повторный
|
||||
|
|
@ -341,11 +341,11 @@ function DemoSeedSection() {
|
|||
</p>
|
||||
|
||||
{status.isLoading && (
|
||||
<p className="text-sm text-slate-500 mt-3">Загружаю статус…</p>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400 mt-3">Загружаю статус…</p>
|
||||
)}
|
||||
|
||||
{s && (
|
||||
<div className="mt-3 text-xs text-slate-600 grid grid-cols-2 sm:grid-cols-5 gap-x-4 gap-y-1">
|
||||
<div className="mt-3 text-xs text-slate-600 dark:text-slate-300 grid grid-cols-2 sm:grid-cols-5 gap-x-4 gap-y-1">
|
||||
<div>Товаров: <b>{s.products}</b></div>
|
||||
<div>Групп: <b>{s.groups}</b></div>
|
||||
<div>Контрагентов: <b>{s.counterparties}</b></div>
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ export function PriceTypesPage() {
|
|||
{form && (
|
||||
<div className="space-y-3">
|
||||
{form.isSystem && (
|
||||
<p className="text-xs text-slate-500 bg-slate-50 dark:bg-slate-800/40 p-2 rounded border border-slate-200 dark:border-slate-700">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 bg-slate-50 dark:bg-slate-800/40 p-2 rounded border border-slate-200 dark:border-slate-700">
|
||||
Системная запись. Имя можно переименовать; флаг «Обязательно» зафиксирован, удаление запрещено.
|
||||
</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -305,7 +305,7 @@ export function ProductEditPage() {
|
|||
|
||||
<div className="mt-4 pt-4 border-t border-slate-100 dark:border-slate-800">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wide text-slate-500">Штрихкоды</h3>
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">Штрихкоды</h3>
|
||||
<Button type="button" variant="secondary" size="sm" onClick={addBarcode}>
|
||||
<Plus className="w-3.5 h-3.5" /> Добавить
|
||||
</Button>
|
||||
|
|
@ -401,7 +401,7 @@ export function ProductEditPage() {
|
|||
>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-x-6 gap-y-5">
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wide text-slate-500 mb-3">Закупка</h3>
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400 mb-3">Закупка</h3>
|
||||
<div className="space-y-3">
|
||||
{org.data?.showReferencePriceOnProduct && (
|
||||
<Field label="Эталонная цена">
|
||||
|
|
@ -436,7 +436,7 @@ export function ProductEditPage() {
|
|||
</div>
|
||||
|
||||
<div className="lg:border-l lg:border-slate-100 lg:dark:border-slate-800 lg:pl-6">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wide text-slate-500 mb-3">Цены продажи</h3>
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400 mb-3">Цены продажи</h3>
|
||||
{/* Список цен рендерится по справочнику PriceType: одно поле на каждый
|
||||
* тип, без выпадашки выбора. Значение хранится в form.prices,
|
||||
* key = priceTypeId. Для отсутствующих записей при наборе создаётся
|
||||
|
|
@ -605,7 +605,7 @@ function AdvancedSection({ children }: { children: ReactNode }) {
|
|||
className="w-full flex items-center justify-between px-5 py-3 border-b border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-800/30 text-left hover:bg-slate-100/60 dark:hover:bg-slate-800/50"
|
||||
>
|
||||
<h2 className="text-sm font-semibold text-slate-900 dark:text-slate-100">Расширенные параметры</h2>
|
||||
<span className="text-xs text-slate-500">{open ? 'свернуть' : 'развернуть'}</span>
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400">{open ? 'свернуть' : 'развернуть'}</span>
|
||||
</button>
|
||||
{open && <div className="p-5">{children}</div>}
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ export function ProductGroupsPage() {
|
|||
)}
|
||||
</span>
|
||||
)},
|
||||
{ header: 'Путь', sortKey: 'path', cell: (r) => <span className="text-slate-500">{r.path}</span> },
|
||||
{ header: 'Путь', sortKey: 'path', cell: (r) => <span className="text-slate-500 dark:text-slate-400">{r.path}</span> },
|
||||
{ header: 'Наценка', width: '140px', cell: (r) => (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<PercentInput
|
||||
|
|
@ -150,7 +150,7 @@ export function ProductGroupsPage() {
|
|||
onChange={(n) => setForm({ ...form, markupPercent: n })}
|
||||
placeholder="нет автонаценки"
|
||||
/>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
||||
При проведении приёмки розничная цена товара = ⌈Себестоимость × (1 + наценка/100)⌉.
|
||||
Пусто — автонаценка отключена.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ function Tri({
|
|||
]
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span className="text-slate-500">{label}</span>
|
||||
<span className="text-slate-500 dark:text-slate-400">{label}</span>
|
||||
<div className="inline-flex rounded border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||
{opts.map((o) => (
|
||||
<button
|
||||
|
|
@ -197,7 +197,7 @@ export function ProductsPage() {
|
|||
<aside className="absolute left-0 top-0 bottom-0 w-72 max-w-[85vw] bg-white dark:bg-slate-900 border-r border-slate-200 dark:border-slate-800 flex flex-col shadow-xl">
|
||||
<div className="h-12 flex items-center justify-between px-4 border-b border-slate-200 dark:border-slate-800">
|
||||
<span className="font-medium text-sm">Группы товаров</span>
|
||||
<button onClick={() => setGroupsOpen(false)} className="text-slate-500"><X className="w-5 h-5" /></button>
|
||||
<button onClick={() => setGroupsOpen(false)} className="text-slate-500 dark:text-slate-400"><X className="w-5 h-5" /></button>
|
||||
</div>
|
||||
<div className="flex-1 min-h-0 overflow-auto">{groupsTree}</div>
|
||||
</aside>
|
||||
|
|
@ -216,7 +216,7 @@ export function ProductsPage() {
|
|||
</button>
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-base font-semibold">Товары</h1>
|
||||
<p className="text-xs text-slate-500">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
{data ? `${data.total.toLocaleString('ru')} записей` : 'Каталог товаров и услуг'}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -241,7 +241,7 @@ export function ProductsPage() {
|
|||
<Tri label="Услуга" value={filters.isService} onChange={(v) => { setFilters({ ...filters, isService: v }); setPage(1) }} />
|
||||
)}
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span className="text-slate-500">Фасовка</span>
|
||||
<span className="text-slate-500 dark:text-slate-400">Фасовка</span>
|
||||
<select
|
||||
value={filters.packaging ?? ''}
|
||||
onChange={(e) => { const v = e.target.value; setFilters({ ...filters, packaging: v ? Number(v) : null }); setPage(1) }}
|
||||
|
|
@ -257,7 +257,7 @@ export function ProductsPage() {
|
|||
<Tri label="Маркируемый" value={filters.isMarked} onChange={(v) => { setFilters({ ...filters, isMarked: v }); setPage(1) }} />
|
||||
)}
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span className="text-slate-500">{systemPriceType?.name ?? 'Цена'}</span>
|
||||
<span className="text-slate-500 dark:text-slate-400">{systemPriceType?.name ?? 'Цена'}</span>
|
||||
<div className="w-32">
|
||||
<MoneyInput
|
||||
value={filters.systemPriceFrom}
|
||||
|
|
@ -281,7 +281,7 @@ export function ProductsPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => { setFilters(defaultFilters); setPage(1) }}
|
||||
className="text-xs text-slate-500 hover:text-slate-800 inline-flex items-center gap-1"
|
||||
className="text-xs text-slate-500 dark:text-slate-400 hover:text-slate-800 inline-flex items-center gap-1"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" /> Сбросить
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export function ProfitReportPage() {
|
|||
<div className="flex flex-col h-full">
|
||||
<div className="px-6 py-4 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900">
|
||||
<h1 className="text-lg font-semibold text-slate-900 dark:text-slate-100">Отчёт «Прибыль»</h1>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
||||
Выручка − себестоимость = прибыль. COGS-snapshot — Product.Cost
|
||||
(скользящее среднее на момент запроса; приближённая оценка).
|
||||
</p>
|
||||
|
|
@ -138,7 +138,7 @@ export function ProfitReportPage() {
|
|||
</section>
|
||||
|
||||
<section className="bg-white dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-800 p-4">
|
||||
{rep.isLoading && <div className="text-sm text-slate-500 py-6 text-center">Загружаю…</div>}
|
||||
{rep.isLoading && <div className="text-sm text-slate-500 dark:text-slate-400 py-6 text-center">Загружаю…</div>}
|
||||
{!rep.isLoading && (rep.data?.length ?? 0) === 0 && (
|
||||
<EmptyState
|
||||
icon={TrendingUp}
|
||||
|
|
@ -151,12 +151,12 @@ export function ProfitReportPage() {
|
|||
<table className="w-full text-sm">
|
||||
<thead className="text-left">
|
||||
<tr className="border-b border-slate-200 dark:border-slate-700">
|
||||
<th className="py-2 pr-3 font-medium text-xs uppercase text-slate-500">Группа</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[130px] text-right">Выручка</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[130px] text-right">Себест.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[130px] text-right">Прибыль</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[110px] text-right">Маржа,%</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[120px] text-right">Кол-во</th>
|
||||
<th className="py-2 pr-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400">Группа</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[130px] text-right">Выручка</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[130px] text-right">Себест.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[130px] text-right">Прибыль</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[110px] text-right">Маржа,%</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[120px] text-right">Кол-во</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -164,10 +164,10 @@ export function ProfitReportPage() {
|
|||
<tr key={r.key} className="border-b border-slate-100 dark:border-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800/30">
|
||||
<td className="py-2 pr-3">{r.label}</td>
|
||||
<td className="py-2 px-3 text-right font-mono">{r.revenue.toLocaleString('ru', moneyFmt)}</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-slate-500">{r.cost.toLocaleString('ru', moneyFmt)}</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-slate-500 dark:text-slate-400">{r.cost.toLocaleString('ru', moneyFmt)}</td>
|
||||
<td className={`py-2 px-3 text-right font-mono ${r.profit >= 0 ? 'text-green-700' : 'text-red-700'}`}>{r.profit.toLocaleString('ru', moneyFmt)}</td>
|
||||
<td className="py-2 px-3 text-right font-mono">{r.marginPercent.toFixed(2)}</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-slate-500">{r.quantity.toLocaleString('ru')}</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-slate-500 dark:text-slate-400">{r.quantity.toLocaleString('ru')}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ export function PromotionsPage() {
|
|||
{ header: 'Мин. чек', width: '110px', className: 'text-right font-mono', cell: (r) => r.minSaleAmount > 0 ? `${r.minSaleAmount.toLocaleString('ru')} ₸` : '—' },
|
||||
{ header: 'Статус', width: '110px', cell: (r) => r.isActive
|
||||
? <span className="text-xs px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-700">Активна</span>
|
||||
: <span className="text-xs px-1.5 py-0.5 rounded bg-slate-200 text-slate-600">Выключена</span> },
|
||||
: <span className="text-xs px-1.5 py-0.5 rounded bg-slate-200 text-slate-600 dark:text-slate-300">Выключена</span> },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ export function RetailSaleEditPage() {
|
|||
{isNew ? 'Новый чек' : existing.data?.number ?? 'Чек'}
|
||||
</h1>
|
||||
{isPosted && (
|
||||
<p className="text-xs text-slate-500">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
<span className="text-green-700 inline-flex items-center gap-1"><CheckCircle className="w-3 h-3" /> Проведён {existing.data?.postedAt ? new Date(existing.data.postedAt).toLocaleString('ru') : ''}</span>
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -378,12 +378,12 @@ export function RetailSaleEditPage() {
|
|||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200 dark:border-slate-700 text-left">
|
||||
<th className="py-2 pr-3 font-medium text-xs uppercase tracking-wide text-slate-500">Товар</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 w-[80px]">Ед.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 w-[120px] text-right">Кол-во</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 w-[120px] text-right">Цена</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 w-[110px] text-right">Скидка</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 w-[140px] text-right">Сумма</th>
|
||||
<th className="py-2 pr-3 font-medium text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400">Товар</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 w-[80px]">Ед.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 w-[120px] text-right">Кол-во</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 w-[120px] text-right">Цена</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 w-[110px] text-right">Скидка</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 w-[140px] text-right">Сумма</th>
|
||||
<th className="py-2 pl-3 w-[40px]"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -394,7 +394,7 @@ export function RetailSaleEditPage() {
|
|||
<div className="font-medium">{l.productName}</div>
|
||||
{l.productArticle && <div className="text-xs text-slate-400 font-mono">{l.productArticle}</div>}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-slate-500">{l.unitName}</td>
|
||||
<td className="py-2 px-3 text-slate-500 dark:text-slate-400">{l.unitName}</td>
|
||||
<td className="py-2 px-3">
|
||||
<NumberInput disabled={isPosted}
|
||||
value={l.quantity}
|
||||
|
|
@ -430,11 +430,11 @@ export function RetailSaleEditPage() {
|
|||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr><td colSpan={4} className="py-2 pr-3 text-right text-sm text-slate-500">Подытог:</td>
|
||||
<td className="py-2 px-3 text-right text-sm text-slate-500"></td>
|
||||
<tr><td colSpan={4} className="py-2 pr-3 text-right text-sm text-slate-500 dark:text-slate-400">Подытог:</td>
|
||||
<td className="py-2 px-3 text-right text-sm text-slate-500 dark:text-slate-400"></td>
|
||||
<td className="py-2 px-3 text-right font-mono">{subtotal.toLocaleString('ru', { maximumFractionDigits: 2 })}</td>
|
||||
<td/></tr>
|
||||
<tr><td colSpan={4} className="py-1 pr-3 text-right text-sm text-slate-500">Скидка:</td>
|
||||
<tr><td colSpan={4} className="py-1 pr-3 text-right text-sm text-slate-500 dark:text-slate-400">Скидка:</td>
|
||||
<td className="py-1 px-3"></td>
|
||||
<td className="py-1 px-3 text-right font-mono text-red-600">−{discountTotal.toLocaleString('ru', { maximumFractionDigits: 2 })}</td>
|
||||
<td/></tr>
|
||||
|
|
@ -442,7 +442,7 @@ export function RetailSaleEditPage() {
|
|||
<td/>
|
||||
<td className="py-3 px-3 text-right font-mono text-lg font-bold">
|
||||
{grandTotal.toLocaleString('ru', { maximumFractionDigits: 2 })}{' '}
|
||||
<span className="text-sm text-slate-500">{currencies.data?.find((c) => c.id === form.currencyId)?.code ?? ''}</span>
|
||||
<span className="text-sm text-slate-500 dark:text-slate-400">{currencies.data?.find((c) => c.id === form.currencyId)?.code ?? ''}</span>
|
||||
</td><td/></tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export function SalesReportPage() {
|
|||
<div className="flex flex-col h-full">
|
||||
<div className="px-6 py-4 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900">
|
||||
<h1 className="text-lg font-semibold text-slate-900 dark:text-slate-100">Отчёт «Продажи»</h1>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
||||
Проведённые чеки за период. Возвраты включаются с минусом (netto).
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -136,7 +136,7 @@ export function SalesReportPage() {
|
|||
</section>
|
||||
|
||||
<section className="bg-white dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-800 p-4">
|
||||
{rep.isLoading && <div className="text-sm text-slate-500 py-6 text-center">Загружаю…</div>}
|
||||
{rep.isLoading && <div className="text-sm text-slate-500 dark:text-slate-400 py-6 text-center">Загружаю…</div>}
|
||||
{!rep.isLoading && (rep.data?.length ?? 0) === 0 && (
|
||||
<EmptyState
|
||||
icon={BarChart3}
|
||||
|
|
@ -149,11 +149,11 @@ export function SalesReportPage() {
|
|||
<table className="w-full text-sm">
|
||||
<thead className="text-left">
|
||||
<tr className="border-b border-slate-200 dark:border-slate-700">
|
||||
<th className="py-2 pr-3 font-medium text-xs uppercase text-slate-500">Группа</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[130px] text-right">Выручка</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[120px] text-right">Скидки</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[90px] text-right">Чеков</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[120px] text-right">Кол-во</th>
|
||||
<th className="py-2 pr-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400">Группа</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[130px] text-right">Выручка</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[120px] text-right">Скидки</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[90px] text-right">Чеков</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[120px] text-right">Кол-во</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -161,9 +161,9 @@ export function SalesReportPage() {
|
|||
<tr key={r.key} className="border-b border-slate-100 dark:border-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800/30">
|
||||
<td className="py-2 pr-3">{r.label}</td>
|
||||
<td className="py-2 px-3 text-right font-mono">{r.revenue.toLocaleString('ru', moneyFmt)}</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-slate-500">{r.discount === 0 ? '—' : r.discount.toLocaleString('ru', moneyFmt)}</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-slate-500">{r.transactions}</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-slate-500">{r.quantity.toLocaleString('ru')}</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-slate-500 dark:text-slate-400">{r.discount === 0 ? '—' : r.discount.toLocaleString('ru', moneyFmt)}</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-slate-500 dark:text-slate-400">{r.transactions}</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-slate-500 dark:text-slate-400">{r.quantity.toLocaleString('ru')}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export function StockReportPage() {
|
|||
<div className="flex flex-col h-full">
|
||||
<div className="px-6 py-4 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900">
|
||||
<h1 className="text-lg font-semibold text-slate-900 dark:text-slate-100">Отчёт «Остатки на дату»</h1>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
||||
Реконструкция через журнал движений. Стоимость — последний UnitCost
|
||||
движения; если в журнале нет — Product.Cost (приближённая оценка).
|
||||
</p>
|
||||
|
|
@ -113,7 +113,7 @@ export function StockReportPage() {
|
|||
</section>
|
||||
|
||||
<section className="bg-white dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-800 p-4">
|
||||
{rep.isLoading && <div className="text-sm text-slate-500 py-6 text-center">Загружаю…</div>}
|
||||
{rep.isLoading && <div className="text-sm text-slate-500 dark:text-slate-400 py-6 text-center">Загружаю…</div>}
|
||||
{!rep.isLoading && (rep.data?.length ?? 0) === 0 && (
|
||||
<EmptyState
|
||||
icon={Warehouse}
|
||||
|
|
@ -126,24 +126,24 @@ export function StockReportPage() {
|
|||
<table className="w-full text-sm">
|
||||
<thead className="text-left">
|
||||
<tr className="border-b border-slate-200 dark:border-slate-700">
|
||||
<th className="py-2 pr-3 font-medium text-xs uppercase text-slate-500">Товар</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[100px]">Артикул</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[80px]">Ед.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[180px]">Склад</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[120px] text-right">Кол-во</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[120px] text-right">Цена</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[140px] text-right">Стоимость</th>
|
||||
<th className="py-2 pr-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400">Товар</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[100px]">Артикул</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[80px]">Ед.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[180px]">Склад</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[120px] text-right">Кол-во</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[120px] text-right">Цена</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[140px] text-right">Стоимость</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rep.data.map((r) => (
|
||||
<tr key={`${r.productId}-${r.storeId}`} className="border-b border-slate-100 dark:border-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800/30">
|
||||
<td className="py-2 pr-3">{r.productName}</td>
|
||||
<td className="py-2 px-3 text-slate-500">{r.productArticle ?? '—'}</td>
|
||||
<td className="py-2 px-3 text-slate-500">{r.unitName ?? '—'}</td>
|
||||
<td className="py-2 px-3 text-slate-500">{r.storeName}</td>
|
||||
<td className="py-2 px-3 text-slate-500 dark:text-slate-400">{r.productArticle ?? '—'}</td>
|
||||
<td className="py-2 px-3 text-slate-500 dark:text-slate-400">{r.unitName ?? '—'}</td>
|
||||
<td className="py-2 px-3 text-slate-500 dark:text-slate-400">{r.storeName}</td>
|
||||
<td className="py-2 px-3 text-right font-mono">{r.quantity.toLocaleString('ru')}</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-slate-500">{r.cost.toLocaleString('ru', moneyFmt)}</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-slate-500 dark:text-slate-400">{r.cost.toLocaleString('ru', moneyFmt)}</td>
|
||||
<td className="py-2 px-3 text-right font-mono">{r.value.toLocaleString('ru', moneyFmt)}</td>
|
||||
</tr>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ function Kpi({ icon: Icon, label, value, hint, accent = 'indigo', muted = false
|
|||
<Icon className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="text-[11px] uppercase tracking-wide text-slate-500">{label}</div>
|
||||
<div className="text-[11px] uppercase tracking-wide text-slate-500 dark:text-slate-400">{label}</div>
|
||||
<div className={cn('text-2xl font-bold mt-0.5 leading-tight', muted && 'text-slate-400')}>{value}</div>
|
||||
{hint && <div className="text-xs text-slate-400 mt-0.5">{hint}</div>}
|
||||
</div>
|
||||
|
|
@ -169,7 +169,7 @@ export function SuperAdminDashboardPage() {
|
|||
{fmt.format(o.productCount)} товаров
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-slate-500">
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400">
|
||||
{fmt.format(o.employeeCount)} сотр.
|
||||
{o.lastLoginAt && <> · last login {new Date(o.lastLoginAt).toLocaleDateString('ru')}</>}
|
||||
</div>
|
||||
|
|
@ -197,12 +197,12 @@ export function SuperAdminDashboardPage() {
|
|||
{audit.data?.slice(0, 6).map((r) => (
|
||||
<li key={r.id} className="py-2 text-sm">
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<span className="text-xs font-mono text-slate-500">{r.actionType}</span>
|
||||
<span className="text-xs font-mono text-slate-500 dark:text-slate-400">{r.actionType}</span>
|
||||
<span className="text-xs text-slate-400">{new Date(r.createdAt).toLocaleString('ru', { dateStyle: 'short', timeStyle: 'short' })}</span>
|
||||
</div>
|
||||
<div className="text-slate-700 dark:text-slate-300 truncate">
|
||||
{r.organizationName && <span className="font-medium mr-1">«{r.organizationName}»</span>}
|
||||
<span className="text-slate-500">{r.description}</span>
|
||||
<span className="text-slate-500 dark:text-slate-400">{r.description}</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@ export function SuperAdminOrgEmployeesPage() {
|
|||
<button
|
||||
title={r.isActive ? 'Деактивировать сотрудника' : 'Активировать сотрудника'}
|
||||
onClick={() => { setToggleConfirm({ row: r, activate: !r.isActive, target: 'employee' }); setToggleReason('') }}
|
||||
className="p-1.5 text-slate-600 hover:bg-slate-100 rounded">
|
||||
className="p-1.5 text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 rounded">
|
||||
{r.isActive ? <PowerOff className="w-4 h-4" /> : <Power className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -289,7 +289,7 @@ export function SuperAdminOrgEmployeesPage() {
|
|||
</div>
|
||||
<Field label="Роль *">
|
||||
<select value={form.roleId} onChange={(e) => setForm({ ...form, roleId: e.target.value })}
|
||||
className="w-full h-10 rounded-md border border-slate-300 bg-white px-3 text-sm">
|
||||
className="w-full h-10 rounded-md border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 px-3 text-sm">
|
||||
<option value="">— выберите —</option>
|
||||
{roles.data?.map((r) => (
|
||||
<option key={r.id} value={r.id}>
|
||||
|
|
@ -327,7 +327,7 @@ export function SuperAdminOrgEmployeesPage() {
|
|||
</>}>
|
||||
{resetFor && (
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-slate-700">
|
||||
<p className="text-sm text-slate-700 dark:text-slate-200">
|
||||
Будет сгенерирован новый временный пароль для <strong>{resetFor.email ?? '—'}</strong>.
|
||||
Все активные сессии этого юзера будут оборваны.
|
||||
</p>
|
||||
|
|
@ -344,7 +344,7 @@ export function SuperAdminOrgEmployeesPage() {
|
|||
footer={<Button onClick={() => setResetResult(null)}>Готово</Button>}>
|
||||
{resetResult && (
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-slate-700">
|
||||
<p className="text-sm text-slate-700 dark:text-slate-200">
|
||||
Передайте логин и пароль пользователю. <strong>Этот пароль показывается один раз.</strong>
|
||||
</p>
|
||||
<Field label="Email">
|
||||
|
|
@ -382,7 +382,7 @@ export function SuperAdminOrgEmployeesPage() {
|
|||
</>}>
|
||||
{toggleConfirm && (
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-slate-700">
|
||||
<p className="text-sm text-slate-700 dark:text-slate-200">
|
||||
{toggleConfirm.target === 'account'
|
||||
? (toggleConfirm.activate
|
||||
? <>Разблокировать вход <strong>{toggleConfirm.row.email}</strong>. Юзер сможет залогиниться при следующей попытке.</>
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ export function SupplierReturnEditPage() {
|
|||
{isNew ? 'Новый возврат поставщику' : existing.data?.number ?? 'Возврат поставщику'}
|
||||
</h1>
|
||||
{isPosted && (
|
||||
<p className="text-xs text-slate-500">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
<span className="text-green-700 inline-flex items-center gap-1"><CheckCircle className="w-3 h-3" /> Проведён {existing.data?.postedAt ? new Date(existing.data.postedAt).toLocaleString('ru') : ''}</span>
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -334,18 +334,18 @@ export function SupplierReturnEditPage() {
|
|||
</div>
|
||||
|
||||
{form.lines.length === 0 ? (
|
||||
<div className="text-sm text-slate-500 py-6 text-center">Нет позиций.</div>
|
||||
<div className="text-sm text-slate-500 dark:text-slate-400 py-6 text-center">Нет позиций.</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-left">
|
||||
<tr className="border-b border-slate-200 dark:border-slate-700">
|
||||
<th className="py-2 pr-3 font-medium text-xs uppercase text-slate-500">Товар</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[80px]">Ед.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[100px] text-right">Остаток</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[120px] text-right">Возвращ.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[140px] text-right">Цена</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[140px] text-right">Сумма</th>
|
||||
<th className="py-2 pr-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400">Товар</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[80px]">Ед.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[100px] text-right">Остаток</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[120px] text-right">Возвращ.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[140px] text-right">Цена</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[140px] text-right">Сумма</th>
|
||||
<th className="w-8"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -354,10 +354,10 @@ export function SupplierReturnEditPage() {
|
|||
<tr key={i} className="border-b border-slate-100 dark:border-slate-800">
|
||||
<td className="py-2 pr-3">
|
||||
<div className="font-medium text-slate-800 dark:text-slate-100">{l.productName || '(без названия)'}</div>
|
||||
{l.productArticle && <div className="text-xs text-slate-500">{l.productArticle}</div>}
|
||||
{l.productArticle && <div className="text-xs text-slate-500 dark:text-slate-400">{l.productArticle}</div>}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-slate-500">{l.unitSymbol ?? '—'}</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-slate-500">
|
||||
<td className="py-2 px-3 text-slate-500 dark:text-slate-400">{l.unitSymbol ?? '—'}</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-slate-500 dark:text-slate-400">
|
||||
{l.stockAtStore != null ? l.stockAtStore.toLocaleString('ru') : '—'}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-right">
|
||||
|
|
|
|||
|
|
@ -300,7 +300,7 @@ export function SupplyEditPage() {
|
|||
{isNew ? 'Новая приёмка' : existing.data?.number ?? 'Приёмка'}
|
||||
</h1>
|
||||
{isPosted && (
|
||||
<p className="text-xs text-slate-500">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
<span className="text-green-700 inline-flex items-center gap-1"><CheckCircle className="w-3 h-3" /> Проведён {existing.data?.postedAt ? new Date(existing.data.postedAt).toLocaleString('ru') : ''}</span>
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -407,7 +407,7 @@ export function SupplyEditPage() {
|
|||
}
|
||||
}}
|
||||
/>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
||||
Только проведённый документ влияет на остатки склада и себестоимость.
|
||||
Черновик можно править, проведённый — только распровести и редактировать заново.
|
||||
{isPosted && existing.data?.postedAt && (
|
||||
|
|
@ -433,12 +433,12 @@ export function SupplyEditPage() {
|
|||
<table className="w-full text-sm">
|
||||
<thead className="text-left">
|
||||
<tr className="border-b border-slate-200 dark:border-slate-700">
|
||||
<th className="py-2 pr-3 font-medium text-xs uppercase tracking-wide text-slate-500">Товар</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 w-[90px]">Ед.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 w-[140px] text-right">Количество</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 w-[140px] text-right">Цена</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 w-[160px] text-right">{systemPriceTypeName} (карточка)</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 w-[160px] text-right">Сумма</th>
|
||||
<th className="py-2 pr-3 font-medium text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400">Товар</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 w-[90px]">Ед.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 w-[140px] text-right">Количество</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 w-[140px] text-right">Цена</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 w-[160px] text-right">{systemPriceTypeName} (карточка)</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400 w-[160px] text-right">Сумма</th>
|
||||
<th className="py-2 pl-3 w-[40px]"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -455,7 +455,7 @@ export function SupplyEditPage() {
|
|||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-slate-500">{l.unitName}</td>
|
||||
<td className="py-2 px-3 text-slate-500 dark:text-slate-400">{l.unitName}</td>
|
||||
<td className="py-2 px-3">
|
||||
<NumberInput disabled={isPosted}
|
||||
value={l.quantity}
|
||||
|
|
@ -502,7 +502,7 @@ export function SupplyEditPage() {
|
|||
<td className="py-3 px-3 text-right font-mono text-lg font-bold">
|
||||
{grandTotal.toLocaleString('ru', { maximumFractionDigits: 2 })}
|
||||
{' '}
|
||||
<span className="text-sm text-slate-500">
|
||||
<span className="text-sm text-slate-500 dark:text-slate-400">
|
||||
{currencies.data?.find((c) => c.id === form.currencyId)?.code ?? ''}
|
||||
</span>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@ export function TransferEditPage() {
|
|||
{isNew ? 'Новое перемещение' : existing.data?.number ?? 'Перемещение'}
|
||||
</h1>
|
||||
{isPosted && (
|
||||
<p className="text-xs text-slate-500">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
<span className="text-green-700 inline-flex items-center gap-1"><CheckCircle className="w-3 h-3" /> Проведён {existing.data?.postedAt ? new Date(existing.data.postedAt).toLocaleString('ru') : ''}</span>
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -312,18 +312,18 @@ export function TransferEditPage() {
|
|||
</div>
|
||||
|
||||
{form.lines.length === 0 ? (
|
||||
<div className="text-sm text-slate-500 py-6 text-center">Нет позиций.</div>
|
||||
<div className="text-sm text-slate-500 dark:text-slate-400 py-6 text-center">Нет позиций.</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-left">
|
||||
<tr className="border-b border-slate-200 dark:border-slate-700">
|
||||
<th className="py-2 pr-3 font-medium text-xs uppercase text-slate-500">Товар</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[80px]">Ед.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[110px] text-right">На отправителе</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[120px] text-right">Кол-во</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[140px] text-right">Цена ед.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 w-[140px] text-right">Сумма</th>
|
||||
<th className="py-2 pr-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400">Товар</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[80px]">Ед.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[110px] text-right">На отправителе</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[120px] text-right">Кол-во</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[140px] text-right">Цена ед.</th>
|
||||
<th className="py-2 px-3 font-medium text-xs uppercase text-slate-500 dark:text-slate-400 w-[140px] text-right">Сумма</th>
|
||||
<th className="w-8"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -332,10 +332,10 @@ export function TransferEditPage() {
|
|||
<tr key={i} className="border-b border-slate-100 dark:border-slate-800">
|
||||
<td className="py-2 pr-3">
|
||||
<div className="font-medium text-slate-800 dark:text-slate-100">{l.productName || '(без названия)'}</div>
|
||||
{l.productArticle && <div className="text-xs text-slate-500">{l.productArticle}</div>}
|
||||
{l.productArticle && <div className="text-xs text-slate-500 dark:text-slate-400">{l.productArticle}</div>}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-slate-500">{l.unitSymbol ?? '—'}</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-slate-500">
|
||||
<td className="py-2 px-3 text-slate-500 dark:text-slate-400">{l.unitSymbol ?? '—'}</td>
|
||||
<td className="py-2 px-3 text-right font-mono text-slate-500 dark:text-slate-400">
|
||||
{l.stockAtFrom != null ? l.stockAtFrom.toLocaleString('ru') : '—'}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-right">
|
||||
|
|
|
|||
|
|
@ -64,15 +64,15 @@ export function TransfersPage() {
|
|||
onSortChange={setSort}
|
||||
onRowClick={(r) => navigate(`/inventory/transfers/${r.id}`)}
|
||||
columns={[
|
||||
{ header: '№', width: '180px', sortKey: 'number', cell: (r) => <span className="font-mono text-slate-600">{r.number}</span> },
|
||||
{ header: '№', width: '180px', sortKey: 'number', cell: (r) => <span className="font-mono text-slate-600 dark:text-slate-300">{r.number}</span> },
|
||||
{ header: 'Дата', width: '120px', sortKey: 'date', cell: (r) => new Date(r.date).toLocaleDateString('ru') },
|
||||
{ header: 'Статус', width: '110px', sortKey: 'status', cell: (r) => (
|
||||
r.status === TransferStatus.Posted
|
||||
? <span className="text-xs px-2 py-0.5 rounded bg-green-50 text-green-700">Проведён</span>
|
||||
: <span className="text-xs px-2 py-0.5 rounded bg-slate-100 text-slate-600">Черновик</span>
|
||||
: <span className="text-xs px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300">Черновик</span>
|
||||
)},
|
||||
{ header: 'Откуда → Куда', cell: (r) => (
|
||||
<span className="inline-flex items-center gap-2 text-slate-700">
|
||||
<span className="inline-flex items-center gap-2 text-slate-700 dark:text-slate-200">
|
||||
<span>{r.fromStoreName}</span>
|
||||
<ArrowRight className="w-3.5 h-3.5 text-slate-400" />
|
||||
<span>{r.toStoreName}</span>
|
||||
|
|
|
|||
80
tests/e2e/scenarios/stage-ui-s10-dark-audit.spec.ts
Normal file
80
tests/e2e/scenarios/stage-ui-s10-dark-audit.spec.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* S10-4: аудит dark mode. Делает скриншоты топ-10 страниц в светлой и
|
||||
* тёмной теме на одном signup-сидед (год-демо). Перед-после фиксируются
|
||||
* в reports/dark-mode/.
|
||||
*
|
||||
* Что проверяется визуально:
|
||||
* - текст не растворяется в фон (text-slate-500 → dark:text-slate-400 etc).
|
||||
* - белые карточки в dark получают slate-900 фон.
|
||||
* - бордеры остаются заметными.
|
||||
*
|
||||
* Этот spec не делает assert'ов — это инструмент для ручного аудита
|
||||
* (screenshot-diff'ы). Считается passed если страница рендерится без
|
||||
* console-errors в обоих режимах.
|
||||
*/
|
||||
import { test, expect, type Page } from '@playwright/test'
|
||||
import { apiSignup, attachSession, watchPage, expectNoErrors } from '../lib/ui.js'
|
||||
import { mkdirSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { request as apiRequest } from '@playwright/test'
|
||||
|
||||
const BASE = process.env.E2E_ADMIN_URL ?? 'https://test.admin.food-market.kz'
|
||||
|
||||
const PAGES = [
|
||||
{ name: '01-dashboard', path: '/dashboard' },
|
||||
{ name: '02-products', path: '/catalog/products' },
|
||||
{ name: '03-counterparties', path: '/catalog/counterparties' },
|
||||
{ name: '04-stock', path: '/inventory/stock' },
|
||||
{ name: '05-supplies', path: '/purchases/supplies' },
|
||||
{ name: '06-retail-sales', path: '/sales/retail' },
|
||||
{ name: '07-report-sales', path: '/reports/sales' },
|
||||
{ name: '08-report-stock', path: '/reports/stock' },
|
||||
{ name: '09-report-profit', path: '/reports/profit' },
|
||||
{ name: '10-report-abc', path: '/reports/abc' },
|
||||
]
|
||||
|
||||
test.describe('S10-4 dark-mode audit', () => {
|
||||
test('snapshot 10 страниц light + dark', async ({ page, browser }) => {
|
||||
test.setTimeout(180_000)
|
||||
const dir = resolve('reports/dark-mode')
|
||||
mkdirSync(dir, { recursive: true })
|
||||
|
||||
// 1. Однажды поднимаем org с богатыми данными (year-seed).
|
||||
const sess = await apiSignup('s10dark')
|
||||
const ctx = await apiRequest.newContext({
|
||||
baseURL: BASE, ignoreHTTPSErrors: true,
|
||||
extraHTTPHeaders: { Authorization: `Bearer ${sess.accessToken}` },
|
||||
timeout: 60_000, // year-seed ~16-20s, 15s default не хватает.
|
||||
})
|
||||
await ctx.post('/api/admin/seed-demo?years=1', { timeout: 60_000 })
|
||||
await ctx.dispose()
|
||||
|
||||
// 2. Light mode проход — текущий page (default ru-RU + light).
|
||||
for (const p of PAGES) {
|
||||
await attachSession(page, sess, p.path)
|
||||
await page.waitForLoadState('networkidle', { timeout: 15_000 }).catch(() => {})
|
||||
await page.screenshot({ path: `${dir}/${p.name}-light.png`, fullPage: false })
|
||||
}
|
||||
|
||||
// 3. Dark mode проход — новый context с prefers-color-scheme: dark.
|
||||
const darkCtx = await browser.newContext({
|
||||
baseURL: BASE, ignoreHTTPSErrors: true,
|
||||
locale: 'ru-RU', colorScheme: 'dark',
|
||||
viewport: { width: 1280, height: 800 },
|
||||
})
|
||||
const darkPage = await darkCtx.newPage()
|
||||
const errs = watchPage(darkPage)
|
||||
|
||||
for (const p of PAGES) {
|
||||
await attachSession(darkPage, sess, p.path)
|
||||
await darkPage.waitForLoadState('networkidle', { timeout: 15_000 }).catch(() => {})
|
||||
await darkPage.screenshot({ path: `${dir}/${p.name}-dark.png`, fullPage: false })
|
||||
}
|
||||
|
||||
expectNoErrors(errs, 'dark mode pages')
|
||||
await darkCtx.close()
|
||||
|
||||
// Sanity: главная в dark должна иметь нашу dashbg.
|
||||
expect(await page.evaluate(() => document.body.tagName.toLowerCase())).toBe('body')
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue