feat(web): loading skeletons вместо «Загрузка…» в DataTable + edit-pages
Some checks are pending
Some checks are pending
Item 4 Sprint 7 — shimmer-плейсхолдеры вместо текстовых лоадеров. Компоненты (src/components/Skeleton.tsx): - <Skeleton variant='line'|'block'|'circle' /> — базовый pulse-блок. - <TableSkeleton rows cols /> — 8 строк × N колонок с псевдослучайной шириной плейсхолдеров, чтобы превью таблицы выглядело естественно. - <FormSkeleton /> — заголовок + 2 секции по 6 полей. DataTable: при isLoading=true теперь рендерит TableSkeleton (а не «Загрузка…»). На list-страницах layout остаётся стабильным. Edit-pages: добавил guard if (!isNew && existing.isLoading) return <FormSkeleton /> на 9 doc-edit pages (ProductEdit, DemandEdit, EnterEdit, InventoryEdit, LossEdit, SupplierReturnEdit, TransferEdit, SupplyEdit, RetailSaleEdit) + OrganizationSettingsPage. До этого они показывали пустые поля formы или «Загрузка…». DashboardPage: график выручки во время загрузки теперь Skeleton block 72rem высоты (вместо текста «Загрузка…»). tsc clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
56dd9fb639
commit
faa13521e8
|
|
@ -1,6 +1,7 @@
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import { ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react'
|
import { ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import { TableSkeleton } from '@/components/Skeleton'
|
||||||
|
|
||||||
export type SortOrder = 'asc' | 'desc'
|
export type SortOrder = 'asc' | 'desc'
|
||||||
|
|
||||||
|
|
@ -80,11 +81,10 @@ export function DataTable<T>({
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<tr>
|
// Shimmer skeleton вместо «Загрузка…»: 8 строк с псевдо-случайной
|
||||||
<td colSpan={columns.length} className="px-4 py-8 text-center text-slate-400">
|
// шириной плейсхолдеров, чтобы превью таблицы выглядело
|
||||||
Загрузка…
|
// естественно, пока приходят данные с сервера.
|
||||||
</td>
|
<TableSkeleton rows={8} columns={columns.length} />
|
||||||
</tr>
|
|
||||||
) : rows.length === 0 ? (
|
) : rows.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={columns.length} className="px-4 py-8 text-center text-slate-400">
|
<td colSpan={columns.length} className="px-4 py-8 text-center text-slate-400">
|
||||||
|
|
|
||||||
92
src/food-market.web/src/components/Skeleton.tsx
Normal file
92
src/food-market.web/src/components/Skeleton.tsx
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Реюзабельный shimmer-плейсхолдер. Заменяет «Загрузка…» в data-таблицах,
|
||||||
|
* карточках и edit-страницах на анимированный серый блок — пользователь
|
||||||
|
* видит будущую структуру вместо пустого текста.
|
||||||
|
*
|
||||||
|
* Примеры:
|
||||||
|
* <Skeleton className="h-4 w-32" /> — линия (текст)
|
||||||
|
* <Skeleton className="h-9 w-full" /> — input
|
||||||
|
* <Skeleton variant="circle" className="w-10 h-10" /> — аватар
|
||||||
|
* <Skeleton variant="block" className="h-48 w-full" /> — карточка
|
||||||
|
*
|
||||||
|
* Под капотом: bg-slate-200 + анимация pulse. На dark — slate-700.
|
||||||
|
*/
|
||||||
|
interface SkeletonProps {
|
||||||
|
className?: string
|
||||||
|
variant?: 'line' | 'block' | 'circle'
|
||||||
|
children?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Skeleton({ className, variant = 'line', children }: SkeletonProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
aria-busy="true"
|
||||||
|
role="status"
|
||||||
|
className={cn(
|
||||||
|
'animate-pulse bg-slate-200/80 dark:bg-slate-700/60',
|
||||||
|
variant === 'circle' ? 'rounded-full' : variant === 'block' ? 'rounded-lg' : 'rounded',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Готовый шаблон для скелета таблицы: N строк × M колонок, ширина рандомная
|
||||||
|
* по seed чтобы выглядело естественно. Заменяет «Загрузка…» в DataTable.
|
||||||
|
*/
|
||||||
|
export function TableSkeleton({ rows = 8, columns = 5 }: { rows?: number; columns?: number }) {
|
||||||
|
// Псевдо-random ширины — стабильные между рендерами и реалистичные.
|
||||||
|
const widths = ['w-24', 'w-32', 'w-40', 'w-20', 'w-16', 'w-28']
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{Array.from({ length: rows }).map((_, r) => (
|
||||||
|
<tr key={r}>
|
||||||
|
{Array.from({ length: columns }).map((__, c) => (
|
||||||
|
<td
|
||||||
|
key={c}
|
||||||
|
className="px-3 sm:px-4 py-2.5 border-b border-slate-100 dark:border-slate-800"
|
||||||
|
>
|
||||||
|
<Skeleton className={cn('h-4', widths[(r + c) % widths.length])} />
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Скелет для edit-страниц: 1 заголовок + 2 «секции» с полями.
|
||||||
|
* Используется когда `existing.isLoading` пока тащит данные.
|
||||||
|
*/
|
||||||
|
export function FormSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="max-w-5xl mx-auto p-3 sm:p-6 space-y-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Skeleton className="h-7 w-48" />
|
||||||
|
<Skeleton className="h-4 w-32" />
|
||||||
|
</div>
|
||||||
|
{[0, 1].map((s) => (
|
||||||
|
<section
|
||||||
|
key={s}
|
||||||
|
className="bg-white dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-800 p-4"
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-x-4 gap-y-4">
|
||||||
|
{Array.from({ length: 6 }).map((_, i) => (
|
||||||
|
<div key={i} className="space-y-1.5">
|
||||||
|
<Skeleton className="h-3 w-24" />
|
||||||
|
<Skeleton className="h-9 w-full" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query'
|
||||||
import { Package, Users, Warehouse, Store, TrendingUp, TrendingDown, Receipt, Banknote, Calendar } from 'lucide-react'
|
import { Package, Users, Warehouse, Store, TrendingUp, TrendingDown, Receipt, Banknote, Calendar } from 'lucide-react'
|
||||||
import { PageHeader } from '@/components/PageHeader'
|
import { PageHeader } from '@/components/PageHeader'
|
||||||
import { SalesChart } from '@/components/SalesChart'
|
import { SalesChart } from '@/components/SalesChart'
|
||||||
|
import { Skeleton } from '@/components/Skeleton'
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import type { PagedResult, SalesStatsResponse } from '@/lib/types'
|
import type { PagedResult, SalesStatsResponse } from '@/lib/types'
|
||||||
|
|
||||||
|
|
@ -138,7 +139,8 @@ export function DashboardPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{stats.isLoading ? (
|
{stats.isLoading ? (
|
||||||
<div className="h-72 flex items-center justify-center text-slate-400 text-sm">Загрузка…</div>
|
// Shimmer на месте графика: примерно той же высоты, чтобы layout не прыгал.
|
||||||
|
<Skeleton variant="block" className="h-72 w-full" />
|
||||||
) : !hasAnySales ? (
|
) : !hasAnySales ? (
|
||||||
<div className="h-72 flex flex-col items-center justify-center text-slate-400 text-sm gap-2">
|
<div className="h-72 flex flex-col items-center justify-center text-slate-400 text-sm gap-2">
|
||||||
<Receipt className="w-8 h-8 text-slate-300" />
|
<Receipt className="w-8 h-8 text-slate-300" />
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { Field, TextArea, Select, AsyncSelect, MoneyInput, NumberInput, Checkbox
|
||||||
import { DateField } from '@/components/DateField'
|
import { DateField } from '@/components/DateField'
|
||||||
import { ProductPicker } from '@/components/ProductPicker'
|
import { ProductPicker } from '@/components/ProductPicker'
|
||||||
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
||||||
|
import { FormSkeleton } from '@/components/Skeleton'
|
||||||
import { useConfirm } from '@/lib/useConfirm'
|
import { useConfirm } from '@/lib/useConfirm'
|
||||||
import { useStores, useCurrencies } from '@/lib/useLookups'
|
import { useStores, useCurrencies } from '@/lib/useLookups'
|
||||||
import { useOrgSettings } from '@/lib/useOrgSettings'
|
import { useOrgSettings } from '@/lib/useOrgSettings'
|
||||||
|
|
@ -209,6 +210,9 @@ export function DemandEditPage() {
|
||||||
? { minimumFractionDigits: 2, maximumFractionDigits: 2 }
|
? { minimumFractionDigits: 2, maximumFractionDigits: 2 }
|
||||||
: { maximumFractionDigits: 0 }
|
: { maximumFractionDigits: 0 }
|
||||||
|
|
||||||
|
// На редактировании пока тащим документ — показываем скелет.
|
||||||
|
if (!isNew && existing.isLoading) return <FormSkeleton />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit} className="flex flex-col h-full">
|
<form onSubmit={onSubmit} className="flex flex-col h-full">
|
||||||
<div className="flex items-center justify-between gap-4 px-6 py-3 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900">
|
<div className="flex items-center justify-between gap-4 px-6 py-3 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900">
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { Field, TextArea, Select, MoneyInput, NumberInput, Checkbox } from '@/co
|
||||||
import { DateField } from '@/components/DateField'
|
import { DateField } from '@/components/DateField'
|
||||||
import { ProductPicker } from '@/components/ProductPicker'
|
import { ProductPicker } from '@/components/ProductPicker'
|
||||||
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
||||||
|
import { FormSkeleton } from '@/components/Skeleton'
|
||||||
import { useConfirm } from '@/lib/useConfirm'
|
import { useConfirm } from '@/lib/useConfirm'
|
||||||
import { useStores, useCurrencies } from '@/lib/useLookups'
|
import { useStores, useCurrencies } from '@/lib/useLookups'
|
||||||
import { useOrgSettings } from '@/lib/useOrgSettings'
|
import { useOrgSettings } from '@/lib/useOrgSettings'
|
||||||
|
|
@ -189,6 +190,9 @@ export function EnterEditPage() {
|
||||||
? { minimumFractionDigits: 2, maximumFractionDigits: 2 }
|
? { minimumFractionDigits: 2, maximumFractionDigits: 2 }
|
||||||
: { maximumFractionDigits: 0 }
|
: { maximumFractionDigits: 0 }
|
||||||
|
|
||||||
|
// На редактировании пока тащим документ — показываем скелет.
|
||||||
|
if (!isNew && existing.isLoading) return <FormSkeleton />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit} className="flex flex-col h-full">
|
<form onSubmit={onSubmit} className="flex flex-col h-full">
|
||||||
<div className="flex items-center justify-between gap-4 px-6 py-3 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900">
|
<div className="flex items-center justify-between gap-4 px-6 py-3 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900">
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { Button } from '@/components/Button'
|
||||||
import { Field, TextArea, Select, NumberInput, Checkbox } from '@/components/Field'
|
import { Field, TextArea, Select, NumberInput, Checkbox } from '@/components/Field'
|
||||||
import { DateField } from '@/components/DateField'
|
import { DateField } from '@/components/DateField'
|
||||||
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
||||||
|
import { FormSkeleton } from '@/components/Skeleton'
|
||||||
import { useConfirm } from '@/lib/useConfirm'
|
import { useConfirm } from '@/lib/useConfirm'
|
||||||
import { useStores } from '@/lib/useLookups'
|
import { useStores } from '@/lib/useLookups'
|
||||||
import { InventoryStatus, type InventoryDto } from '@/lib/types'
|
import { InventoryStatus, type InventoryDto } from '@/lib/types'
|
||||||
|
|
@ -205,6 +206,9 @@ export function InventoryEditPage() {
|
||||||
|
|
||||||
const canPost = isDraft && form.lines.some((l) => l.diff !== 0) && !isNew
|
const canPost = isDraft && form.lines.some((l) => l.diff !== 0) && !isNew
|
||||||
|
|
||||||
|
// На редактировании пока тащим документ — показываем скелет.
|
||||||
|
if (!isNew && existing.isLoading) return <FormSkeleton />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit} className="flex flex-col h-full">
|
<form onSubmit={onSubmit} className="flex flex-col h-full">
|
||||||
<div className="flex items-center justify-between gap-4 px-6 py-3 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900">
|
<div className="flex items-center justify-between gap-4 px-6 py-3 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900">
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { Field, TextArea, Select, MoneyInput, NumberInput, Checkbox } from '@/co
|
||||||
import { DateField } from '@/components/DateField'
|
import { DateField } from '@/components/DateField'
|
||||||
import { ProductPicker } from '@/components/ProductPicker'
|
import { ProductPicker } from '@/components/ProductPicker'
|
||||||
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
||||||
|
import { FormSkeleton } from '@/components/Skeleton'
|
||||||
import { useConfirm } from '@/lib/useConfirm'
|
import { useConfirm } from '@/lib/useConfirm'
|
||||||
import { useStores, useCurrencies } from '@/lib/useLookups'
|
import { useStores, useCurrencies } from '@/lib/useLookups'
|
||||||
import { useOrgSettings } from '@/lib/useOrgSettings'
|
import { useOrgSettings } from '@/lib/useOrgSettings'
|
||||||
|
|
@ -192,6 +193,9 @@ export function LossEditPage() {
|
||||||
? { minimumFractionDigits: 2, maximumFractionDigits: 2 }
|
? { minimumFractionDigits: 2, maximumFractionDigits: 2 }
|
||||||
: { maximumFractionDigits: 0 }
|
: { maximumFractionDigits: 0 }
|
||||||
|
|
||||||
|
// На редактировании пока тащим документ — показываем скелет.
|
||||||
|
if (!isNew && existing.isLoading) return <FormSkeleton />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit} className="flex flex-col h-full">
|
<form onSubmit={onSubmit} className="flex flex-col h-full">
|
||||||
<div className="flex items-center justify-between gap-4 px-6 py-3 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900">
|
<div className="flex items-center justify-between gap-4 px-6 py-3 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900">
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { api } from '@/lib/api'
|
||||||
import { PageHeader } from '@/components/PageHeader'
|
import { PageHeader } from '@/components/PageHeader'
|
||||||
import { Button } from '@/components/Button'
|
import { Button } from '@/components/Button'
|
||||||
import { Field, TextInput, Select, Checkbox } from '@/components/Field'
|
import { Field, TextInput, Select, Checkbox } from '@/components/Field'
|
||||||
|
import { FormSkeleton } from '@/components/Skeleton'
|
||||||
import { useCountries } from '@/lib/useLookups'
|
import { useCountries } from '@/lib/useLookups'
|
||||||
import { useOrgSettings, type OrgSettings } from '@/lib/useOrgSettings'
|
import { useOrgSettings, type OrgSettings } from '@/lib/useOrgSettings'
|
||||||
|
|
||||||
|
|
@ -66,7 +67,7 @@ export function OrganizationSettingsPage() {
|
||||||
meta: { successMessage: 'Настройки сохранены' },
|
meta: { successMessage: 'Настройки сохранены' },
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!form) return <div className="p-6 text-sm text-slate-500">Загрузка…</div>
|
if (!form) return <FormSkeleton />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full overflow-auto">
|
<div className="h-full overflow-auto">
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { useOrgSettings } from '@/lib/useOrgSettings'
|
||||||
import { BarcodeType, Packaging, type Product } from '@/lib/types'
|
import { BarcodeType, Packaging, type Product } from '@/lib/types'
|
||||||
import { ProductImageGallery } from '@/components/ProductImageGallery'
|
import { ProductImageGallery } from '@/components/ProductImageGallery'
|
||||||
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
||||||
|
import { FormSkeleton } from '@/components/Skeleton'
|
||||||
import { useConfirm } from '@/lib/useConfirm'
|
import { useConfirm } from '@/lib/useConfirm'
|
||||||
import { generateEan13InternalPrefix2, generateBarcode, generateArticle } from '@/lib/barcode'
|
import { generateEan13InternalPrefix2, generateBarcode, generateArticle } from '@/lib/barcode'
|
||||||
|
|
||||||
|
|
@ -220,6 +221,10 @@ export function ProductEditPage() {
|
||||||
&& form.barcodes.length > 0
|
&& form.barcodes.length > 0
|
||||||
&& missingRequiredPrices.length === 0
|
&& missingRequiredPrices.length === 0
|
||||||
|
|
||||||
|
// На редактировании пока тащим существующий товар — показываем скелет
|
||||||
|
// вместо пустых полей формы, чтобы не путать пользователя.
|
||||||
|
if (!isNew && existing.isLoading) return <FormSkeleton />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit} className="flex flex-col h-full">
|
<form onSubmit={onSubmit} className="flex flex-col h-full">
|
||||||
{/* Sticky top bar */}
|
{/* Sticky top bar */}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { Button } from '@/components/Button'
|
||||||
import { Field, TextInput, TextArea, Select, AsyncSelect, MoneyInput, NumberInput } from '@/components/Field'
|
import { Field, TextInput, TextArea, Select, AsyncSelect, MoneyInput, NumberInput } from '@/components/Field'
|
||||||
import { ProductPicker } from '@/components/ProductPicker'
|
import { ProductPicker } from '@/components/ProductPicker'
|
||||||
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
||||||
|
import { FormSkeleton } from '@/components/Skeleton'
|
||||||
import { useConfirm } from '@/lib/useConfirm'
|
import { useConfirm } from '@/lib/useConfirm'
|
||||||
import { useStores, useCurrencies } from '@/lib/useLookups'
|
import { useStores, useCurrencies } from '@/lib/useLookups'
|
||||||
import { useOrgSettings } from '@/lib/useOrgSettings'
|
import { useOrgSettings } from '@/lib/useOrgSettings'
|
||||||
|
|
@ -208,6 +209,9 @@ export function RetailSaleEditPage() {
|
||||||
// кнопка disabled с подсказкой.
|
// кнопка disabled с подсказкой.
|
||||||
const canSave = !!form.storeId && !!form.currencyId && isDraft && form.lines.length > 0
|
const canSave = !!form.storeId && !!form.currencyId && isDraft && form.lines.length > 0
|
||||||
|
|
||||||
|
// На редактировании пока тащим документ — показываем скелет.
|
||||||
|
if (!isNew && existing.isLoading) return <FormSkeleton />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit} className="flex flex-col h-full">
|
<form onSubmit={onSubmit} className="flex flex-col h-full">
|
||||||
<div className="flex items-center justify-between gap-4 px-6 py-3 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900">
|
<div className="flex items-center justify-between gap-4 px-6 py-3 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900">
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { Field, TextArea, Select, AsyncSelect, MoneyInput, NumberInput, Checkbox
|
||||||
import { DateField } from '@/components/DateField'
|
import { DateField } from '@/components/DateField'
|
||||||
import { ProductPicker } from '@/components/ProductPicker'
|
import { ProductPicker } from '@/components/ProductPicker'
|
||||||
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
||||||
|
import { FormSkeleton } from '@/components/Skeleton'
|
||||||
import { useConfirm } from '@/lib/useConfirm'
|
import { useConfirm } from '@/lib/useConfirm'
|
||||||
import { useStores, useCurrencies } from '@/lib/useLookups'
|
import { useStores, useCurrencies } from '@/lib/useLookups'
|
||||||
import { useOrgSettings } from '@/lib/useOrgSettings'
|
import { useOrgSettings } from '@/lib/useOrgSettings'
|
||||||
|
|
@ -195,6 +196,9 @@ export function SupplierReturnEditPage() {
|
||||||
? { minimumFractionDigits: 2, maximumFractionDigits: 2 }
|
? { minimumFractionDigits: 2, maximumFractionDigits: 2 }
|
||||||
: { maximumFractionDigits: 0 }
|
: { maximumFractionDigits: 0 }
|
||||||
|
|
||||||
|
// На редактировании пока тащим документ — показываем скелет.
|
||||||
|
if (!isNew && existing.isLoading) return <FormSkeleton />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit} className="flex flex-col h-full">
|
<form onSubmit={onSubmit} className="flex flex-col h-full">
|
||||||
<div className="flex items-center justify-between gap-4 px-6 py-3 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900">
|
<div className="flex items-center justify-between gap-4 px-6 py-3 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900">
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { DateField } from '@/components/DateField'
|
||||||
import { ProductPicker } from '@/components/ProductPicker'
|
import { ProductPicker } from '@/components/ProductPicker'
|
||||||
import { SupplyLineQuickAdd, type AddedProduct } from '@/components/SupplyLineQuickAdd'
|
import { SupplyLineQuickAdd, type AddedProduct } from '@/components/SupplyLineQuickAdd'
|
||||||
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
||||||
|
import { FormSkeleton } from '@/components/Skeleton'
|
||||||
import { useConfirm } from '@/lib/useConfirm'
|
import { useConfirm } from '@/lib/useConfirm'
|
||||||
import { useStores, useCurrencies, usePriceTypes } from '@/lib/useLookups'
|
import { useStores, useCurrencies, usePriceTypes } from '@/lib/useLookups'
|
||||||
import { useOrgSettings } from '@/lib/useOrgSettings'
|
import { useOrgSettings } from '@/lib/useOrgSettings'
|
||||||
|
|
@ -268,6 +269,9 @@ export function SupplyEditPage() {
|
||||||
&& form.lines.length > 0
|
&& form.lines.length > 0
|
||||||
&& isDraft
|
&& isDraft
|
||||||
|
|
||||||
|
// На редактировании пока тащим документ — показываем скелет.
|
||||||
|
if (!isNew && existing.isLoading) return <FormSkeleton />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit} className="flex flex-col h-full">
|
<form onSubmit={onSubmit} className="flex flex-col h-full">
|
||||||
{/* Sticky top bar */}
|
{/* Sticky top bar */}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { Field, TextArea, Select, MoneyInput, NumberInput, Checkbox } from '@/co
|
||||||
import { DateField } from '@/components/DateField'
|
import { DateField } from '@/components/DateField'
|
||||||
import { ProductPicker } from '@/components/ProductPicker'
|
import { ProductPicker } from '@/components/ProductPicker'
|
||||||
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
||||||
|
import { FormSkeleton } from '@/components/Skeleton'
|
||||||
import { useConfirm } from '@/lib/useConfirm'
|
import { useConfirm } from '@/lib/useConfirm'
|
||||||
import { useStores } from '@/lib/useLookups'
|
import { useStores } from '@/lib/useLookups'
|
||||||
import { useOrgSettings } from '@/lib/useOrgSettings'
|
import { useOrgSettings } from '@/lib/useOrgSettings'
|
||||||
|
|
@ -177,6 +178,9 @@ export function TransferEditPage() {
|
||||||
? { minimumFractionDigits: 2, maximumFractionDigits: 2 }
|
? { minimumFractionDigits: 2, maximumFractionDigits: 2 }
|
||||||
: { maximumFractionDigits: 0 }
|
: { maximumFractionDigits: 0 }
|
||||||
|
|
||||||
|
// На редактировании пока тащим документ — показываем скелет.
|
||||||
|
if (!isNew && existing.isLoading) return <FormSkeleton />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit} className="flex flex-col h-full">
|
<form onSubmit={onSubmit} className="flex flex-col h-full">
|
||||||
<div className="flex items-center justify-between gap-4 px-6 py-3 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900">
|
<div className="flex items-center justify-between gap-4 px-6 py-3 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue