diff --git a/src/food-market.web/src/components/Button.tsx b/src/food-market.web/src/components/Button.tsx index 704a935..21cedbf 100644 --- a/src/food-market.web/src/components/Button.tsx +++ b/src/food-market.web/src/components/Button.tsx @@ -1,4 +1,4 @@ -import type { ButtonHTMLAttributes, ReactNode } from 'react' +import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from 'react' import { cn } from '@/lib/utils' import { useReadOnly } from '@/lib/useReadOnly' @@ -27,7 +27,10 @@ const sizes: Record = { md: 'px-3.5 py-1.5 text-sm', } -export function Button({ variant = 'primary', size = 'md', mutating, className, children, disabled, title, ...rest }: ButtonProps) { +export const Button = forwardRef(function Button( + { variant = 'primary', size = 'md', mutating, className, children, disabled, title, ...rest }, + ref, +) { const ro = useReadOnly() // Variant primary/danger по умолчанию считаем мутирующими (Добавить/ // Сохранить/Удалить/Создать почти всегда primary либо danger). Secondary/ @@ -38,6 +41,7 @@ export function Button({ variant = 'primary', size = 'md', mutating, className, const blocked = isMutating && ro.readOnly return ( + + + + + ) +} diff --git a/src/food-market.web/src/components/ProductImageGallery.tsx b/src/food-market.web/src/components/ProductImageGallery.tsx index 476263c..e5cd8d5 100644 --- a/src/food-market.web/src/components/ProductImageGallery.tsx +++ b/src/food-market.web/src/components/ProductImageGallery.tsx @@ -3,6 +3,8 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { Trash2, Star, Upload, ChevronLeft, ChevronRight, X } from 'lucide-react' import { api } from '@/lib/api' import { Button } from '@/components/Button' +import { ConfirmDialog } from '@/components/ConfirmDialog' +import { useConfirm } from '@/lib/useConfirm' interface ImageDto { id: string; url: string; isMain: boolean; sortOrder: number } @@ -12,6 +14,7 @@ export function ProductImageGallery({ productId }: Props) { const qc = useQueryClient() const fileInput = useRef(null) const [lightboxIdx, setLightboxIdx] = useState(null) + const { confirm, dialogProps } = useConfirm() const url = `/api/catalog/products/${productId}/images` @@ -105,7 +108,14 @@ export function ProductImageGallery({ productId }: Props) { )} )} @@ -296,11 +305,21 @@ export function DemandEditPage() { label="Проведено" checked={isPosted} disabled={post.isPending || unpost.isPending || form.lines.length === 0} - onChange={(v) => { + onChange={async (v) => { if (v) { - if (confirm('Провести? Товар спишется со склада.')) post.mutate() + if (await confirm({ + title: 'Провести отгрузку?', + description: 'Товар спишется со склада в адрес покупателя.', + confirmLabel: 'Провести', + tone: 'warning', + })) post.mutate() } else { - if (confirm('Снять проведение? Товар вернётся на склад.')) unpost.mutate() + if (await confirm({ + title: 'Снять проведение?', + description: 'Товар вернётся на склад.', + confirmLabel: 'Снять', + tone: 'warning', + })) unpost.mutate() } }} /> @@ -398,6 +417,7 @@ export function DemandEditPage() { onClose={() => setPickerOpen(false)} onPick={(p) => { addLineFromProduct(p); setPickerOpen(false) }} /> + ) } diff --git a/src/food-market.web/src/pages/EmployeeRolesPage.tsx b/src/food-market.web/src/pages/EmployeeRolesPage.tsx index 18e87ae..f7b41ec 100644 --- a/src/food-market.web/src/pages/EmployeeRolesPage.tsx +++ b/src/food-market.web/src/pages/EmployeeRolesPage.tsx @@ -7,6 +7,8 @@ import { SearchBar } from '@/components/SearchBar' import { Button } from '@/components/Button' import { Modal } from '@/components/Modal' import { Field, TextInput, TextArea, Checkbox } from '@/components/Field' +import { ConfirmDialog } from '@/components/ConfirmDialog' +import { useConfirm } from '@/lib/useConfirm' import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog' const URL = '/api/organization/employee-roles' @@ -125,6 +127,7 @@ export function EmployeeRolesPage() { // Шаг выбора шаблона: показывается ПЕРЕД редактированием матрицы при добавлении новой роли. const [pickTemplate, setPickTemplate] = useState(false) const [templateId, setTemplateId] = useState('blank') + const { confirm, dialogProps } = useConfirm() const save = async () => { if (!form) return @@ -244,7 +247,11 @@ export function EmployeeRolesPage() { <> {form?.id && !form.isSystem && ( )} @@ -255,11 +264,21 @@ export function EnterEditPage() { label="Проведено" checked={isPosted} disabled={post.isPending || unpost.isPending || form.lines.length === 0} - onChange={(v) => { + onChange={async (v) => { if (v) { - if (confirm('Провести? Товар будет оприходован на склад.')) post.mutate() + if (await confirm({ + title: 'Провести оприходование?', + description: 'Товар будет оприходован на склад.', + confirmLabel: 'Провести', + tone: 'warning', + })) post.mutate() } else { - if (confirm('Снять проведение? Остатки откатятся (заблокировано, если товар уже списан).')) unpost.mutate() + if (await confirm({ + title: 'Снять проведение?', + description: 'Остатки откатятся (заблокировано, если товар уже списан).', + confirmLabel: 'Снять', + tone: 'warning', + })) unpost.mutate() } }} /> @@ -342,6 +361,7 @@ export function EnterEditPage() { onClose={() => setPickerOpen(false)} onPick={(p) => { addLineFromProduct(p); setPickerOpen(false) }} /> + ) } diff --git a/src/food-market.web/src/pages/InventoryEditPage.tsx b/src/food-market.web/src/pages/InventoryEditPage.tsx index 9f2980f..0f63c50 100644 --- a/src/food-market.web/src/pages/InventoryEditPage.tsx +++ b/src/food-market.web/src/pages/InventoryEditPage.tsx @@ -6,6 +6,8 @@ import { api } from '@/lib/api' import { Button } from '@/components/Button' import { Field, TextArea, Select, NumberInput, Checkbox } from '@/components/Field' import { DateField } from '@/components/DateField' +import { ConfirmDialog } from '@/components/ConfirmDialog' +import { useConfirm } from '@/lib/useConfirm' import { useStores } from '@/lib/useLookups' import { InventoryStatus, type InventoryDto } from '@/lib/types' @@ -41,6 +43,7 @@ export function InventoryEditPage() { const qc = useQueryClient() const stores = useStores() + const { confirm, dialogProps } = useConfirm() const [form, setForm] = useState
(emptyForm) const [error, setError] = useState(null) const csvInputRef = useRef(null) @@ -223,7 +226,13 @@ export function InventoryEditPage() { - @@ -277,11 +286,21 @@ export function InventoryEditPage() { label="Проведено" checked={isPosted} disabled={post.isPending || unpost.isPending || !canPost && !isPosted} - onChange={(v) => { + onChange={async (v) => { if (v) { - if (confirm('Провести? Учтённые остатки будут скорректированы на разницу.')) post.mutate() + if (await confirm({ + title: 'Провести инвентаризацию?', + description: 'Учтённые остатки будут скорректированы на разницу.', + confirmLabel: 'Провести', + tone: 'warning', + })) post.mutate() } else { - if (confirm('Снять проведение? Корректировки отменятся.')) unpost.mutate() + if (await confirm({ + title: 'Снять проведение?', + description: 'Корректировки отменятся.', + confirmLabel: 'Снять', + tone: 'warning', + })) unpost.mutate() } }} /> @@ -348,6 +367,7 @@ export function InventoryEditPage() { )} + ) } diff --git a/src/food-market.web/src/pages/LossEditPage.tsx b/src/food-market.web/src/pages/LossEditPage.tsx index 93f43b7..19f93d4 100644 --- a/src/food-market.web/src/pages/LossEditPage.tsx +++ b/src/food-market.web/src/pages/LossEditPage.tsx @@ -7,6 +7,8 @@ import { Button } from '@/components/Button' import { Field, TextArea, Select, MoneyInput, NumberInput, Checkbox } from '@/components/Field' import { DateField } from '@/components/DateField' import { ProductPicker } from '@/components/ProductPicker' +import { ConfirmDialog } from '@/components/ConfirmDialog' +import { useConfirm } from '@/lib/useConfirm' import { useStores, useCurrencies } from '@/lib/useLookups' import { useOrgSettings } from '@/lib/useOrgSettings' import { LossStatus, LossReason, lossReasonLabel, type LossDto, type Product } from '@/lib/types' @@ -48,6 +50,7 @@ export function LossEditPage() { const stores = useStores() const currencies = useCurrencies() const org = useOrgSettings() + const { confirm, dialogProps } = useConfirm() const [form, setForm] = useState
(emptyForm) const [pickerOpen, setPickerOpen] = useState(false) @@ -205,7 +208,13 @@ export function LossEditPage() {
{isDraft && !isNew && ( - )} @@ -266,11 +275,21 @@ export function LossEditPage() { label="Проведено" checked={isPosted} disabled={post.isPending || unpost.isPending || form.lines.length === 0} - onChange={(v) => { + onChange={async (v) => { if (v) { - if (confirm('Провести? Товар будет списан со склада.')) post.mutate() + if (await confirm({ + title: 'Провести списание?', + description: 'Товар будет списан со склада.', + confirmLabel: 'Провести', + tone: 'warning', + })) post.mutate() } else { - if (confirm('Снять проведение? Списанный товар вернётся на остаток.')) unpost.mutate() + if (await confirm({ + title: 'Снять проведение?', + description: 'Списанный товар вернётся на остаток.', + confirmLabel: 'Снять', + tone: 'warning', + })) unpost.mutate() } }} /> @@ -357,6 +376,7 @@ export function LossEditPage() { onClose={() => setPickerOpen(false)} onPick={(p) => { addLineFromProduct(p); setPickerOpen(false) }} /> + ) } diff --git a/src/food-market.web/src/pages/PriceTypesPage.tsx b/src/food-market.web/src/pages/PriceTypesPage.tsx index de6d72b..4e2c05e 100644 --- a/src/food-market.web/src/pages/PriceTypesPage.tsx +++ b/src/food-market.web/src/pages/PriceTypesPage.tsx @@ -7,6 +7,8 @@ import { SearchBar } from '@/components/SearchBar' import { Button } from '@/components/Button' import { Modal } from '@/components/Modal' import { Field, TextInput, Checkbox } from '@/components/Field' +import { ConfirmDialog } from '@/components/ConfirmDialog' +import { useConfirm } from '@/lib/useConfirm' import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog' import { useQueryClient } from '@tanstack/react-query' import type { PriceType } from '@/lib/types' @@ -29,6 +31,7 @@ export function PriceTypesPage() { const [form, setForm] = useState
(null) const [nameErr, setNameErr] = useState(null) const qc = useQueryClient() + const { confirm, dialogProps } = useConfirm() const save = async () => { if (!form) return @@ -92,7 +95,11 @@ export function PriceTypesPage() { <> {form?.id && !form.isSystem && (
)} + ) } diff --git a/src/food-market.web/src/pages/ProductEditPage.tsx b/src/food-market.web/src/pages/ProductEditPage.tsx index af788c5..1ac03ef 100644 --- a/src/food-market.web/src/pages/ProductEditPage.tsx +++ b/src/food-market.web/src/pages/ProductEditPage.tsx @@ -11,6 +11,8 @@ import { import { useOrgSettings } from '@/lib/useOrgSettings' import { BarcodeType, Packaging, type Product } from '@/lib/types' import { ProductImageGallery } from '@/components/ProductImageGallery' +import { ConfirmDialog } from '@/components/ConfirmDialog' +import { useConfirm } from '@/lib/useConfirm' import { generateEan13InternalPrefix2, generateBarcode, generateArticle } from '@/lib/barcode' interface PriceRow { id?: string; priceTypeId: string; amount: number; currencyId: string } @@ -60,6 +62,7 @@ export function ProductEditPage() { const groups = useProductGroups() const countries = useCountries() const currencies = useCurrencies() + const { confirm, dialogProps } = useConfirm() const priceTypes = usePriceTypes() const org = useOrgSettings() @@ -242,7 +245,13 @@ export function ProductEditPage() { type="button" variant="danger" size="sm" - onClick={() => { if (confirm('Удалить товар?')) remove.mutate() }} + onClick={async () => { + if (await confirm({ + title: 'Удалить товар?', + description: <>Удалить «{form.name || 'без названия'}»? Действие необратимо., + confirmLabel: 'Удалить', + })) remove.mutate() + }} > Удалить @@ -534,6 +543,7 @@ export function ProductEditPage() { )} + ) } diff --git a/src/food-market.web/src/pages/ProductGroupsPage.tsx b/src/food-market.web/src/pages/ProductGroupsPage.tsx index 8bbc421..f9fe108 100644 --- a/src/food-market.web/src/pages/ProductGroupsPage.tsx +++ b/src/food-market.web/src/pages/ProductGroupsPage.tsx @@ -7,6 +7,8 @@ import { SearchBar } from '@/components/SearchBar' import { Button } from '@/components/Button' import { Modal } from '@/components/Modal' import { Field, TextInput, Select, PercentInput } from '@/components/Field' +import { ConfirmDialog } from '@/components/ConfirmDialog' +import { useConfirm } from '@/lib/useConfirm' import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog' import type { ProductGroup } from '@/lib/types' import { useQueryClient } from '@tanstack/react-query' @@ -22,6 +24,7 @@ export function ProductGroupsPage() { const { create, update, remove } = useCatalogMutations(URL, URL) const [form, setForm] = useState
(null) const qc = useQueryClient() + const { confirm, dialogProps } = useConfirm() // inline-сохранение наценки прямо из таблицы — без открытия модалки. const saveMarkup = async (g: ProductGroup, markupPercent: number | null) => { await api.put(`/api/catalog/product-groups/${g.id}`, { @@ -102,7 +105,11 @@ export function ProductGroupsPage() { <> {form?.id && ( )} {isDraft && !isNew && ( - )} @@ -417,6 +431,7 @@ export function RetailSaleEditPage() { setPickerOpen(false)} onPick={addLineFromProduct} /> + ) } diff --git a/src/food-market.web/src/pages/StoresPage.tsx b/src/food-market.web/src/pages/StoresPage.tsx index 206c06d..279d355 100644 --- a/src/food-market.web/src/pages/StoresPage.tsx +++ b/src/food-market.web/src/pages/StoresPage.tsx @@ -7,6 +7,8 @@ import { SearchBar } from '@/components/SearchBar' import { Button } from '@/components/Button' import { Modal } from '@/components/Modal' import { Field, TextInput, Checkbox } from '@/components/Field' +import { ConfirmDialog } from '@/components/ConfirmDialog' +import { useConfirm } from '@/lib/useConfirm' import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog' import { type Store } from '@/lib/types' @@ -33,6 +35,7 @@ export function StoresPage() { const { create, update, remove } = useCatalogMutations(URL, URL) const [form, setForm] = useState
(null) const [nameErr, setNameErr] = useState(null) + const { confirm, dialogProps } = useConfirm() const save = async () => { if (!form) return @@ -87,7 +90,11 @@ export function StoresPage() { <> {form?.id && ( )} @@ -270,11 +279,21 @@ export function SupplierReturnEditPage() { label="Проведено" checked={isPosted} disabled={post.isPending || unpost.isPending || form.lines.length === 0} - onChange={(v) => { + onChange={async (v) => { if (v) { - if (confirm('Провести? Товар спишется со склада в адрес поставщика.')) post.mutate() + if (await confirm({ + title: 'Провести возврат поставщику?', + description: 'Товар спишется со склада в адрес поставщика.', + confirmLabel: 'Провести', + tone: 'warning', + })) post.mutate() } else { - if (confirm('Снять проведение? Товар вернётся на склад.')) unpost.mutate() + if (await confirm({ + title: 'Снять проведение?', + description: 'Товар вернётся на склад.', + confirmLabel: 'Снять', + tone: 'warning', + })) unpost.mutate() } }} /> @@ -361,6 +380,7 @@ export function SupplierReturnEditPage() { onClose={() => setPickerOpen(false)} onPick={(p) => { addLineFromProduct(p); setPickerOpen(false) }} /> + ) } diff --git a/src/food-market.web/src/pages/SupplyEditPage.tsx b/src/food-market.web/src/pages/SupplyEditPage.tsx index a7bc7d1..810ca40 100644 --- a/src/food-market.web/src/pages/SupplyEditPage.tsx +++ b/src/food-market.web/src/pages/SupplyEditPage.tsx @@ -8,6 +8,8 @@ import { Field, TextArea, Select, AsyncSelect, Checkbox, MoneyInput, NumberInput import { DateField } from '@/components/DateField' import { ProductPicker } from '@/components/ProductPicker' import { SupplyLineQuickAdd, type AddedProduct } from '@/components/SupplyLineQuickAdd' +import { ConfirmDialog } from '@/components/ConfirmDialog' +import { useConfirm } from '@/lib/useConfirm' import { useStores, useCurrencies, usePriceTypes } from '@/lib/useLookups' import { useOrgSettings } from '@/lib/useOrgSettings' import { SupplyStatus, type SupplyDto, type Product } from '@/lib/types' @@ -64,6 +66,7 @@ export function SupplyEditPage() { const currencies = useCurrencies() const org = useOrgSettings() const priceTypes = usePriceTypes() + const { confirm, dialogProps } = useConfirm() // Системный (главный) тип цен — на нём по умолчанию ведётся розница на кассе. // Заголовок колонки «Розничная» подменяется его именем чтобы соответствовать // тому, что увидит пользователь в карточке товара и в справочнике типов цен. @@ -282,7 +285,13 @@ export function SupplyEditPage() {
{isDraft && !isNew && ( - )} @@ -357,11 +366,21 @@ export function SupplyEditPage() { checked={isPosted} disabled={post.isPending || unpost.isPending || form.lines.length === 0 || form.lines.some(l => l.quantity <= 0 || l.unitPrice <= 0)} - onChange={(v) => { + onChange={async (v) => { if (v) { - if (confirm('После проведения товары будут оприходованы на склад и обновят себестоимость (скользящее среднее). Продолжить?')) post.mutate() + if (await confirm({ + title: 'Провести приёмку?', + description: 'После проведения товары будут оприходованы на склад и обновят себестоимость (скользящее среднее).', + confirmLabel: 'Провести', + tone: 'warning', + })) post.mutate() } else { - if (confirm('Снять проведение? Остатки откатятся, себестоимость останется (пересчитать вручную при необходимости).')) unpost.mutate() + if (await confirm({ + title: 'Снять проведение?', + description: 'Остатки откатятся, себестоимость останется (пересчитать вручную при необходимости).', + confirmLabel: 'Снять', + tone: 'warning', + })) unpost.mutate() } }} /> @@ -492,6 +511,7 @@ export function SupplyEditPage() { )} setPickerOpen(false)} onPick={addLineFromProduct} /> + ) } diff --git a/src/food-market.web/src/pages/TransferEditPage.tsx b/src/food-market.web/src/pages/TransferEditPage.tsx index 5d549af..b1f7d32 100644 --- a/src/food-market.web/src/pages/TransferEditPage.tsx +++ b/src/food-market.web/src/pages/TransferEditPage.tsx @@ -7,6 +7,8 @@ import { Button } from '@/components/Button' import { Field, TextArea, Select, MoneyInput, NumberInput, Checkbox } from '@/components/Field' import { DateField } from '@/components/DateField' import { ProductPicker } from '@/components/ProductPicker' +import { ConfirmDialog } from '@/components/ConfirmDialog' +import { useConfirm } from '@/lib/useConfirm' import { useStores } from '@/lib/useLookups' import { useOrgSettings } from '@/lib/useOrgSettings' import { TransferStatus, type TransferDto, type Product } from '@/lib/types' @@ -46,6 +48,7 @@ export function TransferEditPage() { const stores = useStores() const org = useOrgSettings() + const { confirm, dialogProps } = useConfirm() const [form, setForm] = useState
(emptyForm) const [pickerOpen, setPickerOpen] = useState(false) @@ -190,7 +193,13 @@ export function TransferEditPage() {
{isDraft && !isNew && ( - )} @@ -248,11 +257,21 @@ export function TransferEditPage() { label="Проведено" checked={isPosted} disabled={post.isPending || unpost.isPending || form.lines.length === 0} - onChange={(v) => { + onChange={async (v) => { if (v) { - if (confirm('Провести? Товар спишется с первого склада и встанет на второй.')) post.mutate() + if (await confirm({ + title: 'Провести перемещение?', + description: 'Товар спишется с первого склада и встанет на второй.', + confirmLabel: 'Провести', + tone: 'warning', + })) post.mutate() } else { - if (confirm('Снять проведение? Остатки обоих складов вернутся к предыдущим значениям.')) unpost.mutate() + if (await confirm({ + title: 'Снять проведение?', + description: 'Остатки обоих складов вернутся к предыдущим значениям.', + confirmLabel: 'Снять', + tone: 'warning', + })) unpost.mutate() } }} /> @@ -339,6 +358,7 @@ export function TransferEditPage() { onClose={() => setPickerOpen(false)} onPick={(p) => { addLineFromProduct(p); setPickerOpen(false) }} /> + ) }