feat(web): привычные названия денег (ПКО/РКО) + прибыль на главной + статусы с подсказкой
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
П.7: типы денег названы привычно — «Приход наличных (ПКО)», «Расход наличных (РКО)», «Входящий/Исходящий платёж», «Перевод между счетами». П.8: блок «Прибыль за сегодня/месяц» на дашборде (во всех режимах, /api/dashboard/margin), с пометкой «приблизительно; точно после FIFO». П.9: общий DocStatusBadge (Черновик/Проведён/Завершена) с tooltip «что это значит», применён на списках Приёмок/Продаж/Инвентаризаций. П.6: проверено — Оприходование/Списание в простом режиме не прячутся (отдельные пункты). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ff4391d19f
commit
0f1574df64
|
|
@ -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 ✅; деплой+скрины — по ходу.
|
||||
|
||||
## Коммиты
|
||||
(заполняется по ходу)
|
||||
|
|
|
|||
40
src/food-market.web/src/components/DocStatusBadge.tsx
Normal file
40
src/food-market.web/src/components/DocStatusBadge.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* Единый бейдж статуса документа с понятной русской подписью и подсказкой
|
||||
* «что это значит» при наведении (п.9 паритета).
|
||||
*
|
||||
* Статусы:
|
||||
* - draft (Черновик) — документ создан, но ещё НЕ влияет на остатки/деньги; можно свободно править.
|
||||
* - posted (Проведён) — документ проведён: остатки и деньги изменены; для правки сначала отмените проведение.
|
||||
* - completed (Завершена) — для инвентаризации: пересчёт завершён; движения оформляются отдельными документами.
|
||||
*/
|
||||
export type DocStatusKind = 'draft' | 'posted' | 'completed'
|
||||
|
||||
const MAP: Record<DocStatusKind, { label: string; tip: string; cls: string }> = {
|
||||
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 (
|
||||
<span
|
||||
title={s.tip}
|
||||
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium cursor-help ${s.cls}`}
|
||||
>
|
||||
{labelOverride ?? s.label}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
53
src/food-market.web/src/components/ProfitHomeBlock.tsx
Normal file
53
src/food-market.web/src/components/ProfitHomeBlock.tsx
Normal file
|
|
@ -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<MarginSummary>({
|
||||
queryKey: ['/api/dashboard/margin', days],
|
||||
queryFn: async () => (await api.get<MarginSummary>(`/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<typeof useMargin> }) => (
|
||||
<div className="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-5">
|
||||
<div className="flex items-center gap-2 text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400">
|
||||
<TrendingUp className="w-4 h-4 text-emerald-600" /> {label}
|
||||
</div>
|
||||
<div className="mt-1 text-2xl font-semibold text-emerald-700 dark:text-emerald-400">
|
||||
{q.data ? fmt(q.data.margin, { compact: true }) : '—'}
|
||||
</div>
|
||||
{q.data && (
|
||||
<div className="mt-0.5 text-xs text-slate-400">
|
||||
выручка {fmt(q.data.revenue, { compact: true })} · маржа {q.data.marginPercent}%
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<Card label="Прибыль за сегодня" q={today} />
|
||||
<Card label="Прибыль за месяц" q={month} />
|
||||
</div>
|
||||
<p className="mt-1.5 text-[11px] text-slate-400">
|
||||
Прибыль приблизительная (по текущей себестоимости из карточек). Точной станет после перехода на FIFO.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -41,13 +41,15 @@ export const ACCOUNT_KIND_LABEL: Record<number, string> = {
|
|||
[MoneyAccountKind.Acquiring]: 'Эквайринг',
|
||||
}
|
||||
|
||||
// Привычные названия (как у бухгалтеров/МойСклада): ПКО/РКО для наличных,
|
||||
// входящий/исходящий платёж для безнала, перевод между счетами.
|
||||
export const TX_KIND_LABEL: Record<number, string> = {
|
||||
[MoneyTxKind.PaymentIn]: 'Поступление (безнал)',
|
||||
[MoneyTxKind.PaymentOut]: 'Расход (безнал)',
|
||||
[MoneyTxKind.CashIn]: 'Приход наличных',
|
||||
[MoneyTxKind.CashOut]: 'Расход наличных',
|
||||
[MoneyTxKind.TransferOut]: 'Перевод (списание)',
|
||||
[MoneyTxKind.TransferIn]: 'Перевод (зачисление)',
|
||||
[MoneyTxKind.CashIn]: 'Приход наличных (ПКО)',
|
||||
[MoneyTxKind.CashOut]: 'Расход наличных (РКО)',
|
||||
[MoneyTxKind.PaymentIn]: 'Входящий платёж',
|
||||
[MoneyTxKind.PaymentOut]: 'Исходящий платёж',
|
||||
[MoneyTxKind.TransferOut]: 'Перевод между счетами (списание)',
|
||||
[MoneyTxKind.TransferIn]: 'Перевод между счетами (зачисление)',
|
||||
}
|
||||
|
||||
/** Приходный ли тип (увеличивает баланс счёта) — для знака и цвета суммы. */
|
||||
|
|
|
|||
|
|
@ -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). */}
|
||||
<OverdueNegativeStockBlock />
|
||||
|
||||
{/* Прибыль за сегодня/месяц — на главной во всех режимах (п.8 паритета). */}
|
||||
<ProfitHomeBlock />
|
||||
|
||||
{/* KPI блок продажи: today / week / month + prev-month сравнение */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<KpiCard
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { DocStatusBadge } from '@/components/DocStatusBadge'
|
||||
import { useRef } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { ClipboardList, Pencil, Smartphone } from 'lucide-react'
|
||||
|
|
@ -76,9 +77,7 @@ export function InventoriesPage() {
|
|||
{ header: '№', width: '160px', sortKey: 'number', cell: (r) => <span className="font-mono text-slate-600">{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 === InventoryStatus.Completed
|
||||
? <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>
|
||||
<DocStatusBadge status={r.status === InventoryStatus.Completed ? 'completed' : 'draft'} />
|
||||
)},
|
||||
{ header: 'Склад', cell: (r) => r.storeName },
|
||||
{ header: 'Позиций', width: '100px', className: 'text-right', cell: (r) => r.lineCount },
|
||||
|
|
|
|||
|
|
@ -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) => <span className="font-mono text-slate-600">{r.number}</span> },
|
||||
{ header: 'Дата/время', width: '160px', sortKey: 'date', cell: (r) => new Date(r.date).toLocaleString('ru') },
|
||||
{ header: 'Статус', width: '120px', sortKey: 'status', cell: (r) => (
|
||||
r.status === RetailSaleStatus.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>
|
||||
<DocStatusBadge status={r.status === RetailSaleStatus.Posted ? 'posted' : 'draft'} />
|
||||
)},
|
||||
{ header: 'Магазин', sortKey: 'store', cell: (r) => r.storeName },
|
||||
{ header: 'Касса', width: '160px', cell: (r) => r.retailPointName ?? '—' },
|
||||
|
|
|
|||
|
|
@ -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) => <span className="font-mono text-slate-600">{r.number}</span> },
|
||||
{ header: 'Дата', width: '120px', sortKey: 'date', cell: (r) => new Date(r.date).toLocaleDateString('ru') },
|
||||
{ header: 'Статус', width: '130px', sortKey: 'status', cell: (r) => (
|
||||
r.status === SupplyStatus.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>
|
||||
<DocStatusBadge status={r.status === SupplyStatus.Posted ? 'posted' : 'draft'} />
|
||||
)},
|
||||
{ header: 'Поставщик', sortKey: 'supplier', cell: (r) => r.supplierName },
|
||||
{ header: 'Склад', width: '180px', sortKey: 'store', cell: (r) => r.storeName },
|
||||
|
|
|
|||
Loading…
Reference in a new issue