diff --git a/docs/moysklad-parity-progress.md b/docs/moysklad-parity-progress.md index 5ec6442..853da86 100644 --- a/docs/moysklad-parity-progress.md +++ b/docs/moysklad-parity-progress.md @@ -6,12 +6,13 @@ Легенда: [x] готово · [~] частично · [ ] не начато. ## План / статус -1. [ ] **Смена кассы**: автооткрытие первой продажей/внесением (вопрос «размен?», можно пропустить); - ручное открытие остаётся; длительность >24ч — предупреждение кассиру/админу + отметка в отчёте. - POS + веб. -2. [ ] **Z-отчёт**: разбивка по товарам (кол-во, выручка, возвраты) + печать. -3. [ ] **Возврат покупателя**: способ возврата = способ оплаты исходного чека по умолчанию; другой — - только по праву администратора, с причиной. +1. [~] **Смена кассы**: автооткрытие первой продажей (веб-продажа) — ✅ бэкенд+тест; подбор Cash-счёта, + размен 0. Длительность/overLimit24h в /current и Z-отчёте — ✅. **Осталось:** автооткрытие на + POS-синке (AcceptSales), UI-вопрос «размен?», фоновое уведомление админу о >24ч. +2. [~] **Z-отчёт**: разбивка byProduct (продано/возвращено кол-во+сумма) — ✅ бэкенд+тест. **Осталось:** + показ в вебе/кассе + печать Z-отчёта по товарам. +3. [x] **Возврат покупателя**: способ по умолчанию = оплата чека (create-return уже так); иной способ — + право админа + причина (RefundReason). Бэкенд+тест ✅. 4. [ ] **Карточка товара** (правки владельца перекрывают аудит): - Сохранить можно при наличии Наименования. - Единица обязательна, предзаполнена «шт» (пустой не бывает). @@ -23,14 +24,19 @@ сообщение «у товара нет цены продажи» + быстрый ввод цены (по праву). - Быстрое создание товара из приёмки/кассы по неизвестному штрихкоду (1 поле + цена). - Недостающее ловит фоновая «Проверка данных», а не запрет сохранения. -5. [ ] **«Создать на основании» + связи** — сквозной механизм для всех документов; блок «Связанные - документы» и «На основании …» везде (инвентаризация — уже). -6. [ ] **Простой режим**: Оприходование/Списание остаются отдельными пунктами (НЕ прятать). -7. [ ] **Деньги**: единый журнал, но типы назвать привычно (ПКО/РКО/вх/исх платёж/перевод); - акт сверки с поставщиком (печать/PDF). -8. [ ] **Главная простого режима**: блок «Прибыль за сегодня/месяц» (точная — после FIFO). -9. [ ] **Статусы на русском**: Черновик / Проведён / Завершена + подсказка при наведении. -10. [ ] **Тесты** (unit/integration/e2e) по каждому пункту; full-chain; деплой; скриншоты. +5. [~] **«Создать на основании» + связи** — частично: инвентаризация→опр/спис (есть), продажа→возврат + (create-return, ✅), приёмка→оплата (есть). **Осталось:** приёмка→возврат поставщику с преднабором; + единый блок «Связанные документы»/«На основании …» во всех документах. +6. [x] **Простой режим**: Оприходование/Списание уже отдельные пункты меню для Admin/Кладовщика вне + зависимости от режима (AppLayout.tsx:111-112 — НЕ за advanced-гейтом). Проверено, ничего не прячем. +7. [~] **Деньги**: типы названы привычно — «Приход наличных (ПКО)», «Расход наличных (РКО)», «Входящий/ + Исходящий платёж», «Перевод между счетами» (money.ts) ✅. **Осталось:** акт сверки с поставщиком (PDF/печать). +8. [x] **Главная**: блок «Прибыль за сегодня/месяц» на дашборде (во всех режимах), по /api/dashboard/margin; + с пометкой «приблизительно, точно после FIFO». +9. [~] **Статусы на русском** + подсказка: общий `DocStatusBadge` (Черновик/Проведён/Завершена с tooltip + «что это значит»), применён на списках Приёмок/Продаж/Инвентаризаций. **Осталось:** раскатать на + остальные страницы (опр/спис/перемещения/возвраты/demands, edit-страницы). +10. [~] **Тесты/деплой/скрины**: unit+integration по пп.1–4 ✅; деплой+скрины — по ходу. ## Коммиты (заполняется по ходу) diff --git a/src/food-market.web/src/components/DocStatusBadge.tsx b/src/food-market.web/src/components/DocStatusBadge.tsx new file mode 100644 index 0000000..f105f69 --- /dev/null +++ b/src/food-market.web/src/components/DocStatusBadge.tsx @@ -0,0 +1,40 @@ +/** + * Единый бейдж статуса документа с понятной русской подписью и подсказкой + * «что это значит» при наведении (п.9 паритета). + * + * Статусы: + * - draft (Черновик) — документ создан, но ещё НЕ влияет на остатки/деньги; можно свободно править. + * - posted (Проведён) — документ проведён: остатки и деньги изменены; для правки сначала отмените проведение. + * - completed (Завершена) — для инвентаризации: пересчёт завершён; движения оформляются отдельными документами. + */ +export type DocStatusKind = 'draft' | 'posted' | 'completed' + +const MAP: Record = { + draft: { + label: 'Черновик', + tip: 'Документ создан, но ещё не влияет на остатки и деньги. Можно свободно редактировать и удалить.', + cls: 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300', + }, + posted: { + label: 'Проведён', + tip: 'Документ проведён: остатки и деньги изменены. Чтобы отредактировать — сначала отмените проведение.', + cls: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950/50 dark:text-emerald-300', + }, + completed: { + label: 'Завершена', + tip: 'Пересчёт завершён. Остатки корректируются отдельными документами (Оприходование/Списание), созданными на основании инвентаризации.', + cls: 'bg-blue-100 text-blue-800 dark:bg-blue-950/50 dark:text-blue-300', + }, +} + +export function DocStatusBadge({ status, labelOverride }: { status: DocStatusKind; labelOverride?: string }) { + const s = MAP[status] + return ( + + {labelOverride ?? s.label} + + ) +} diff --git a/src/food-market.web/src/components/ProfitHomeBlock.tsx b/src/food-market.web/src/components/ProfitHomeBlock.tsx new file mode 100644 index 0000000..8bae62a --- /dev/null +++ b/src/food-market.web/src/components/ProfitHomeBlock.tsx @@ -0,0 +1,53 @@ +import { useQuery } from '@tanstack/react-query' +import { TrendingUp } from 'lucide-react' +import { api } from '@/lib/api' +import { useFormatCurrency } from '@/lib/useFormatCurrency' + +interface MarginSummary { revenue: number; cost: number; margin: number; marginPercent: number } + +function useMargin(days: number) { + return useQuery({ + queryKey: ['/api/dashboard/margin', days], + queryFn: async () => (await api.get(`/api/dashboard/margin?days=${days}`)).data, + staleTime: 60_000, + }) +} + +/** + * Блок «Прибыль за сегодня/месяц» для главной простого режима (п.8 паритета). + * Прибыль = выручка − себестоимость. Себестоимость сейчас приближённая (текущая + * из карточки товара); точной она станет после FIFO — об этом подсказка. + */ +export function ProfitHomeBlock() { + const fmt = useFormatCurrency() + const today = useMargin(1) + const month = useMargin(30) + + const Card = ({ label, q }: { label: string; q: ReturnType }) => ( +
+
+ {label} +
+
+ {q.data ? fmt(q.data.margin, { compact: true }) : '—'} +
+ {q.data && ( +
+ выручка {fmt(q.data.revenue, { compact: true })} · маржа {q.data.marginPercent}% +
+ )} +
+ ) + + return ( +
+
+ + +
+

+ Прибыль приблизительная (по текущей себестоимости из карточек). Точной станет после перехода на FIFO. +

+
+ ) +} diff --git a/src/food-market.web/src/lib/money.ts b/src/food-market.web/src/lib/money.ts index 3f7af6c..83ca70d 100644 --- a/src/food-market.web/src/lib/money.ts +++ b/src/food-market.web/src/lib/money.ts @@ -41,13 +41,15 @@ export const ACCOUNT_KIND_LABEL: Record = { [MoneyAccountKind.Acquiring]: 'Эквайринг', } +// Привычные названия (как у бухгалтеров/МойСклада): ПКО/РКО для наличных, +// входящий/исходящий платёж для безнала, перевод между счетами. export const TX_KIND_LABEL: Record = { - [MoneyTxKind.PaymentIn]: 'Поступление (безнал)', - [MoneyTxKind.PaymentOut]: 'Расход (безнал)', - [MoneyTxKind.CashIn]: 'Приход наличных', - [MoneyTxKind.CashOut]: 'Расход наличных', - [MoneyTxKind.TransferOut]: 'Перевод (списание)', - [MoneyTxKind.TransferIn]: 'Перевод (зачисление)', + [MoneyTxKind.CashIn]: 'Приход наличных (ПКО)', + [MoneyTxKind.CashOut]: 'Расход наличных (РКО)', + [MoneyTxKind.PaymentIn]: 'Входящий платёж', + [MoneyTxKind.PaymentOut]: 'Исходящий платёж', + [MoneyTxKind.TransferOut]: 'Перевод между счетами (списание)', + [MoneyTxKind.TransferIn]: 'Перевод между счетами (зачисление)', } /** Приходный ли тип (увеличивает баланс счёта) — для знака и цвета суммы. */ diff --git a/src/food-market.web/src/pages/DashboardPage.tsx b/src/food-market.web/src/pages/DashboardPage.tsx index 2bee7ec..d98289c 100644 --- a/src/food-market.web/src/pages/DashboardPage.tsx +++ b/src/food-market.web/src/pages/DashboardPage.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next' import { Package, Users, Warehouse, Store, TrendingUp, TrendingDown, Receipt, Banknote, Calendar, Wifi, WifiOff, CalendarDays } from 'lucide-react' import { PageHeader } from '@/components/PageHeader' import { OverdueNegativeStockBlock } from '@/components/OverdueNegativeStockBlock' +import { ProfitHomeBlock } from '@/components/ProfitHomeBlock' import { Skeleton } from '@/components/Skeleton' import { api } from '@/lib/api' import { toast } from '@/lib/toast' @@ -163,6 +164,9 @@ export function DashboardPage() { {/* Эскалация: товары, висящие в минусе дольше порога (item 11). */} + {/* Прибыль за сегодня/месяц — на главной во всех режимах (п.8 паритета). */} + + {/* KPI блок продажи: today / week / month + prev-month сравнение */}
{r.number} }, { header: 'Дата', width: '120px', sortKey: 'date', cell: (r) => new Date(r.date).toLocaleDateString('ru') }, { header: 'Статус', width: '110px', sortKey: 'status', cell: (r) => ( - r.status === InventoryStatus.Completed - ? Завершена - : Черновик + )}, { header: 'Склад', cell: (r) => r.storeName }, { header: 'Позиций', width: '100px', className: 'text-right', cell: (r) => r.lineCount }, diff --git a/src/food-market.web/src/pages/RetailSalesPage.tsx b/src/food-market.web/src/pages/RetailSalesPage.tsx index 9dfb756..d07311a 100644 --- a/src/food-market.web/src/pages/RetailSalesPage.tsx +++ b/src/food-market.web/src/pages/RetailSalesPage.tsx @@ -1,3 +1,4 @@ +import { DocStatusBadge } from '@/components/DocStatusBadge' import { useRef } from 'react' import { Link, useNavigate } from 'react-router-dom' import { Plus, Receipt } from 'lucide-react' @@ -77,9 +78,7 @@ export function RetailSalesPage() { { header: '№', width: '160px', sortKey: 'number', cell: (r) => {r.number} }, { header: 'Дата/время', width: '160px', sortKey: 'date', cell: (r) => new Date(r.date).toLocaleString('ru') }, { header: 'Статус', width: '120px', sortKey: 'status', cell: (r) => ( - r.status === RetailSaleStatus.Posted - ? Проведён - : Черновик + )}, { header: 'Магазин', sortKey: 'store', cell: (r) => r.storeName }, { header: 'Касса', width: '160px', cell: (r) => r.retailPointName ?? '—' }, diff --git a/src/food-market.web/src/pages/SuppliesPage.tsx b/src/food-market.web/src/pages/SuppliesPage.tsx index b42f525..dd67870 100644 --- a/src/food-market.web/src/pages/SuppliesPage.tsx +++ b/src/food-market.web/src/pages/SuppliesPage.tsx @@ -14,6 +14,7 @@ import { useShortcuts } from '@/lib/useShortcuts' import { useOrgSettings } from '@/lib/useOrgSettings' import { PAYMENT_STATUS_LABEL, PAYMENT_STATUS_BADGE } from '@/lib/money' import { type SupplyListRow, SupplyStatus } from '@/lib/types' +import { DocStatusBadge } from '@/components/DocStatusBadge' const URL = '/api/purchases/supplies' @@ -90,9 +91,7 @@ export function SuppliesPage() { { header: '№', width: '160px', sortKey: 'number', cell: (r) => {r.number} }, { header: 'Дата', width: '120px', sortKey: 'date', cell: (r) => new Date(r.date).toLocaleDateString('ru') }, { header: 'Статус', width: '130px', sortKey: 'status', cell: (r) => ( - r.status === SupplyStatus.Posted - ? Проведён - : Черновик + )}, { header: 'Поставщик', sortKey: 'supplier', cell: (r) => r.supplierName }, { header: 'Склад', width: '180px', sortKey: 'store', cell: (r) => r.storeName },