diff --git a/src/food-market.web/src/components/EmptyState.tsx b/src/food-market.web/src/components/EmptyState.tsx new file mode 100644 index 0000000..a95e3d9 --- /dev/null +++ b/src/food-market.web/src/components/EmptyState.tsx @@ -0,0 +1,56 @@ +import type { ReactNode } from 'react' +import type { LucideIcon } from 'lucide-react' +import { Button } from './Button' + +/** + * Centered empty-state block для list-страниц, когда данных пока нет. + * Заменяет «Нет данных» в DataTable на дружественный CTA: иконка + + * заголовок + пояснение + кнопка «Создать первый …». + * + * Использование на list-pages: проверить (items.length===0 && !isLoading) + * и подменить таблицу на . Можно использовать как `empty` + * prop у DataTable — он отрендерит внутри пустой строки. + */ +interface EmptyStateProps { + icon: LucideIcon + title: string + description?: ReactNode + /** CTA-кнопка справа от описания: для создания первой записи. */ + actionLabel?: string + onAction?: () => void + /** Если задано, рисуется secondary-link под основной кнопкой. */ + secondaryLabel?: string + onSecondary?: () => void +} + +export function EmptyState({ + icon: Icon, title, description, actionLabel, onAction, secondaryLabel, onSecondary, +}: EmptyStateProps) { + return ( +
+
+ +
+

{title}

+ {description && ( +

{description}

+ )} + {(actionLabel || secondaryLabel) && ( +
+ {actionLabel && onAction && ( + + )} + {secondaryLabel && onSecondary && ( + + )} +
+ )} +
+ ) +} diff --git a/src/food-market.web/src/pages/AbcReportPage.tsx b/src/food-market.web/src/pages/AbcReportPage.tsx index 4d9b0b1..fb05d37 100644 --- a/src/food-market.web/src/pages/AbcReportPage.tsx +++ b/src/food-market.web/src/pages/AbcReportPage.tsx @@ -1,8 +1,9 @@ import { useState } from 'react' import { useQuery } from '@tanstack/react-query' -import { Download } from 'lucide-react' +import { Download, BarChart3 } from 'lucide-react' import { api } from '@/lib/api' import { Button } from '@/components/Button' +import { EmptyState } from '@/components/EmptyState' import { Field, Select } from '@/components/Field' import { DateField } from '@/components/DateField' import { useStores, useProductGroups } from '@/lib/useLookups' @@ -141,7 +142,11 @@ export function AbcReportPage() {
{rep.isLoading &&
Загружаю…
} {!rep.isLoading && (rep.data?.length ?? 0) === 0 && ( -
За период нет продаж.
+ )} {!rep.isLoading && rep.data && rep.data.length > 0 && (
diff --git a/src/food-market.web/src/pages/CounterpartiesPage.tsx b/src/food-market.web/src/pages/CounterpartiesPage.tsx index 8720c0f..a28ea1e 100644 --- a/src/food-market.web/src/pages/CounterpartiesPage.tsx +++ b/src/food-market.web/src/pages/CounterpartiesPage.tsx @@ -1,10 +1,11 @@ import { useState } from 'react' import { useQuery } from '@tanstack/react-query' import { validateEmail, validatePhone } from '@/lib/validation' -import { Plus, Trash2 } from 'lucide-react' +import { Plus, Trash2, Users } from 'lucide-react' import { api } from '@/lib/api' import { ListPageShell } from '@/components/ListPageShell' import { DataTable } from '@/components/DataTable' +import { EmptyState } from '@/components/EmptyState' import { Pagination } from '@/components/Pagination' import { SearchBar } from '@/components/SearchBar' import { Button } from '@/components/Button' @@ -87,28 +88,38 @@ export function CounterpartiesPage() { )} > - r.id} - sortKey={sortKey} - sortOrder={sortOrder} - onSortChange={setSort} - onRowClick={(r) => { setFieldErrors({}); setForm({ - id: r.id, name: r.name, legalName: r.legalName ?? '', type: r.type, - bin: r.bin ?? '', iin: r.iin ?? '', taxNumber: r.taxNumber ?? '', - countryId: r.countryId ?? '', address: r.address ?? '', phone: r.phone ?? '', email: r.email ?? '', - bankName: r.bankName ?? '', bankAccount: r.bankAccount ?? '', bik: r.bik ?? '', - contactPerson: r.contactPerson ?? '', notes: r.notes ?? '', - }) }} - columns={[ - { header: 'Название', sortKey: 'name', cell: (r) => r.name }, - { header: 'Тип', width: '120px', sortKey: 'type', cell: (r) => typeLabel[r.type] }, - { header: 'БИН/ИИН', width: '140px', cell: (r) => {r.bin ?? r.iin ?? '—'} }, - { header: 'Телефон', width: '160px', sortKey: 'phone', cell: (r) => r.phone ?? '—' }, - { header: 'Страна', width: '120px', sortKey: 'country', cell: (r) => r.countryName ?? '—' }, - ]} - /> + {!isLoading && (data?.items?.length ?? 0) === 0 && !search ? ( + { setForm(blankForm); setFieldErrors({}) }} + /> + ) : ( + r.id} + sortKey={sortKey} + sortOrder={sortOrder} + onSortChange={setSort} + onRowClick={(r) => { setFieldErrors({}); setForm({ + id: r.id, name: r.name, legalName: r.legalName ?? '', type: r.type, + bin: r.bin ?? '', iin: r.iin ?? '', taxNumber: r.taxNumber ?? '', + countryId: r.countryId ?? '', address: r.address ?? '', phone: r.phone ?? '', email: r.email ?? '', + bankName: r.bankName ?? '', bankAccount: r.bankAccount ?? '', bik: r.bik ?? '', + contactPerson: r.contactPerson ?? '', notes: r.notes ?? '', + }) }} + columns={[ + { header: 'Название', sortKey: 'name', cell: (r) => r.name }, + { header: 'Тип', width: '120px', sortKey: 'type', cell: (r) => typeLabel[r.type] }, + { header: 'БИН/ИИН', width: '140px', cell: (r) => {r.bin ?? r.iin ?? '—'} }, + { header: 'Телефон', width: '160px', sortKey: 'phone', cell: (r) => r.phone ?? '—' }, + { header: 'Страна', width: '120px', sortKey: 'country', cell: (r) => r.countryName ?? '—' }, + ]} + /> + )} )} > - r.id} - sortKey={sortKey} - sortOrder={sortOrder} - onSortChange={setSort} - onRowClick={(r) => navigate(`/sales/demands/${r.id}`)} - columns={[ - { header: '№', width: '160px', sortKey: 'number', cell: (r) => {r.number} }, - { header: 'Дата', width: '120px', sortKey: 'date', cell: (r) => new Date(r.date).toLocaleDateString('ru') }, - { header: 'Статус', width: '110px', sortKey: 'status', cell: (r) => ( - r.status === DemandStatus.Posted - ? Проведён - : Черновик - )}, - { header: 'Контрагент', sortKey: 'customer', cell: (r) => r.customerName }, - { header: 'Склад', width: '160px', cell: (r) => r.storeName }, - { header: 'Оплата', width: '110px', cell: (r) => demandPaymentLabel[r.payment] ?? r.payment }, - { header: 'Позиций', width: '90px', className: 'text-right', cell: (r) => r.lineCount }, - { header: 'Сумма', width: '140px', className: 'text-right font-mono', sortKey: 'total', cell: (r) => `${r.total.toLocaleString('ru', moneyFmt)} ${r.currencyCode}` }, - { header: 'Оплачено', width: '140px', className: 'text-right font-mono text-slate-500', cell: (r) => r.paidAmount.toLocaleString('ru', moneyFmt) }, - ]} - empty="Отгрузок пока нет. Создай первую — товар спишется со склада после проведения." - /> + {!isLoading && (data?.items?.length ?? 0) === 0 && !search ? ( + navigate('/sales/demands/new')} + /> + ) : ( + r.id} + sortKey={sortKey} + sortOrder={sortOrder} + onSortChange={setSort} + onRowClick={(r) => navigate(`/sales/demands/${r.id}`)} + columns={[ + { header: '№', width: '160px', sortKey: 'number', cell: (r) => {r.number} }, + { header: 'Дата', width: '120px', sortKey: 'date', cell: (r) => new Date(r.date).toLocaleDateString('ru') }, + { header: 'Статус', width: '110px', sortKey: 'status', cell: (r) => ( + r.status === DemandStatus.Posted + ? Проведён + : Черновик + )}, + { header: 'Контрагент', sortKey: 'customer', cell: (r) => r.customerName }, + { header: 'Склад', width: '160px', cell: (r) => r.storeName }, + { header: 'Оплата', width: '110px', cell: (r) => demandPaymentLabel[r.payment] ?? r.payment }, + { header: 'Позиций', width: '90px', className: 'text-right', cell: (r) => r.lineCount }, + { header: 'Сумма', width: '140px', className: 'text-right font-mono', sortKey: 'total', cell: (r) => `${r.total.toLocaleString('ru', moneyFmt)} ${r.currencyCode}` }, + { header: 'Оплачено', width: '140px', className: 'text-right font-mono text-slate-500', cell: (r) => r.paidAmount.toLocaleString('ru', moneyFmt) }, + ]} + empty="Отгрузок пока нет. Создай первую — товар спишется со склада после проведения." + /> + )} ) } diff --git a/src/food-market.web/src/pages/EntersPage.tsx b/src/food-market.web/src/pages/EntersPage.tsx index 1a92068..709545d 100644 --- a/src/food-market.web/src/pages/EntersPage.tsx +++ b/src/food-market.web/src/pages/EntersPage.tsx @@ -1,7 +1,8 @@ import { Link, useNavigate } from 'react-router-dom' -import { Plus } from 'lucide-react' +import { Plus, PackagePlus } from 'lucide-react' import { ListPageShell } from '@/components/ListPageShell' import { DataTable } from '@/components/DataTable' +import { EmptyState } from '@/components/EmptyState' import { Pagination } from '@/components/Pagination' import { SearchBar } from '@/components/SearchBar' import { Button } from '@/components/Button' @@ -36,28 +37,38 @@ export function EntersPage() { )} > - r.id} - sortKey={sortKey} - sortOrder={sortOrder} - onSortChange={setSort} - onRowClick={(r) => navigate(`/inventory/enters/${r.id}`)} - columns={[ - { 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 === EnterStatus.Posted - ? Проведён - : Черновик - )}, - { header: 'Склад', sortKey: 'store', cell: (r) => r.storeName }, - { header: 'Позиций', width: '100px', className: 'text-right', cell: (r) => r.lineCount }, - { header: 'Сумма', width: '160px', className: 'text-right font-mono', sortKey: 'total', cell: (r) => `${r.total.toLocaleString('ru', moneyFmt)} ${r.currencyCode}` }, - ]} - empty="Оприходований пока нет. Создай первое — товар попадёт на склад после проведения." - /> + {!isLoading && (data?.items?.length ?? 0) === 0 && !search ? ( + navigate('/inventory/enters/new')} + /> + ) : ( + r.id} + sortKey={sortKey} + sortOrder={sortOrder} + onSortChange={setSort} + onRowClick={(r) => navigate(`/inventory/enters/${r.id}`)} + columns={[ + { 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 === EnterStatus.Posted + ? Проведён + : Черновик + )}, + { header: 'Склад', sortKey: 'store', cell: (r) => r.storeName }, + { header: 'Позиций', width: '100px', className: 'text-right', cell: (r) => r.lineCount }, + { header: 'Сумма', width: '160px', className: 'text-right font-mono', sortKey: 'total', cell: (r) => `${r.total.toLocaleString('ru', moneyFmt)} ${r.currencyCode}` }, + ]} + empty="Оприходований пока нет. Создай первое — товар попадёт на склад после проведения." + /> + )} ) } diff --git a/src/food-market.web/src/pages/InventoriesPage.tsx b/src/food-market.web/src/pages/InventoriesPage.tsx index 830ef0d..45426a3 100644 --- a/src/food-market.web/src/pages/InventoriesPage.tsx +++ b/src/food-market.web/src/pages/InventoriesPage.tsx @@ -1,7 +1,8 @@ import { Link, useNavigate } from 'react-router-dom' -import { Plus } from 'lucide-react' +import { Plus, ClipboardList } from 'lucide-react' import { ListPageShell } from '@/components/ListPageShell' import { DataTable } from '@/components/DataTable' +import { EmptyState } from '@/components/EmptyState' import { Pagination } from '@/components/Pagination' import { SearchBar } from '@/components/SearchBar' import { Button } from '@/components/Button' @@ -36,29 +37,39 @@ export function InventoriesPage() { )} > - r.id} - sortKey={sortKey} - sortOrder={sortOrder} - onSortChange={setSort} - onRowClick={(r) => navigate(`/inventory/inventories/${r.id}`)} - columns={[ - { header: '№', width: '160px', sortKey: 'number', cell: (r) => {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.Posted - ? Проведён - : Черновик - )}, - { header: 'Склад', cell: (r) => r.storeName }, - { header: 'Позиций', width: '100px', className: 'text-right', cell: (r) => r.lineCount }, - { header: 'Излишек', width: '140px', className: 'text-right font-mono text-green-700', cell: (r) => `+${r.surplusValue.toLocaleString('ru', moneyFmt)}` }, - { header: 'Недостача', width: '140px', className: 'text-right font-mono text-red-700', cell: (r) => `${r.shortageValue.toLocaleString('ru', moneyFmt)}` }, - ]} - empty="Инвентаризаций пока нет." - /> + {!isLoading && (data?.items?.length ?? 0) === 0 && !search ? ( + navigate('/inventory/inventories/new')} + /> + ) : ( + r.id} + sortKey={sortKey} + sortOrder={sortOrder} + onSortChange={setSort} + onRowClick={(r) => navigate(`/inventory/inventories/${r.id}`)} + columns={[ + { header: '№', width: '160px', sortKey: 'number', cell: (r) => {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.Posted + ? Проведён + : Черновик + )}, + { header: 'Склад', cell: (r) => r.storeName }, + { header: 'Позиций', width: '100px', className: 'text-right', cell: (r) => r.lineCount }, + { header: 'Излишек', width: '140px', className: 'text-right font-mono text-green-700', cell: (r) => `+${r.surplusValue.toLocaleString('ru', moneyFmt)}` }, + { header: 'Недостача', width: '140px', className: 'text-right font-mono text-red-700', cell: (r) => `${r.shortageValue.toLocaleString('ru', moneyFmt)}` }, + ]} + empty="Инвентаризаций пока нет." + /> + )} ) } diff --git a/src/food-market.web/src/pages/LossesPage.tsx b/src/food-market.web/src/pages/LossesPage.tsx index cb66967..e0cec03 100644 --- a/src/food-market.web/src/pages/LossesPage.tsx +++ b/src/food-market.web/src/pages/LossesPage.tsx @@ -1,7 +1,8 @@ import { Link, useNavigate } from 'react-router-dom' -import { Plus } from 'lucide-react' +import { Plus, Trash2 } from 'lucide-react' import { ListPageShell } from '@/components/ListPageShell' import { DataTable } from '@/components/DataTable' +import { EmptyState } from '@/components/EmptyState' import { Pagination } from '@/components/Pagination' import { SearchBar } from '@/components/SearchBar' import { Button } from '@/components/Button' @@ -36,29 +37,39 @@ export function LossesPage() { )} > - r.id} - sortKey={sortKey} - sortOrder={sortOrder} - onSortChange={setSort} - onRowClick={(r) => navigate(`/inventory/losses/${r.id}`)} - columns={[ - { header: '№', width: '160px', sortKey: 'number', cell: (r) => {r.number} }, - { header: 'Дата', width: '120px', sortKey: 'date', cell: (r) => new Date(r.date).toLocaleDateString('ru') }, - { header: 'Статус', width: '110px', sortKey: 'status', cell: (r) => ( - r.status === LossStatus.Posted - ? Проведён - : Черновик - )}, - { header: 'Причина', width: '140px', sortKey: 'reason', cell: (r) => lossReasonLabel[r.reason] ?? r.reason }, - { header: 'Склад', sortKey: 'store', cell: (r) => r.storeName }, - { header: 'Позиций', width: '90px', className: 'text-right', cell: (r) => r.lineCount }, - { header: 'Сумма', width: '160px', className: 'text-right font-mono', sortKey: 'total', cell: (r) => `${r.total.toLocaleString('ru', moneyFmt)} ${r.currencyCode}` }, - ]} - empty="Списаний пока нет." - /> + {!isLoading && (data?.items?.length ?? 0) === 0 && !search ? ( + navigate('/inventory/losses/new')} + /> + ) : ( + r.id} + sortKey={sortKey} + sortOrder={sortOrder} + onSortChange={setSort} + onRowClick={(r) => navigate(`/inventory/losses/${r.id}`)} + columns={[ + { header: '№', width: '160px', sortKey: 'number', cell: (r) => {r.number} }, + { header: 'Дата', width: '120px', sortKey: 'date', cell: (r) => new Date(r.date).toLocaleDateString('ru') }, + { header: 'Статус', width: '110px', sortKey: 'status', cell: (r) => ( + r.status === LossStatus.Posted + ? Проведён + : Черновик + )}, + { header: 'Причина', width: '140px', sortKey: 'reason', cell: (r) => lossReasonLabel[r.reason] ?? r.reason }, + { header: 'Склад', sortKey: 'store', cell: (r) => r.storeName }, + { header: 'Позиций', width: '90px', className: 'text-right', cell: (r) => r.lineCount }, + { header: 'Сумма', width: '160px', className: 'text-right font-mono', sortKey: 'total', cell: (r) => `${r.total.toLocaleString('ru', moneyFmt)} ${r.currencyCode}` }, + ]} + empty="Списаний пока нет." + /> + )} ) } diff --git a/src/food-market.web/src/pages/ProductsPage.tsx b/src/food-market.web/src/pages/ProductsPage.tsx index 72d8814..db49f5b 100644 --- a/src/food-market.web/src/pages/ProductsPage.tsx +++ b/src/food-market.web/src/pages/ProductsPage.tsx @@ -1,10 +1,11 @@ import { useState } from 'react' import { Link, useNavigate } from 'react-router-dom' import { DataTable } from '@/components/DataTable' +import { EmptyState } from '@/components/EmptyState' import { Pagination } from '@/components/Pagination' import { SearchBar } from '@/components/SearchBar' import { Button } from '@/components/Button' -import { Plus, Filter, X, FolderTree } from 'lucide-react' +import { Plus, Filter, X, FolderTree, Package } from 'lucide-react' import { useCatalogList } from '@/lib/useCatalog' import { useOrgSettings } from '@/lib/useOrgSettings' import { usePriceTypes } from '@/lib/useLookups' @@ -282,17 +283,27 @@ export function ProductsPage() { {/* Table */}
- r.id} - sortKey={sortKey} - sortOrder={sortOrder} - onSortChange={setSort} - onRowClick={(r) => navigate(`/catalog/products/${r.id}`)} - columns={baseColumns} - empty="Товаров ещё нет. Они появятся после приёмки или через API." - /> + {!isLoading && (data?.items?.length ?? 0) === 0 && !search && activeCount === 0 ? ( + navigate('/catalog/products/new')} + /> + ) : ( + r.id} + sortKey={sortKey} + sortOrder={sortOrder} + onSortChange={setSort} + onRowClick={(r) => navigate(`/catalog/products/${r.id}`)} + columns={baseColumns} + empty="Товаров ещё нет. Они появятся после приёмки или через API." + /> + )}
{data && data.total > 0 && ( diff --git a/src/food-market.web/src/pages/ProfitReportPage.tsx b/src/food-market.web/src/pages/ProfitReportPage.tsx index e1c1d08..b06809a 100644 --- a/src/food-market.web/src/pages/ProfitReportPage.tsx +++ b/src/food-market.web/src/pages/ProfitReportPage.tsx @@ -1,8 +1,9 @@ import { useState } from 'react' import { useQuery } from '@tanstack/react-query' -import { Download } from 'lucide-react' +import { Download, TrendingUp } from 'lucide-react' import { api } from '@/lib/api' import { Button } from '@/components/Button' +import { EmptyState } from '@/components/EmptyState' import { Field, Select } from '@/components/Field' import { DateField } from '@/components/DateField' import { useStores, useProductGroups } from '@/lib/useLookups' @@ -139,7 +140,11 @@ export function ProfitReportPage() {
{rep.isLoading &&
Загружаю…
} {!rep.isLoading && (rep.data?.length ?? 0) === 0 && ( -
За период нет данных.
+ )} {!rep.isLoading && rep.data && rep.data.length > 0 && (
diff --git a/src/food-market.web/src/pages/RetailSalesPage.tsx b/src/food-market.web/src/pages/RetailSalesPage.tsx index b960845..31f0c72 100644 --- a/src/food-market.web/src/pages/RetailSalesPage.tsx +++ b/src/food-market.web/src/pages/RetailSalesPage.tsx @@ -1,7 +1,8 @@ import { Link, useNavigate } from 'react-router-dom' -import { Plus } from 'lucide-react' +import { Plus, Receipt } from 'lucide-react' import { ListPageShell } from '@/components/ListPageShell' import { DataTable } from '@/components/DataTable' +import { EmptyState } from '@/components/EmptyState' import { Pagination } from '@/components/Pagination' import { SearchBar } from '@/components/SearchBar' import { Button } from '@/components/Button' @@ -44,31 +45,41 @@ export function RetailSalesPage() { )} > - r.id} - sortKey={sortKey} - sortOrder={sortOrder} - onSortChange={setSort} - onRowClick={(r) => navigate(`/sales/retail/${r.id}`)} - columns={[ - { 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 ?? '—' }, - { header: 'Покупатель', width: '180px', cell: (r) => r.customerName ?? аноним }, - { header: 'Оплата', width: '120px', cell: (r) => paymentLabel[r.payment] ?? '?' }, - { header: 'Позиций', width: '90px', className: 'text-right', cell: (r) => r.lineCount }, - { header: 'Сумма', width: '160px', className: 'text-right font-mono font-semibold', sortKey: 'total', cell: (r) => `${r.total.toLocaleString('ru', moneyFmt)} ${r.currencyCode}` }, - ]} - empty="Чеков пока нет. На Phase 5 здесь начнут падать продажи с касс. Сейчас можно создать вручную для теста." - /> + {!isLoading && (data?.items?.length ?? 0) === 0 && !search ? ( + navigate('/sales/retail/new')} + /> + ) : ( + r.id} + sortKey={sortKey} + sortOrder={sortOrder} + onSortChange={setSort} + onRowClick={(r) => navigate(`/sales/retail/${r.id}`)} + columns={[ + { 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 ?? '—' }, + { header: 'Покупатель', width: '180px', cell: (r) => r.customerName ?? аноним }, + { header: 'Оплата', width: '120px', cell: (r) => paymentLabel[r.payment] ?? '?' }, + { header: 'Позиций', width: '90px', className: 'text-right', cell: (r) => r.lineCount }, + { header: 'Сумма', width: '160px', className: 'text-right font-mono font-semibold', sortKey: 'total', cell: (r) => `${r.total.toLocaleString('ru', moneyFmt)} ${r.currencyCode}` }, + ]} + empty="Чеков пока нет. На Phase 5 здесь начнут падать продажи с касс. Сейчас можно создать вручную для теста." + /> + )} ) } diff --git a/src/food-market.web/src/pages/SalesReportPage.tsx b/src/food-market.web/src/pages/SalesReportPage.tsx index e64d37c..c871051 100644 --- a/src/food-market.web/src/pages/SalesReportPage.tsx +++ b/src/food-market.web/src/pages/SalesReportPage.tsx @@ -1,8 +1,9 @@ import { useState } from 'react' import { useQuery } from '@tanstack/react-query' -import { Download } from 'lucide-react' +import { Download, BarChart3 } from 'lucide-react' import { api } from '@/lib/api' import { Button } from '@/components/Button' +import { EmptyState } from '@/components/EmptyState' import { Field, Select } from '@/components/Field' import { DateField } from '@/components/DateField' import { useStores, useProductGroups } from '@/lib/useLookups' @@ -137,7 +138,11 @@ export function SalesReportPage() {
{rep.isLoading &&
Загружаю…
} {!rep.isLoading && (rep.data?.length ?? 0) === 0 && ( -
За период нет данных.
+ )} {!rep.isLoading && rep.data && rep.data.length > 0 && (
diff --git a/src/food-market.web/src/pages/StockReportPage.tsx b/src/food-market.web/src/pages/StockReportPage.tsx index 0142426..0ef6be4 100644 --- a/src/food-market.web/src/pages/StockReportPage.tsx +++ b/src/food-market.web/src/pages/StockReportPage.tsx @@ -1,8 +1,9 @@ import { useState } from 'react' import { useQuery } from '@tanstack/react-query' -import { Download } from 'lucide-react' +import { Download, Warehouse } from 'lucide-react' import { api } from '@/lib/api' import { Button } from '@/components/Button' +import { EmptyState } from '@/components/EmptyState' import { Field, Select, Checkbox } from '@/components/Field' import { DateField } from '@/components/DateField' import { useStores, useProductGroups } from '@/lib/useLookups' @@ -114,7 +115,11 @@ export function StockReportPage() {
{rep.isLoading &&
Загружаю…
} {!rep.isLoading && (rep.data?.length ?? 0) === 0 && ( -
На эту дату нет остатков.
+ )} {!rep.isLoading && rep.data && rep.data.length > 0 && (
diff --git a/src/food-market.web/src/pages/SupplierReturnsPage.tsx b/src/food-market.web/src/pages/SupplierReturnsPage.tsx index 8fe77a4..7f82b31 100644 --- a/src/food-market.web/src/pages/SupplierReturnsPage.tsx +++ b/src/food-market.web/src/pages/SupplierReturnsPage.tsx @@ -1,7 +1,8 @@ import { Link, useNavigate } from 'react-router-dom' -import { Plus } from 'lucide-react' +import { Plus, Undo2 } from 'lucide-react' import { ListPageShell } from '@/components/ListPageShell' import { DataTable } from '@/components/DataTable' +import { EmptyState } from '@/components/EmptyState' import { Pagination } from '@/components/Pagination' import { SearchBar } from '@/components/SearchBar' import { Button } from '@/components/Button' @@ -36,30 +37,40 @@ export function SupplierReturnsPage() { )} > - r.id} - sortKey={sortKey} - sortOrder={sortOrder} - onSortChange={setSort} - onRowClick={(r) => navigate(`/purchases/supplier-returns/${r.id}`)} - columns={[ - { header: '№', width: '160px', sortKey: 'number', cell: (r) => {r.number} }, - { header: 'Дата', width: '120px', sortKey: 'date', cell: (r) => new Date(r.date).toLocaleDateString('ru') }, - { header: 'Статус', width: '110px', sortKey: 'status', cell: (r) => ( - r.status === SupplierReturnStatus.Posted - ? Проведён - : Черновик - )}, - { header: 'Поставщик', sortKey: 'supplier', cell: (r) => r.supplierName }, - { header: 'По приёмке', width: '140px', cell: (r) => r.referenceSupplyNumber ? {r.referenceSupplyNumber} : '—' }, - { header: 'Склад', width: '180px', cell: (r) => r.storeName }, - { header: 'Позиций', width: '90px', className: 'text-right', cell: (r) => r.lineCount }, - { header: 'Сумма', width: '160px', className: 'text-right font-mono', sortKey: 'total', cell: (r) => `${r.total.toLocaleString('ru', moneyFmt)} ${r.currencyCode}` }, - ]} - empty="Возвратов поставщикам нет." - /> + {!isLoading && (data?.items?.length ?? 0) === 0 && !search ? ( + navigate('/purchases/supplier-returns/new')} + /> + ) : ( + r.id} + sortKey={sortKey} + sortOrder={sortOrder} + onSortChange={setSort} + onRowClick={(r) => navigate(`/purchases/supplier-returns/${r.id}`)} + columns={[ + { header: '№', width: '160px', sortKey: 'number', cell: (r) => {r.number} }, + { header: 'Дата', width: '120px', sortKey: 'date', cell: (r) => new Date(r.date).toLocaleDateString('ru') }, + { header: 'Статус', width: '110px', sortKey: 'status', cell: (r) => ( + r.status === SupplierReturnStatus.Posted + ? Проведён + : Черновик + )}, + { header: 'Поставщик', sortKey: 'supplier', cell: (r) => r.supplierName }, + { header: 'По приёмке', width: '140px', cell: (r) => r.referenceSupplyNumber ? {r.referenceSupplyNumber} : '—' }, + { header: 'Склад', width: '180px', cell: (r) => r.storeName }, + { header: 'Позиций', width: '90px', className: 'text-right', cell: (r) => r.lineCount }, + { header: 'Сумма', width: '160px', className: 'text-right font-mono', sortKey: 'total', cell: (r) => `${r.total.toLocaleString('ru', moneyFmt)} ${r.currencyCode}` }, + ]} + empty="Возвратов поставщикам нет." + /> + )} ) } diff --git a/src/food-market.web/src/pages/SuppliesPage.tsx b/src/food-market.web/src/pages/SuppliesPage.tsx index 3bab9fb..77ec3e2 100644 --- a/src/food-market.web/src/pages/SuppliesPage.tsx +++ b/src/food-market.web/src/pages/SuppliesPage.tsx @@ -1,7 +1,8 @@ import { Link, useNavigate } from 'react-router-dom' -import { Plus } from 'lucide-react' +import { Plus, PackagePlus } from 'lucide-react' import { ListPageShell } from '@/components/ListPageShell' import { DataTable } from '@/components/DataTable' +import { EmptyState } from '@/components/EmptyState' import { Pagination } from '@/components/Pagination' import { SearchBar } from '@/components/SearchBar' import { Button } from '@/components/Button' @@ -36,29 +37,39 @@ export function SuppliesPage() { )} > - r.id} - sortKey={sortKey} - sortOrder={sortOrder} - onSortChange={setSort} - onRowClick={(r) => navigate(`/purchases/supplies/${r.id}`)} - columns={[ - { 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 }, - { header: 'Позиций', width: '100px', className: 'text-right', cell: (r) => r.lineCount }, - { header: 'Сумма', width: '160px', className: 'text-right font-mono', sortKey: 'total', cell: (r) => `${r.total.toLocaleString('ru', moneyFmt)} ${r.currencyCode}` }, - ]} - empty="Приёмок пока нет. Создай первую — товар попадёт на склад после проведения." - /> + {!isLoading && (data?.items?.length ?? 0) === 0 && !search ? ( + navigate('/purchases/supplies/new')} + /> + ) : ( + r.id} + sortKey={sortKey} + sortOrder={sortOrder} + onSortChange={setSort} + onRowClick={(r) => navigate(`/purchases/supplies/${r.id}`)} + columns={[ + { 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 }, + { header: 'Позиций', width: '100px', className: 'text-right', cell: (r) => r.lineCount }, + { header: 'Сумма', width: '160px', className: 'text-right font-mono', sortKey: 'total', cell: (r) => `${r.total.toLocaleString('ru', moneyFmt)} ${r.currencyCode}` }, + ]} + empty="Приёмок пока нет. Создай первую — товар попадёт на склад после проведения." + /> + )} ) } diff --git a/src/food-market.web/src/pages/TransfersPage.tsx b/src/food-market.web/src/pages/TransfersPage.tsx index a987dae..cac9ed6 100644 --- a/src/food-market.web/src/pages/TransfersPage.tsx +++ b/src/food-market.web/src/pages/TransfersPage.tsx @@ -1,7 +1,8 @@ import { Link, useNavigate } from 'react-router-dom' -import { Plus, ArrowRight } from 'lucide-react' +import { Plus, ArrowRight, ArrowLeftRight } from 'lucide-react' import { ListPageShell } from '@/components/ListPageShell' import { DataTable } from '@/components/DataTable' +import { EmptyState } from '@/components/EmptyState' import { Pagination } from '@/components/Pagination' import { SearchBar } from '@/components/SearchBar' import { Button } from '@/components/Button' @@ -36,34 +37,44 @@ export function TransfersPage() { )} > - r.id} - sortKey={sortKey} - sortOrder={sortOrder} - onSortChange={setSort} - onRowClick={(r) => navigate(`/inventory/transfers/${r.id}`)} - columns={[ - { header: '№', width: '180px', sortKey: 'number', cell: (r) => {r.number} }, - { header: 'Дата', width: '120px', sortKey: 'date', cell: (r) => new Date(r.date).toLocaleDateString('ru') }, - { header: 'Статус', width: '110px', sortKey: 'status', cell: (r) => ( - r.status === TransferStatus.Posted - ? Проведён - : Черновик - )}, - { header: 'Откуда → Куда', cell: (r) => ( - - {r.fromStoreName} - - {r.toStoreName} - - )}, - { header: 'Позиций', width: '90px', className: 'text-right', cell: (r) => r.lineCount }, - { header: 'Сумма', width: '160px', className: 'text-right font-mono', sortKey: 'total', cell: (r) => `${r.total.toLocaleString('ru', moneyFmt)}` }, - ]} - empty="Перемещений пока нет." - /> + {!isLoading && (data?.items?.length ?? 0) === 0 && !search ? ( + navigate('/inventory/transfers/new')} + /> + ) : ( + r.id} + sortKey={sortKey} + sortOrder={sortOrder} + onSortChange={setSort} + onRowClick={(r) => navigate(`/inventory/transfers/${r.id}`)} + columns={[ + { header: '№', width: '180px', sortKey: 'number', cell: (r) => {r.number} }, + { header: 'Дата', width: '120px', sortKey: 'date', cell: (r) => new Date(r.date).toLocaleDateString('ru') }, + { header: 'Статус', width: '110px', sortKey: 'status', cell: (r) => ( + r.status === TransferStatus.Posted + ? Проведён + : Черновик + )}, + { header: 'Откуда → Куда', cell: (r) => ( + + {r.fromStoreName} + + {r.toStoreName} + + )}, + { header: 'Позиций', width: '90px', className: 'text-right', cell: (r) => r.lineCount }, + { header: 'Сумма', width: '160px', className: 'text-right font-mono', sortKey: 'total', cell: (r) => `${r.total.toLocaleString('ru', moneyFmt)}` }, + ]} + empty="Перемещений пока нет." + /> + )} ) }