feat(web): ConfirmDialog компонент + useConfirm hook вместо window.confirm()
Some checks are pending
Some checks are pending
Item 2 Sprint 7 — заменил все нативные confirm() в фронте на собственный
<ConfirmDialog> с понятной типографикой, Esc=cancel, focus-on-Cancel
(чтобы случайный Enter не подтверждал удаление), tone='danger' | 'warning'.
Компоненты:
- src/components/ConfirmDialog.tsx — UI поверх Modal-overlay, AlertTriangle
иконка, primary/danger кнопки. Текст description конкретный («Удалить
товар «Молоко 3.2%»? Действие необратимо»). aria-labelledby выставлен.
- src/lib/useConfirm.ts — хук-обёртка: const { confirm, dialogProps } =
useConfirm(); if (await confirm({...})) action(). Возвращает Promise<bool>.
Button: переведён на forwardRef, чтобы dialog мог поставить фокус на Cancel.
Применено (17 страниц + 1 компонент):
- ProductEditPage (delete product)
- DemandEditPage / EnterEditPage / InventoryEditPage / LossEditPage /
SupplierReturnEditPage / TransferEditPage / SupplyEditPage / RetailSaleEditPage:
delete draft + post + unpost (всего 3 диалога на форму)
- EmployeesPage: уволить (warning) / удалить навсегда (danger), сохранена
динамика по статусу
- CounterpartiesPage / StoresPage / ProductGroupsPage / RetailPointsPage /
CountriesPage / PriceTypesPage / EmployeeRolesPage / SuperAdminUnitsOfMeasurePage:
delete с именем сущности в description
- ProductImageGallery: delete image
tsc --noEmit: clean. Текстов: в описаниях есть имена сущностей (товар,
номер документа), чтобы было ясно что именно удаляется.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
26959d56d1
commit
17a6da2f8b
|
|
@ -1,4 +1,4 @@
|
||||||
import type { ButtonHTMLAttributes, ReactNode } from 'react'
|
import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from 'react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useReadOnly } from '@/lib/useReadOnly'
|
import { useReadOnly } from '@/lib/useReadOnly'
|
||||||
|
|
||||||
|
|
@ -27,7 +27,10 @@ const sizes: Record<Size, string> = {
|
||||||
md: 'px-3.5 py-1.5 text-sm',
|
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<HTMLButtonElement, ButtonProps>(function Button(
|
||||||
|
{ variant = 'primary', size = 'md', mutating, className, children, disabled, title, ...rest },
|
||||||
|
ref,
|
||||||
|
) {
|
||||||
const ro = useReadOnly()
|
const ro = useReadOnly()
|
||||||
// Variant primary/danger по умолчанию считаем мутирующими (Добавить/
|
// Variant primary/danger по умолчанию считаем мутирующими (Добавить/
|
||||||
// Сохранить/Удалить/Создать почти всегда primary либо danger). Secondary/
|
// Сохранить/Удалить/Создать почти всегда primary либо danger). Secondary/
|
||||||
|
|
@ -38,6 +41,7 @@ export function Button({ variant = 'primary', size = 'md', mutating, className,
|
||||||
const blocked = isMutating && ro.readOnly
|
const blocked = isMutating && ro.readOnly
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
ref={ref}
|
||||||
{...rest}
|
{...rest}
|
||||||
disabled={disabled || blocked}
|
disabled={disabled || blocked}
|
||||||
title={blocked ? ro.reason : title}
|
title={blocked ? ro.reason : title}
|
||||||
|
|
@ -51,4 +55,4 @@ export function Button({ variant = 'primary', size = 'md', mutating, className,
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
|
||||||
89
src/food-market.web/src/components/ConfirmDialog.tsx
Normal file
89
src/food-market.web/src/components/ConfirmDialog.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { useEffect, useRef, type ReactNode } from 'react'
|
||||||
|
import { AlertTriangle } from 'lucide-react'
|
||||||
|
import { Button } from './Button'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Универсальный confirm-диалог для destructive actions. Заменяет нативный
|
||||||
|
* `window.confirm()` — даёт собственный UI (поверх Modal), Esc=отмена, фокус на
|
||||||
|
* Cancel (чтобы случайный Enter не подтверждал удаление).
|
||||||
|
*
|
||||||
|
* Использование: держим стейтом `{ open, payload }`, в `onConfirm` запускаем
|
||||||
|
* мутацию, в `onCancel` — `setOpen(false)`. Текст description — конкретный, с
|
||||||
|
* именем сущности («Удалить товар «Молоко 3.2%»? Действие необратимо»).
|
||||||
|
*
|
||||||
|
* Варианты:
|
||||||
|
* - tone='danger' (default): красная кнопка справа, иконка треугольник
|
||||||
|
* - tone='warning': жёлтая кнопка («Снять проведение»), всё ещё деструктивно
|
||||||
|
* но не уничтожающе. Иконка та же.
|
||||||
|
*/
|
||||||
|
interface ConfirmDialogProps {
|
||||||
|
open: boolean
|
||||||
|
title: string
|
||||||
|
description?: ReactNode
|
||||||
|
confirmLabel?: string
|
||||||
|
cancelLabel?: string
|
||||||
|
tone?: 'danger' | 'warning'
|
||||||
|
busy?: boolean
|
||||||
|
onConfirm: () => void
|
||||||
|
onCancel: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConfirmDialog({
|
||||||
|
open, title, description, confirmLabel = 'Удалить', cancelLabel = 'Отмена',
|
||||||
|
tone = 'danger', busy = false, onConfirm, onCancel,
|
||||||
|
}: ConfirmDialogProps) {
|
||||||
|
const cancelBtnRef = useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return
|
||||||
|
const onKey = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') { e.preventDefault(); onCancel() }
|
||||||
|
else if (e.key === 'Enter') { e.preventDefault(); if (!busy) onConfirm() }
|
||||||
|
}
|
||||||
|
document.addEventListener('keydown', onKey)
|
||||||
|
// Фокус на Cancel по умолчанию — безопасно для случайного Enter.
|
||||||
|
cancelBtnRef.current?.focus()
|
||||||
|
return () => document.removeEventListener('keydown', onKey)
|
||||||
|
}, [open, busy, onCancel, onConfirm])
|
||||||
|
|
||||||
|
if (!open) return null
|
||||||
|
|
||||||
|
const iconColor = tone === 'danger' ? 'text-red-600' : 'text-amber-600'
|
||||||
|
const iconBg = tone === 'danger' ? 'bg-red-50 dark:bg-red-900/20' : 'bg-amber-50 dark:bg-amber-900/20'
|
||||||
|
const confirmVariant = tone === 'danger' ? 'danger' : 'primary'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[60] flex items-center justify-center p-4 bg-slate-900/50 backdrop-blur-sm"
|
||||||
|
onClick={onCancel}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="confirm-dialog-title"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-full max-w-md bg-white dark:bg-slate-900 rounded-xl shadow-xl flex flex-col"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="flex gap-3 px-5 pt-5">
|
||||||
|
<div className={`shrink-0 w-10 h-10 rounded-full ${iconBg} flex items-center justify-center ${iconColor}`}>
|
||||||
|
<AlertTriangle className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h2 id="confirm-dialog-title" className="font-semibold text-slate-900 dark:text-slate-100">{title}</h2>
|
||||||
|
{description && (
|
||||||
|
<div className="mt-1 text-sm text-slate-600 dark:text-slate-400">{description}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="px-5 py-4 flex flex-wrap justify-end gap-2">
|
||||||
|
<Button ref={cancelBtnRef} variant="secondary" mutating={false} onClick={onCancel} disabled={busy}>
|
||||||
|
{cancelLabel}
|
||||||
|
</Button>
|
||||||
|
<Button variant={confirmVariant} onClick={onConfirm} disabled={busy}>
|
||||||
|
{busy ? '…' : confirmLabel}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,8 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { Trash2, Star, Upload, ChevronLeft, ChevronRight, X } from 'lucide-react'
|
import { Trash2, Star, Upload, ChevronLeft, ChevronRight, X } from 'lucide-react'
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import { Button } from '@/components/Button'
|
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 }
|
interface ImageDto { id: string; url: string; isMain: boolean; sortOrder: number }
|
||||||
|
|
||||||
|
|
@ -12,6 +14,7 @@ export function ProductImageGallery({ productId }: Props) {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const fileInput = useRef<HTMLInputElement>(null)
|
const fileInput = useRef<HTMLInputElement>(null)
|
||||||
const [lightboxIdx, setLightboxIdx] = useState<number | null>(null)
|
const [lightboxIdx, setLightboxIdx] = useState<number | null>(null)
|
||||||
|
const { confirm, dialogProps } = useConfirm()
|
||||||
|
|
||||||
const url = `/api/catalog/products/${productId}/images`
|
const url = `/api/catalog/products/${productId}/images`
|
||||||
|
|
||||||
|
|
@ -105,7 +108,14 @@ export function ProductImageGallery({ productId }: Props) {
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => { e.stopPropagation(); if (confirm('Удалить это изображение?')) remove.mutate(img.id) }}
|
onClick={async (e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (await confirm({
|
||||||
|
title: 'Удалить изображение?',
|
||||||
|
description: 'Файл будет удалён со склада, действие необратимо.',
|
||||||
|
confirmLabel: 'Удалить',
|
||||||
|
})) remove.mutate(img.id)
|
||||||
|
}}
|
||||||
className="p-1 rounded bg-white/90 hover:bg-white text-red-600"
|
className="p-1 rounded bg-white/90 hover:bg-white text-red-600"
|
||||||
title="Удалить"
|
title="Удалить"
|
||||||
>
|
>
|
||||||
|
|
@ -159,6 +169,7 @@ export function ProductImageGallery({ productId }: Props) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<ConfirmDialog {...dialogProps} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
59
src/food-market.web/src/lib/useConfirm.ts
Normal file
59
src/food-market.web/src/lib/useConfirm.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { useCallback, useState } from 'react'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Хук-обёртка над <ConfirmDialog>: переписывает `if (confirm('...'))` на
|
||||||
|
* `await confirm({ title, description, ... })`. Возвращает true если
|
||||||
|
* пользователь подтвердил, false если отмёл (Esc, бэкграунд клик, Cancel).
|
||||||
|
*
|
||||||
|
* Пример:
|
||||||
|
* const { confirm, dialogProps } = useConfirm()
|
||||||
|
* ...
|
||||||
|
* if (await confirm({ title: 'Удалить товар?', description: name, confirmLabel: 'Удалить' })) {
|
||||||
|
* remove.mutate()
|
||||||
|
* }
|
||||||
|
* ...
|
||||||
|
* <ConfirmDialog {...dialogProps} />
|
||||||
|
*/
|
||||||
|
export interface ConfirmRequest {
|
||||||
|
title: string
|
||||||
|
description?: ReactNode
|
||||||
|
confirmLabel?: string
|
||||||
|
cancelLabel?: string
|
||||||
|
tone?: 'danger' | 'warning'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useConfirm() {
|
||||||
|
const [state, setState] = useState<{
|
||||||
|
req: ConfirmRequest
|
||||||
|
resolve: (v: boolean) => void
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
|
const confirm = useCallback((req: ConfirmRequest): Promise<boolean> => {
|
||||||
|
return new Promise<boolean>((resolve) => setState({ req, resolve }))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onCancel = useCallback(() => {
|
||||||
|
state?.resolve(false)
|
||||||
|
setState(null)
|
||||||
|
}, [state])
|
||||||
|
|
||||||
|
const onConfirm = useCallback(() => {
|
||||||
|
state?.resolve(true)
|
||||||
|
setState(null)
|
||||||
|
}, [state])
|
||||||
|
|
||||||
|
return {
|
||||||
|
confirm,
|
||||||
|
dialogProps: {
|
||||||
|
open: !!state,
|
||||||
|
title: state?.req.title ?? '',
|
||||||
|
description: state?.req.description,
|
||||||
|
confirmLabel: state?.req.confirmLabel,
|
||||||
|
cancelLabel: state?.req.cancelLabel,
|
||||||
|
tone: state?.req.tone,
|
||||||
|
onCancel,
|
||||||
|
onConfirm,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,8 @@ import { Button } from '@/components/Button'
|
||||||
import { Modal } from '@/components/Modal'
|
import { Modal } from '@/components/Modal'
|
||||||
import { Field, TextInput, TextArea, Select } from '@/components/Field'
|
import { Field, TextInput, TextArea, Select } from '@/components/Field'
|
||||||
import { PhoneInput } from '@/components/PhoneInput'
|
import { PhoneInput } from '@/components/PhoneInput'
|
||||||
|
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
||||||
|
import { useConfirm } from '@/lib/useConfirm'
|
||||||
import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog'
|
import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog'
|
||||||
import { type Counterparty, type Country, type PagedResult, CounterpartyType } from '@/lib/types'
|
import { type Counterparty, type Country, type PagedResult, CounterpartyType } from '@/lib/types'
|
||||||
|
|
||||||
|
|
@ -53,6 +55,7 @@ export function CounterpartiesPage() {
|
||||||
const { create, update, remove } = useCatalogMutations(URL, URL)
|
const { create, update, remove } = useCatalogMutations(URL, URL)
|
||||||
const [form, setForm] = useState<Form | null>(null)
|
const [form, setForm] = useState<Form | null>(null)
|
||||||
const [fieldErrors, setFieldErrors] = useState<Partial<Record<'name' | 'email' | 'phone', string>>>({})
|
const [fieldErrors, setFieldErrors] = useState<Partial<Record<'name' | 'email' | 'phone', string>>>({})
|
||||||
|
const { confirm, dialogProps } = useConfirm()
|
||||||
|
|
||||||
const countries = useQuery({
|
const countries = useQuery({
|
||||||
queryKey: ['countries-lookup'],
|
queryKey: ['countries-lookup'],
|
||||||
|
|
@ -117,7 +120,11 @@ export function CounterpartiesPage() {
|
||||||
<>
|
<>
|
||||||
{form?.id && (
|
{form?.id && (
|
||||||
<Button variant="danger" size="sm" onClick={async () => {
|
<Button variant="danger" size="sm" onClick={async () => {
|
||||||
if (confirm('Удалить контрагента?')) {
|
if (await confirm({
|
||||||
|
title: 'Удалить контрагента?',
|
||||||
|
description: <>Удалить <strong>«{form.name || 'без названия'}»</strong>? Действие необратимо.</>,
|
||||||
|
confirmLabel: 'Удалить',
|
||||||
|
})) {
|
||||||
await remove.mutateAsync(form.id!)
|
await remove.mutateAsync(form.id!)
|
||||||
setForm(null); setFieldErrors({})
|
setForm(null); setFieldErrors({})
|
||||||
}
|
}
|
||||||
|
|
@ -191,6 +198,7 @@ export function CounterpartiesPage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<ConfirmDialog {...dialogProps} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import { SearchBar } from '@/components/SearchBar'
|
||||||
import { Button } from '@/components/Button'
|
import { Button } from '@/components/Button'
|
||||||
import { Modal } from '@/components/Modal'
|
import { Modal } from '@/components/Modal'
|
||||||
import { Field, TextInput, Select, NumberInput } from '@/components/Field'
|
import { Field, TextInput, Select, NumberInput } from '@/components/Field'
|
||||||
|
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
||||||
|
import { useConfirm } from '@/lib/useConfirm'
|
||||||
import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog'
|
import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog'
|
||||||
import { useCurrencies } from '@/lib/useLookups'
|
import { useCurrencies } from '@/lib/useLookups'
|
||||||
import type { Country } from '@/lib/types'
|
import type { Country } from '@/lib/types'
|
||||||
|
|
@ -28,6 +30,7 @@ export function CountriesPage() {
|
||||||
const { create, update, remove } = useCatalogMutations(URL, URL)
|
const { create, update, remove } = useCatalogMutations(URL, URL)
|
||||||
const currencies = useCurrencies()
|
const currencies = useCurrencies()
|
||||||
const [form, setForm] = useState<Form | null>(null)
|
const [form, setForm] = useState<Form | null>(null)
|
||||||
|
const { confirm, dialogProps } = useConfirm()
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
if (!form) return
|
if (!form) return
|
||||||
|
|
@ -80,7 +83,11 @@ export function CountriesPage() {
|
||||||
<>
|
<>
|
||||||
{form?.id && (
|
{form?.id && (
|
||||||
<Button variant="danger" size="sm" onClick={async () => {
|
<Button variant="danger" size="sm" onClick={async () => {
|
||||||
if (confirm('Удалить страну?')) {
|
if (await confirm({
|
||||||
|
title: 'Удалить страну?',
|
||||||
|
description: <>Удалить <strong>«{form.name || 'без названия'}»</strong>? Действие необратимо.</>,
|
||||||
|
confirmLabel: 'Удалить',
|
||||||
|
})) {
|
||||||
await remove.mutateAsync(form.id!)
|
await remove.mutateAsync(form.id!)
|
||||||
setForm(null)
|
setForm(null)
|
||||||
}
|
}
|
||||||
|
|
@ -121,6 +128,7 @@ export function CountriesPage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<ConfirmDialog {...dialogProps} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import { Button } from '@/components/Button'
|
||||||
import { Field, TextArea, Select, AsyncSelect, MoneyInput, NumberInput, Checkbox } from '@/components/Field'
|
import { Field, TextArea, Select, AsyncSelect, MoneyInput, NumberInput, Checkbox } from '@/components/Field'
|
||||||
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 { 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'
|
||||||
import {
|
import {
|
||||||
|
|
@ -57,6 +59,7 @@ export function DemandEditPage() {
|
||||||
const stores = useStores()
|
const stores = useStores()
|
||||||
const currencies = useCurrencies()
|
const currencies = useCurrencies()
|
||||||
const org = useOrgSettings()
|
const org = useOrgSettings()
|
||||||
|
const { confirm, dialogProps } = useConfirm()
|
||||||
|
|
||||||
const [form, setForm] = useState<Form>(emptyForm)
|
const [form, setForm] = useState<Form>(emptyForm)
|
||||||
const [pickerOpen, setPickerOpen] = useState(false)
|
const [pickerOpen, setPickerOpen] = useState(false)
|
||||||
|
|
@ -222,7 +225,13 @@ export function DemandEditPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3 flex-shrink-0 items-center">
|
<div className="flex gap-3 flex-shrink-0 items-center">
|
||||||
{isDraft && !isNew && (
|
{isDraft && !isNew && (
|
||||||
<Button type="button" variant="danger" size="sm" onClick={() => { if (confirm('Удалить черновик?')) remove.mutate() }}>
|
<Button type="button" variant="danger" size="sm" onClick={async () => {
|
||||||
|
if (await confirm({
|
||||||
|
title: 'Удалить черновик отгрузки?',
|
||||||
|
description: <>Удалить черновик № <strong>{existing.data?.number || '—'}</strong>? Действие необратимо.</>,
|
||||||
|
confirmLabel: 'Удалить',
|
||||||
|
})) remove.mutate()
|
||||||
|
}}>
|
||||||
<Trash2 className="w-4 h-4" /> Удалить
|
<Trash2 className="w-4 h-4" /> Удалить
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
@ -296,11 +305,21 @@ export function DemandEditPage() {
|
||||||
label="Проведено"
|
label="Проведено"
|
||||||
checked={isPosted}
|
checked={isPosted}
|
||||||
disabled={post.isPending || unpost.isPending || form.lines.length === 0}
|
disabled={post.isPending || unpost.isPending || form.lines.length === 0}
|
||||||
onChange={(v) => {
|
onChange={async (v) => {
|
||||||
if (v) {
|
if (v) {
|
||||||
if (confirm('Провести? Товар спишется со склада.')) post.mutate()
|
if (await confirm({
|
||||||
|
title: 'Провести отгрузку?',
|
||||||
|
description: 'Товар спишется со склада в адрес покупателя.',
|
||||||
|
confirmLabel: 'Провести',
|
||||||
|
tone: 'warning',
|
||||||
|
})) post.mutate()
|
||||||
} else {
|
} 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)}
|
onClose={() => setPickerOpen(false)}
|
||||||
onPick={(p) => { addLineFromProduct(p); setPickerOpen(false) }}
|
onPick={(p) => { addLineFromProduct(p); setPickerOpen(false) }}
|
||||||
/>
|
/>
|
||||||
|
<ConfirmDialog {...dialogProps} />
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import { SearchBar } from '@/components/SearchBar'
|
||||||
import { Button } from '@/components/Button'
|
import { Button } from '@/components/Button'
|
||||||
import { Modal } from '@/components/Modal'
|
import { Modal } from '@/components/Modal'
|
||||||
import { Field, TextInput, TextArea, Checkbox } from '@/components/Field'
|
import { Field, TextInput, TextArea, Checkbox } from '@/components/Field'
|
||||||
|
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
||||||
|
import { useConfirm } from '@/lib/useConfirm'
|
||||||
import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog'
|
import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog'
|
||||||
|
|
||||||
const URL = '/api/organization/employee-roles'
|
const URL = '/api/organization/employee-roles'
|
||||||
|
|
@ -125,6 +127,7 @@ export function EmployeeRolesPage() {
|
||||||
// Шаг выбора шаблона: показывается ПЕРЕД редактированием матрицы при добавлении новой роли.
|
// Шаг выбора шаблона: показывается ПЕРЕД редактированием матрицы при добавлении новой роли.
|
||||||
const [pickTemplate, setPickTemplate] = useState(false)
|
const [pickTemplate, setPickTemplate] = useState(false)
|
||||||
const [templateId, setTemplateId] = useState<string>('blank')
|
const [templateId, setTemplateId] = useState<string>('blank')
|
||||||
|
const { confirm, dialogProps } = useConfirm()
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
if (!form) return
|
if (!form) return
|
||||||
|
|
@ -244,7 +247,11 @@ export function EmployeeRolesPage() {
|
||||||
<>
|
<>
|
||||||
{form?.id && !form.isSystem && (
|
{form?.id && !form.isSystem && (
|
||||||
<Button variant="danger" size="sm" onClick={async () => {
|
<Button variant="danger" size="sm" onClick={async () => {
|
||||||
if (confirm('Удалить роль?')) {
|
if (await confirm({
|
||||||
|
title: 'Удалить роль?',
|
||||||
|
description: <>Удалить <strong>«{form.name || 'без названия'}»</strong>? Действие необратимо.</>,
|
||||||
|
confirmLabel: 'Удалить',
|
||||||
|
})) {
|
||||||
await remove.mutateAsync(form.id!)
|
await remove.mutateAsync(form.id!)
|
||||||
setForm(null); setNameErr(null)
|
setForm(null); setNameErr(null)
|
||||||
}
|
}
|
||||||
|
|
@ -304,6 +311,7 @@ export function EmployeeRolesPage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<ConfirmDialog {...dialogProps} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import { Button } from '@/components/Button'
|
||||||
import { Modal } from '@/components/Modal'
|
import { Modal } from '@/components/Modal'
|
||||||
import { Field, TextInput, TextArea, Checkbox, MoneyInput } from '@/components/Field'
|
import { Field, TextInput, TextArea, Checkbox, MoneyInput } from '@/components/Field'
|
||||||
import { PhoneInput } from '@/components/PhoneInput'
|
import { PhoneInput } from '@/components/PhoneInput'
|
||||||
|
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
||||||
|
import { useConfirm } from '@/lib/useConfirm'
|
||||||
import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog'
|
import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog'
|
||||||
import type { PagedResult, RetailPoint } from '@/lib/types'
|
import type { PagedResult, RetailPoint } from '@/lib/types'
|
||||||
import type { EmployeeRoleDto } from '@/pages/EmployeeRolesPage'
|
import type { EmployeeRoleDto } from '@/pages/EmployeeRolesPage'
|
||||||
|
|
@ -93,6 +95,7 @@ export function EmployeesPage() {
|
||||||
// над любой ошибкой 4xx/5xx с сервера.
|
// над любой ошибкой 4xx/5xx с сервера.
|
||||||
const [blockedDelete, setBlockedDelete] = useState<{ title: string; body: string } | null>(null)
|
const [blockedDelete, setBlockedDelete] = useState<{ title: string; body: string } | null>(null)
|
||||||
const [fieldErrors, setFieldErrors] = useState<Partial<Record<'lastName' | 'firstName' | 'email' | 'phone', string>>>({})
|
const [fieldErrors, setFieldErrors] = useState<Partial<Record<'lastName' | 'firstName' | 'email' | 'phone', string>>>({})
|
||||||
|
const { confirm, dialogProps } = useConfirm()
|
||||||
|
|
||||||
const roles = useQuery({
|
const roles = useQuery({
|
||||||
queryKey: ['employee-roles-lookup'],
|
queryKey: ['employee-roles-lookup'],
|
||||||
|
|
@ -283,10 +286,20 @@ export function EmployeesPage() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const fullName = `${activeEmployee.lastName ?? ''} ${activeEmployee.firstName ?? ''}`.trim()
|
const fullName = `${activeEmployee.lastName ?? ''} ${activeEmployee.firstName ?? ''}`.trim()
|
||||||
const confirmText = activeEmployee.status === 'active'
|
const confirmReq = activeEmployee.status === 'active'
|
||||||
? `Уволить сотрудника «${fullName}»?\n\nЕго учётная запись потеряет доступ, документы и история останутся в системе. Восстановить можно в любой момент.`
|
? {
|
||||||
: `Удалить запись о сотруднике «${fullName}»?\n\nСотрудник уже уволен. После удаления он скрывается из обычных списков, но во всех связанных документах остаётся подпись «${fullName} (удалён)».`
|
title: 'Уволить сотрудника?',
|
||||||
if (!confirm(confirmText)) return
|
description: <>Уволить <strong>«{fullName}»</strong>? Его учётная запись потеряет доступ, документы и история останутся в системе. Восстановить можно в любой момент.</>,
|
||||||
|
confirmLabel: 'Уволить',
|
||||||
|
tone: 'warning' as const,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
title: 'Удалить навсегда?',
|
||||||
|
description: <>Удалить запись о сотруднике <strong>«{fullName}»</strong>? Сотрудник уже уволен. После удаления он скрывается из обычных списков, но во всех связанных документах остаётся подпись «{fullName} (удалён)». Действие необратимо.</>,
|
||||||
|
confirmLabel: 'Удалить',
|
||||||
|
tone: 'danger' as const,
|
||||||
|
}
|
||||||
|
if (!await confirm(confirmReq)) return
|
||||||
try {
|
try {
|
||||||
await remove.mutateAsync(form.id!)
|
await remove.mutateAsync(form.id!)
|
||||||
list.refetch?.()
|
list.refetch?.()
|
||||||
|
|
@ -476,6 +489,7 @@ export function EmployeesPage() {
|
||||||
{blockedDelete?.body}
|
{blockedDelete?.body}
|
||||||
</p>
|
</p>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<ConfirmDialog {...dialogProps} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import { Button } from '@/components/Button'
|
||||||
import { Field, TextArea, Select, MoneyInput, NumberInput, Checkbox } from '@/components/Field'
|
import { Field, TextArea, Select, MoneyInput, NumberInput, Checkbox } from '@/components/Field'
|
||||||
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 { 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'
|
||||||
import { EnterStatus, type EnterDto, type Product } from '@/lib/types'
|
import { EnterStatus, type EnterDto, type Product } from '@/lib/types'
|
||||||
|
|
@ -49,6 +51,7 @@ export function EnterEditPage() {
|
||||||
const stores = useStores()
|
const stores = useStores()
|
||||||
const currencies = useCurrencies()
|
const currencies = useCurrencies()
|
||||||
const org = useOrgSettings()
|
const org = useOrgSettings()
|
||||||
|
const { confirm, dialogProps } = useConfirm()
|
||||||
|
|
||||||
const [form, setForm] = useState<Form>(emptyForm)
|
const [form, setForm] = useState<Form>(emptyForm)
|
||||||
const [pickerOpen, setPickerOpen] = useState(false)
|
const [pickerOpen, setPickerOpen] = useState(false)
|
||||||
|
|
@ -202,7 +205,13 @@ export function EnterEditPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3 flex-shrink-0 items-center">
|
<div className="flex gap-3 flex-shrink-0 items-center">
|
||||||
{isDraft && !isNew && (
|
{isDraft && !isNew && (
|
||||||
<Button type="button" variant="danger" size="sm" onClick={() => { if (confirm('Удалить черновик?')) remove.mutate() }}>
|
<Button type="button" variant="danger" size="sm" onClick={async () => {
|
||||||
|
if (await confirm({
|
||||||
|
title: 'Удалить черновик оприходования?',
|
||||||
|
description: <>Удалить черновик № <strong>{existing.data?.number || '—'}</strong>? Действие необратимо.</>,
|
||||||
|
confirmLabel: 'Удалить',
|
||||||
|
})) remove.mutate()
|
||||||
|
}}>
|
||||||
<Trash2 className="w-4 h-4" /> Удалить
|
<Trash2 className="w-4 h-4" /> Удалить
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
@ -255,11 +264,21 @@ export function EnterEditPage() {
|
||||||
label="Проведено"
|
label="Проведено"
|
||||||
checked={isPosted}
|
checked={isPosted}
|
||||||
disabled={post.isPending || unpost.isPending || form.lines.length === 0}
|
disabled={post.isPending || unpost.isPending || form.lines.length === 0}
|
||||||
onChange={(v) => {
|
onChange={async (v) => {
|
||||||
if (v) {
|
if (v) {
|
||||||
if (confirm('Провести? Товар будет оприходован на склад.')) post.mutate()
|
if (await confirm({
|
||||||
|
title: 'Провести оприходование?',
|
||||||
|
description: 'Товар будет оприходован на склад.',
|
||||||
|
confirmLabel: 'Провести',
|
||||||
|
tone: 'warning',
|
||||||
|
})) post.mutate()
|
||||||
} else {
|
} 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)}
|
onClose={() => setPickerOpen(false)}
|
||||||
onPick={(p) => { addLineFromProduct(p); setPickerOpen(false) }}
|
onPick={(p) => { addLineFromProduct(p); setPickerOpen(false) }}
|
||||||
/>
|
/>
|
||||||
|
<ConfirmDialog {...dialogProps} />
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import { api } from '@/lib/api'
|
||||||
import { Button } from '@/components/Button'
|
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 { 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'
|
||||||
|
|
||||||
|
|
@ -41,6 +43,7 @@ export function InventoryEditPage() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
|
||||||
const stores = useStores()
|
const stores = useStores()
|
||||||
|
const { confirm, dialogProps } = useConfirm()
|
||||||
const [form, setForm] = useState<Form>(emptyForm)
|
const [form, setForm] = useState<Form>(emptyForm)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const csvInputRef = useRef<HTMLInputElement>(null)
|
const csvInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
@ -223,7 +226,13 @@ export function InventoryEditPage() {
|
||||||
<Button type="button" variant="secondary" size="sm" onClick={() => csvInputRef.current?.click()}>
|
<Button type="button" variant="secondary" size="sm" onClick={() => csvInputRef.current?.click()}>
|
||||||
<Upload className="w-4 h-4" /> Импорт CSV
|
<Upload className="w-4 h-4" /> Импорт CSV
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" variant="danger" size="sm" onClick={() => { if (confirm('Удалить черновик?')) remove.mutate() }}>
|
<Button type="button" variant="danger" size="sm" onClick={async () => {
|
||||||
|
if (await confirm({
|
||||||
|
title: 'Удалить черновик инвентаризации?',
|
||||||
|
description: <>Удалить черновик № <strong>{existing.data?.number || '—'}</strong>? Действие необратимо.</>,
|
||||||
|
confirmLabel: 'Удалить',
|
||||||
|
})) remove.mutate()
|
||||||
|
}}>
|
||||||
<Trash2 className="w-4 h-4" /> Удалить
|
<Trash2 className="w-4 h-4" /> Удалить
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
|
|
@ -277,11 +286,21 @@ export function InventoryEditPage() {
|
||||||
label="Проведено"
|
label="Проведено"
|
||||||
checked={isPosted}
|
checked={isPosted}
|
||||||
disabled={post.isPending || unpost.isPending || !canPost && !isPosted}
|
disabled={post.isPending || unpost.isPending || !canPost && !isPosted}
|
||||||
onChange={(v) => {
|
onChange={async (v) => {
|
||||||
if (v) {
|
if (v) {
|
||||||
if (confirm('Провести? Учтённые остатки будут скорректированы на разницу.')) post.mutate()
|
if (await confirm({
|
||||||
|
title: 'Провести инвентаризацию?',
|
||||||
|
description: 'Учтённые остатки будут скорректированы на разницу.',
|
||||||
|
confirmLabel: 'Провести',
|
||||||
|
tone: 'warning',
|
||||||
|
})) post.mutate()
|
||||||
} else {
|
} else {
|
||||||
if (confirm('Снять проведение? Корректировки отменятся.')) unpost.mutate()
|
if (await confirm({
|
||||||
|
title: 'Снять проведение?',
|
||||||
|
description: 'Корректировки отменятся.',
|
||||||
|
confirmLabel: 'Снять',
|
||||||
|
tone: 'warning',
|
||||||
|
})) unpost.mutate()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -348,6 +367,7 @@ export function InventoryEditPage() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ConfirmDialog {...dialogProps} />
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import { Button } from '@/components/Button'
|
||||||
import { Field, TextArea, Select, MoneyInput, NumberInput, Checkbox } from '@/components/Field'
|
import { Field, TextArea, Select, MoneyInput, NumberInput, Checkbox } from '@/components/Field'
|
||||||
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 { 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'
|
||||||
import { LossStatus, LossReason, lossReasonLabel, type LossDto, type Product } from '@/lib/types'
|
import { LossStatus, LossReason, lossReasonLabel, type LossDto, type Product } from '@/lib/types'
|
||||||
|
|
@ -48,6 +50,7 @@ export function LossEditPage() {
|
||||||
const stores = useStores()
|
const stores = useStores()
|
||||||
const currencies = useCurrencies()
|
const currencies = useCurrencies()
|
||||||
const org = useOrgSettings()
|
const org = useOrgSettings()
|
||||||
|
const { confirm, dialogProps } = useConfirm()
|
||||||
|
|
||||||
const [form, setForm] = useState<Form>(emptyForm)
|
const [form, setForm] = useState<Form>(emptyForm)
|
||||||
const [pickerOpen, setPickerOpen] = useState(false)
|
const [pickerOpen, setPickerOpen] = useState(false)
|
||||||
|
|
@ -205,7 +208,13 @@ export function LossEditPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3 flex-shrink-0 items-center">
|
<div className="flex gap-3 flex-shrink-0 items-center">
|
||||||
{isDraft && !isNew && (
|
{isDraft && !isNew && (
|
||||||
<Button type="button" variant="danger" size="sm" onClick={() => { if (confirm('Удалить черновик?')) remove.mutate() }}>
|
<Button type="button" variant="danger" size="sm" onClick={async () => {
|
||||||
|
if (await confirm({
|
||||||
|
title: 'Удалить черновик списания?',
|
||||||
|
description: <>Удалить черновик № <strong>{existing.data?.number || '—'}</strong>? Действие необратимо.</>,
|
||||||
|
confirmLabel: 'Удалить',
|
||||||
|
})) remove.mutate()
|
||||||
|
}}>
|
||||||
<Trash2 className="w-4 h-4" /> Удалить
|
<Trash2 className="w-4 h-4" /> Удалить
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
@ -266,11 +275,21 @@ export function LossEditPage() {
|
||||||
label="Проведено"
|
label="Проведено"
|
||||||
checked={isPosted}
|
checked={isPosted}
|
||||||
disabled={post.isPending || unpost.isPending || form.lines.length === 0}
|
disabled={post.isPending || unpost.isPending || form.lines.length === 0}
|
||||||
onChange={(v) => {
|
onChange={async (v) => {
|
||||||
if (v) {
|
if (v) {
|
||||||
if (confirm('Провести? Товар будет списан со склада.')) post.mutate()
|
if (await confirm({
|
||||||
|
title: 'Провести списание?',
|
||||||
|
description: 'Товар будет списан со склада.',
|
||||||
|
confirmLabel: 'Провести',
|
||||||
|
tone: 'warning',
|
||||||
|
})) post.mutate()
|
||||||
} else {
|
} 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)}
|
onClose={() => setPickerOpen(false)}
|
||||||
onPick={(p) => { addLineFromProduct(p); setPickerOpen(false) }}
|
onPick={(p) => { addLineFromProduct(p); setPickerOpen(false) }}
|
||||||
/>
|
/>
|
||||||
|
<ConfirmDialog {...dialogProps} />
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import { SearchBar } from '@/components/SearchBar'
|
||||||
import { Button } from '@/components/Button'
|
import { Button } from '@/components/Button'
|
||||||
import { Modal } from '@/components/Modal'
|
import { Modal } from '@/components/Modal'
|
||||||
import { Field, TextInput, Checkbox } from '@/components/Field'
|
import { Field, TextInput, Checkbox } from '@/components/Field'
|
||||||
|
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
||||||
|
import { useConfirm } from '@/lib/useConfirm'
|
||||||
import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog'
|
import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import type { PriceType } from '@/lib/types'
|
import type { PriceType } from '@/lib/types'
|
||||||
|
|
@ -29,6 +31,7 @@ export function PriceTypesPage() {
|
||||||
const [form, setForm] = useState<Form | null>(null)
|
const [form, setForm] = useState<Form | null>(null)
|
||||||
const [nameErr, setNameErr] = useState<string | null>(null)
|
const [nameErr, setNameErr] = useState<string | null>(null)
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
const { confirm, dialogProps } = useConfirm()
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
if (!form) return
|
if (!form) return
|
||||||
|
|
@ -92,7 +95,11 @@ export function PriceTypesPage() {
|
||||||
<>
|
<>
|
||||||
{form?.id && !form.isSystem && (
|
{form?.id && !form.isSystem && (
|
||||||
<Button variant="danger" size="sm" onClick={async () => {
|
<Button variant="danger" size="sm" onClick={async () => {
|
||||||
if (confirm('Удалить тип цены?')) {
|
if (await confirm({
|
||||||
|
title: 'Удалить тип цены?',
|
||||||
|
description: <>Удалить <strong>«{form.name || 'без названия'}»</strong>? Действие необратимо.</>,
|
||||||
|
confirmLabel: 'Удалить',
|
||||||
|
})) {
|
||||||
await remove.mutateAsync(form.id!)
|
await remove.mutateAsync(form.id!)
|
||||||
await qc.invalidateQueries({ queryKey: ['lookup:price-types'] })
|
await qc.invalidateQueries({ queryKey: ['lookup:price-types'] })
|
||||||
setForm(null); setNameErr(null)
|
setForm(null); setNameErr(null)
|
||||||
|
|
@ -134,6 +141,7 @@ export function PriceTypesPage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<ConfirmDialog {...dialogProps} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import {
|
||||||
import { useOrgSettings } from '@/lib/useOrgSettings'
|
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 { useConfirm } from '@/lib/useConfirm'
|
||||||
import { generateEan13InternalPrefix2, generateBarcode, generateArticle } from '@/lib/barcode'
|
import { generateEan13InternalPrefix2, generateBarcode, generateArticle } from '@/lib/barcode'
|
||||||
|
|
||||||
interface PriceRow { id?: string; priceTypeId: string; amount: number; currencyId: string }
|
interface PriceRow { id?: string; priceTypeId: string; amount: number; currencyId: string }
|
||||||
|
|
@ -60,6 +62,7 @@ export function ProductEditPage() {
|
||||||
const groups = useProductGroups()
|
const groups = useProductGroups()
|
||||||
const countries = useCountries()
|
const countries = useCountries()
|
||||||
const currencies = useCurrencies()
|
const currencies = useCurrencies()
|
||||||
|
const { confirm, dialogProps } = useConfirm()
|
||||||
const priceTypes = usePriceTypes()
|
const priceTypes = usePriceTypes()
|
||||||
const org = useOrgSettings()
|
const org = useOrgSettings()
|
||||||
|
|
||||||
|
|
@ -242,7 +245,13 @@ export function ProductEditPage() {
|
||||||
type="button"
|
type="button"
|
||||||
variant="danger"
|
variant="danger"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => { if (confirm('Удалить товар?')) remove.mutate() }}
|
onClick={async () => {
|
||||||
|
if (await confirm({
|
||||||
|
title: 'Удалить товар?',
|
||||||
|
description: <>Удалить <strong>«{form.name || 'без названия'}»</strong>? Действие необратимо.</>,
|
||||||
|
confirmLabel: 'Удалить',
|
||||||
|
})) remove.mutate()
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" /> Удалить
|
<Trash2 className="w-4 h-4" /> Удалить
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -534,6 +543,7 @@ export function ProductEditPage() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ConfirmDialog {...dialogProps} />
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import { SearchBar } from '@/components/SearchBar'
|
||||||
import { Button } from '@/components/Button'
|
import { Button } from '@/components/Button'
|
||||||
import { Modal } from '@/components/Modal'
|
import { Modal } from '@/components/Modal'
|
||||||
import { Field, TextInput, Select, PercentInput } from '@/components/Field'
|
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 { useCatalogList, useCatalogMutations } from '@/lib/useCatalog'
|
||||||
import type { ProductGroup } from '@/lib/types'
|
import type { ProductGroup } from '@/lib/types'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
|
|
@ -22,6 +24,7 @@ export function ProductGroupsPage() {
|
||||||
const { create, update, remove } = useCatalogMutations(URL, URL)
|
const { create, update, remove } = useCatalogMutations(URL, URL)
|
||||||
const [form, setForm] = useState<Form | null>(null)
|
const [form, setForm] = useState<Form | null>(null)
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
const { confirm, dialogProps } = useConfirm()
|
||||||
// inline-сохранение наценки прямо из таблицы — без открытия модалки.
|
// inline-сохранение наценки прямо из таблицы — без открытия модалки.
|
||||||
const saveMarkup = async (g: ProductGroup, markupPercent: number | null) => {
|
const saveMarkup = async (g: ProductGroup, markupPercent: number | null) => {
|
||||||
await api.put(`/api/catalog/product-groups/${g.id}`, {
|
await api.put(`/api/catalog/product-groups/${g.id}`, {
|
||||||
|
|
@ -102,7 +105,11 @@ export function ProductGroupsPage() {
|
||||||
<>
|
<>
|
||||||
{form?.id && (
|
{form?.id && (
|
||||||
<Button variant="danger" size="sm" onClick={async () => {
|
<Button variant="danger" size="sm" onClick={async () => {
|
||||||
if (confirm('Удалить группу? (должна быть пустой)')) {
|
if (await confirm({
|
||||||
|
title: 'Удалить группу?',
|
||||||
|
description: <>Удалить <strong>«{form.name || 'без названия'}»</strong>? Группа должна быть пустой.</>,
|
||||||
|
confirmLabel: 'Удалить',
|
||||||
|
})) {
|
||||||
try { await remove.mutateAsync(form.id!); setForm(null) }
|
try { await remove.mutateAsync(form.id!); setForm(null) }
|
||||||
catch (e) { alert((e as Error).message) }
|
catch (e) { alert((e as Error).message) }
|
||||||
}
|
}
|
||||||
|
|
@ -151,6 +158,7 @@ export function ProductGroupsPage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<ConfirmDialog {...dialogProps} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import { SearchBar } from '@/components/SearchBar'
|
||||||
import { Button } from '@/components/Button'
|
import { Button } from '@/components/Button'
|
||||||
import { Modal } from '@/components/Modal'
|
import { Modal } from '@/components/Modal'
|
||||||
import { Field, TextInput, Select, Checkbox } from '@/components/Field'
|
import { Field, TextInput, Select, Checkbox } from '@/components/Field'
|
||||||
|
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
||||||
|
import { useConfirm } from '@/lib/useConfirm'
|
||||||
import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog'
|
import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog'
|
||||||
import type { PagedResult, RetailPoint, Store } from '@/lib/types'
|
import type { PagedResult, RetailPoint, Store } from '@/lib/types'
|
||||||
|
|
||||||
|
|
@ -36,6 +38,7 @@ export function RetailPointsPage() {
|
||||||
const { create, update, remove } = useCatalogMutations(URL, URL)
|
const { create, update, remove } = useCatalogMutations(URL, URL)
|
||||||
const [form, setForm] = useState<Form | null>(null)
|
const [form, setForm] = useState<Form | null>(null)
|
||||||
const [nameErr, setNameErr] = useState<string | null>(null)
|
const [nameErr, setNameErr] = useState<string | null>(null)
|
||||||
|
const { confirm, dialogProps } = useConfirm()
|
||||||
|
|
||||||
const stores = useQuery({
|
const stores = useQuery({
|
||||||
queryKey: ['stores-lookup'],
|
queryKey: ['stores-lookup'],
|
||||||
|
|
@ -102,7 +105,11 @@ export function RetailPointsPage() {
|
||||||
<>
|
<>
|
||||||
{form?.id && (
|
{form?.id && (
|
||||||
<Button variant="danger" size="sm" onClick={async () => {
|
<Button variant="danger" size="sm" onClick={async () => {
|
||||||
if (confirm('Удалить кассу?')) {
|
if (await confirm({
|
||||||
|
title: 'Удалить кассу?',
|
||||||
|
description: <>Удалить <strong>«{form.name || 'без названия'}»</strong>? Действие необратимо.</>,
|
||||||
|
confirmLabel: 'Удалить',
|
||||||
|
})) {
|
||||||
await remove.mutateAsync(form.id!)
|
await remove.mutateAsync(form.id!)
|
||||||
setForm(null); setNameErr(null)
|
setForm(null); setNameErr(null)
|
||||||
}
|
}
|
||||||
|
|
@ -152,6 +159,7 @@ export function RetailPointsPage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<ConfirmDialog {...dialogProps} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import { api } from '@/lib/api'
|
||||||
import { Button } from '@/components/Button'
|
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 { 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'
|
||||||
import { RetailSaleStatus, PaymentMethod, type RetailSaleDto, type Product } from '@/lib/types'
|
import { RetailSaleStatus, PaymentMethod, type RetailSaleDto, type Product } from '@/lib/types'
|
||||||
|
|
@ -53,6 +55,7 @@ export function RetailSaleEditPage() {
|
||||||
const stores = useStores()
|
const stores = useStores()
|
||||||
const currencies = useCurrencies()
|
const currencies = useCurrencies()
|
||||||
const org = useOrgSettings()
|
const org = useOrgSettings()
|
||||||
|
const { confirm, dialogProps } = useConfirm()
|
||||||
|
|
||||||
const [form, setForm] = useState<Form>(empty)
|
const [form, setForm] = useState<Form>(empty)
|
||||||
const [pickerOpen, setPickerOpen] = useState(false)
|
const [pickerOpen, setPickerOpen] = useState(false)
|
||||||
|
|
@ -225,7 +228,12 @@ export function RetailSaleEditPage() {
|
||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (!confirm('Создать возврат по этому чеку?')) return
|
if (!await confirm({
|
||||||
|
title: 'Создать возврат?',
|
||||||
|
description: <>Создать возврат по чеку <strong>{existing.data?.number || '—'}</strong>?</>,
|
||||||
|
confirmLabel: 'Создать возврат',
|
||||||
|
tone: 'warning',
|
||||||
|
})) return
|
||||||
const r = await api.post<RetailSaleDto>(`/api/sales/retail/${id}/create-return`)
|
const r = await api.post<RetailSaleDto>(`/api/sales/retail/${id}/create-return`)
|
||||||
navigate(`/sales/retail/${r.data.id}`)
|
navigate(`/sales/retail/${r.data.id}`)
|
||||||
}}
|
}}
|
||||||
|
|
@ -239,7 +247,13 @@ export function RetailSaleEditPage() {
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{isDraft && !isNew && (
|
{isDraft && !isNew && (
|
||||||
<Button type="button" variant="danger" size="sm" onClick={() => { if (confirm('Удалить черновик?')) remove.mutate() }}>
|
<Button type="button" variant="danger" size="sm" onClick={async () => {
|
||||||
|
if (await confirm({
|
||||||
|
title: 'Удалить черновик чека?',
|
||||||
|
description: <>Удалить черновик № <strong>{existing.data?.number || '—'}</strong>? Действие необратимо.</>,
|
||||||
|
confirmLabel: 'Удалить',
|
||||||
|
})) remove.mutate()
|
||||||
|
}}>
|
||||||
<Trash2 className="w-4 h-4" /> Удалить
|
<Trash2 className="w-4 h-4" /> Удалить
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
@ -417,6 +431,7 @@ export function RetailSaleEditPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ProductPicker open={pickerOpen} onClose={() => setPickerOpen(false)} onPick={addLineFromProduct} />
|
<ProductPicker open={pickerOpen} onClose={() => setPickerOpen(false)} onPick={addLineFromProduct} />
|
||||||
|
<ConfirmDialog {...dialogProps} />
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import { SearchBar } from '@/components/SearchBar'
|
||||||
import { Button } from '@/components/Button'
|
import { Button } from '@/components/Button'
|
||||||
import { Modal } from '@/components/Modal'
|
import { Modal } from '@/components/Modal'
|
||||||
import { Field, TextInput, Checkbox } from '@/components/Field'
|
import { Field, TextInput, Checkbox } from '@/components/Field'
|
||||||
|
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
||||||
|
import { useConfirm } from '@/lib/useConfirm'
|
||||||
import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog'
|
import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog'
|
||||||
import { type Store } from '@/lib/types'
|
import { type Store } from '@/lib/types'
|
||||||
|
|
||||||
|
|
@ -33,6 +35,7 @@ export function StoresPage() {
|
||||||
const { create, update, remove } = useCatalogMutations(URL, URL)
|
const { create, update, remove } = useCatalogMutations(URL, URL)
|
||||||
const [form, setForm] = useState<Form | null>(null)
|
const [form, setForm] = useState<Form | null>(null)
|
||||||
const [nameErr, setNameErr] = useState<string | null>(null)
|
const [nameErr, setNameErr] = useState<string | null>(null)
|
||||||
|
const { confirm, dialogProps } = useConfirm()
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
if (!form) return
|
if (!form) return
|
||||||
|
|
@ -87,7 +90,11 @@ export function StoresPage() {
|
||||||
<>
|
<>
|
||||||
{form?.id && (
|
{form?.id && (
|
||||||
<Button variant="danger" size="sm" onClick={async () => {
|
<Button variant="danger" size="sm" onClick={async () => {
|
||||||
if (confirm('Удалить склад?')) {
|
if (await confirm({
|
||||||
|
title: 'Удалить склад?',
|
||||||
|
description: <>Удалить <strong>«{form.name || 'без названия'}»</strong>? Действие необратимо.</>,
|
||||||
|
confirmLabel: 'Удалить',
|
||||||
|
})) {
|
||||||
await remove.mutateAsync(form.id!)
|
await remove.mutateAsync(form.id!)
|
||||||
setForm(null); setNameErr(null)
|
setForm(null); setNameErr(null)
|
||||||
}
|
}
|
||||||
|
|
@ -128,6 +135,7 @@ export function StoresPage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<ConfirmDialog {...dialogProps} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import { SearchBar } from '@/components/SearchBar'
|
||||||
import { Button } from '@/components/Button'
|
import { Button } from '@/components/Button'
|
||||||
import { Modal } from '@/components/Modal'
|
import { Modal } from '@/components/Modal'
|
||||||
import { Field, TextInput } from '@/components/Field'
|
import { Field, TextInput } from '@/components/Field'
|
||||||
|
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
||||||
|
import { useConfirm } from '@/lib/useConfirm'
|
||||||
import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog'
|
import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog'
|
||||||
import type { UnitOfMeasure } from '@/lib/types'
|
import type { UnitOfMeasure } from '@/lib/types'
|
||||||
|
|
||||||
|
|
@ -25,6 +27,7 @@ export function SuperAdminUnitsOfMeasurePage() {
|
||||||
const { create, update, remove } = useCatalogMutations(URL, URL)
|
const { create, update, remove } = useCatalogMutations(URL, URL)
|
||||||
const [form, setForm] = useState<Form | null>(null)
|
const [form, setForm] = useState<Form | null>(null)
|
||||||
const [submitError, setSubmitError] = useState<string | null>(null)
|
const [submitError, setSubmitError] = useState<string | null>(null)
|
||||||
|
const { confirm, dialogProps } = useConfirm()
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
if (!form) return
|
if (!form) return
|
||||||
|
|
@ -43,7 +46,11 @@ export function SuperAdminUnitsOfMeasurePage() {
|
||||||
|
|
||||||
const onDelete = async () => {
|
const onDelete = async () => {
|
||||||
if (!form?.id) return
|
if (!form?.id) return
|
||||||
if (!confirm('Деактивировать единицу? Если на неё ссылаются товары или орги — операция не пройдёт.')) return
|
if (!await confirm({
|
||||||
|
title: 'Деактивировать единицу?',
|
||||||
|
description: <>Деактивировать <strong>«{form.name || 'без названия'}»</strong>? Если на неё ссылаются товары или орги — операция не пройдёт.</>,
|
||||||
|
confirmLabel: 'Деактивировать',
|
||||||
|
})) return
|
||||||
try {
|
try {
|
||||||
await remove.mutateAsync(form.id)
|
await remove.mutateAsync(form.id)
|
||||||
setForm(null)
|
setForm(null)
|
||||||
|
|
@ -125,6 +132,7 @@ export function SuperAdminUnitsOfMeasurePage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<ConfirmDialog {...dialogProps} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import { Button } from '@/components/Button'
|
||||||
import { Field, TextArea, Select, AsyncSelect, MoneyInput, NumberInput, Checkbox } from '@/components/Field'
|
import { Field, TextArea, Select, AsyncSelect, MoneyInput, NumberInput, Checkbox } from '@/components/Field'
|
||||||
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 { 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'
|
||||||
import { SupplierReturnStatus, type SupplierReturnDto, type Product } from '@/lib/types'
|
import { SupplierReturnStatus, type SupplierReturnDto, type Product } from '@/lib/types'
|
||||||
|
|
@ -50,6 +52,7 @@ export function SupplierReturnEditPage() {
|
||||||
const stores = useStores()
|
const stores = useStores()
|
||||||
const currencies = useCurrencies()
|
const currencies = useCurrencies()
|
||||||
const org = useOrgSettings()
|
const org = useOrgSettings()
|
||||||
|
const { confirm, dialogProps } = useConfirm()
|
||||||
|
|
||||||
const [form, setForm] = useState<Form>(emptyForm)
|
const [form, setForm] = useState<Form>(emptyForm)
|
||||||
const [pickerOpen, setPickerOpen] = useState(false)
|
const [pickerOpen, setPickerOpen] = useState(false)
|
||||||
|
|
@ -208,7 +211,13 @@ export function SupplierReturnEditPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3 flex-shrink-0 items-center">
|
<div className="flex gap-3 flex-shrink-0 items-center">
|
||||||
{isDraft && !isNew && (
|
{isDraft && !isNew && (
|
||||||
<Button type="button" variant="danger" size="sm" onClick={() => { if (confirm('Удалить черновик?')) remove.mutate() }}>
|
<Button type="button" variant="danger" size="sm" onClick={async () => {
|
||||||
|
if (await confirm({
|
||||||
|
title: 'Удалить черновик возврата?',
|
||||||
|
description: <>Удалить черновик № <strong>{existing.data?.number || '—'}</strong>? Действие необратимо.</>,
|
||||||
|
confirmLabel: 'Удалить',
|
||||||
|
})) remove.mutate()
|
||||||
|
}}>
|
||||||
<Trash2 className="w-4 h-4" /> Удалить
|
<Trash2 className="w-4 h-4" /> Удалить
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
@ -270,11 +279,21 @@ export function SupplierReturnEditPage() {
|
||||||
label="Проведено"
|
label="Проведено"
|
||||||
checked={isPosted}
|
checked={isPosted}
|
||||||
disabled={post.isPending || unpost.isPending || form.lines.length === 0}
|
disabled={post.isPending || unpost.isPending || form.lines.length === 0}
|
||||||
onChange={(v) => {
|
onChange={async (v) => {
|
||||||
if (v) {
|
if (v) {
|
||||||
if (confirm('Провести? Товар спишется со склада в адрес поставщика.')) post.mutate()
|
if (await confirm({
|
||||||
|
title: 'Провести возврат поставщику?',
|
||||||
|
description: 'Товар спишется со склада в адрес поставщика.',
|
||||||
|
confirmLabel: 'Провести',
|
||||||
|
tone: 'warning',
|
||||||
|
})) post.mutate()
|
||||||
} else {
|
} 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)}
|
onClose={() => setPickerOpen(false)}
|
||||||
onPick={(p) => { addLineFromProduct(p); setPickerOpen(false) }}
|
onPick={(p) => { addLineFromProduct(p); setPickerOpen(false) }}
|
||||||
/>
|
/>
|
||||||
|
<ConfirmDialog {...dialogProps} />
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ import { Field, TextArea, Select, AsyncSelect, Checkbox, MoneyInput, NumberInput
|
||||||
import { DateField } from '@/components/DateField'
|
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 { 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'
|
||||||
import { SupplyStatus, type SupplyDto, type Product } from '@/lib/types'
|
import { SupplyStatus, type SupplyDto, type Product } from '@/lib/types'
|
||||||
|
|
@ -64,6 +66,7 @@ export function SupplyEditPage() {
|
||||||
const currencies = useCurrencies()
|
const currencies = useCurrencies()
|
||||||
const org = useOrgSettings()
|
const org = useOrgSettings()
|
||||||
const priceTypes = usePriceTypes()
|
const priceTypes = usePriceTypes()
|
||||||
|
const { confirm, dialogProps } = useConfirm()
|
||||||
// Системный (главный) тип цен — на нём по умолчанию ведётся розница на кассе.
|
// Системный (главный) тип цен — на нём по умолчанию ведётся розница на кассе.
|
||||||
// Заголовок колонки «Розничная» подменяется его именем чтобы соответствовать
|
// Заголовок колонки «Розничная» подменяется его именем чтобы соответствовать
|
||||||
// тому, что увидит пользователь в карточке товара и в справочнике типов цен.
|
// тому, что увидит пользователь в карточке товара и в справочнике типов цен.
|
||||||
|
|
@ -282,7 +285,13 @@ export function SupplyEditPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3 flex-shrink-0 items-center">
|
<div className="flex gap-3 flex-shrink-0 items-center">
|
||||||
{isDraft && !isNew && (
|
{isDraft && !isNew && (
|
||||||
<Button type="button" variant="danger" size="sm" onClick={() => { if (confirm('Удалить черновик приёмки?')) remove.mutate() }}>
|
<Button type="button" variant="danger" size="sm" onClick={async () => {
|
||||||
|
if (await confirm({
|
||||||
|
title: 'Удалить черновик приёмки?',
|
||||||
|
description: <>Удалить черновик № <strong>{existing.data?.number || '—'}</strong>? Действие необратимо.</>,
|
||||||
|
confirmLabel: 'Удалить',
|
||||||
|
})) remove.mutate()
|
||||||
|
}}>
|
||||||
<Trash2 className="w-4 h-4" /> Удалить
|
<Trash2 className="w-4 h-4" /> Удалить
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
@ -357,11 +366,21 @@ export function SupplyEditPage() {
|
||||||
checked={isPosted}
|
checked={isPosted}
|
||||||
disabled={post.isPending || unpost.isPending || form.lines.length === 0
|
disabled={post.isPending || unpost.isPending || form.lines.length === 0
|
||||||
|| form.lines.some(l => l.quantity <= 0 || l.unitPrice <= 0)}
|
|| form.lines.some(l => l.quantity <= 0 || l.unitPrice <= 0)}
|
||||||
onChange={(v) => {
|
onChange={async (v) => {
|
||||||
if (v) {
|
if (v) {
|
||||||
if (confirm('После проведения товары будут оприходованы на склад и обновят себестоимость (скользящее среднее). Продолжить?')) post.mutate()
|
if (await confirm({
|
||||||
|
title: 'Провести приёмку?',
|
||||||
|
description: 'После проведения товары будут оприходованы на склад и обновят себестоимость (скользящее среднее).',
|
||||||
|
confirmLabel: 'Провести',
|
||||||
|
tone: 'warning',
|
||||||
|
})) post.mutate()
|
||||||
} else {
|
} else {
|
||||||
if (confirm('Снять проведение? Остатки откатятся, себестоимость останется (пересчитать вручную при необходимости).')) unpost.mutate()
|
if (await confirm({
|
||||||
|
title: 'Снять проведение?',
|
||||||
|
description: 'Остатки откатятся, себестоимость останется (пересчитать вручную при необходимости).',
|
||||||
|
confirmLabel: 'Снять',
|
||||||
|
tone: 'warning',
|
||||||
|
})) unpost.mutate()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -492,6 +511,7 @@ export function SupplyEditPage() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ProductPicker open={pickerOpen} onClose={() => setPickerOpen(false)} onPick={addLineFromProduct} />
|
<ProductPicker open={pickerOpen} onClose={() => setPickerOpen(false)} onPick={addLineFromProduct} />
|
||||||
|
<ConfirmDialog {...dialogProps} />
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import { Button } from '@/components/Button'
|
||||||
import { Field, TextArea, Select, MoneyInput, NumberInput, Checkbox } from '@/components/Field'
|
import { Field, TextArea, Select, MoneyInput, NumberInput, Checkbox } from '@/components/Field'
|
||||||
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 { useConfirm } from '@/lib/useConfirm'
|
||||||
import { useStores } from '@/lib/useLookups'
|
import { useStores } from '@/lib/useLookups'
|
||||||
import { useOrgSettings } from '@/lib/useOrgSettings'
|
import { useOrgSettings } from '@/lib/useOrgSettings'
|
||||||
import { TransferStatus, type TransferDto, type Product } from '@/lib/types'
|
import { TransferStatus, type TransferDto, type Product } from '@/lib/types'
|
||||||
|
|
@ -46,6 +48,7 @@ export function TransferEditPage() {
|
||||||
|
|
||||||
const stores = useStores()
|
const stores = useStores()
|
||||||
const org = useOrgSettings()
|
const org = useOrgSettings()
|
||||||
|
const { confirm, dialogProps } = useConfirm()
|
||||||
|
|
||||||
const [form, setForm] = useState<Form>(emptyForm)
|
const [form, setForm] = useState<Form>(emptyForm)
|
||||||
const [pickerOpen, setPickerOpen] = useState(false)
|
const [pickerOpen, setPickerOpen] = useState(false)
|
||||||
|
|
@ -190,7 +193,13 @@ export function TransferEditPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3 flex-shrink-0 items-center">
|
<div className="flex gap-3 flex-shrink-0 items-center">
|
||||||
{isDraft && !isNew && (
|
{isDraft && !isNew && (
|
||||||
<Button type="button" variant="danger" size="sm" onClick={() => { if (confirm('Удалить черновик?')) remove.mutate() }}>
|
<Button type="button" variant="danger" size="sm" onClick={async () => {
|
||||||
|
if (await confirm({
|
||||||
|
title: 'Удалить черновик перемещения?',
|
||||||
|
description: <>Удалить черновик № <strong>{existing.data?.number || '—'}</strong>? Действие необратимо.</>,
|
||||||
|
confirmLabel: 'Удалить',
|
||||||
|
})) remove.mutate()
|
||||||
|
}}>
|
||||||
<Trash2 className="w-4 h-4" /> Удалить
|
<Trash2 className="w-4 h-4" /> Удалить
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
@ -248,11 +257,21 @@ export function TransferEditPage() {
|
||||||
label="Проведено"
|
label="Проведено"
|
||||||
checked={isPosted}
|
checked={isPosted}
|
||||||
disabled={post.isPending || unpost.isPending || form.lines.length === 0}
|
disabled={post.isPending || unpost.isPending || form.lines.length === 0}
|
||||||
onChange={(v) => {
|
onChange={async (v) => {
|
||||||
if (v) {
|
if (v) {
|
||||||
if (confirm('Провести? Товар спишется с первого склада и встанет на второй.')) post.mutate()
|
if (await confirm({
|
||||||
|
title: 'Провести перемещение?',
|
||||||
|
description: 'Товар спишется с первого склада и встанет на второй.',
|
||||||
|
confirmLabel: 'Провести',
|
||||||
|
tone: 'warning',
|
||||||
|
})) post.mutate()
|
||||||
} else {
|
} 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)}
|
onClose={() => setPickerOpen(false)}
|
||||||
onPick={(p) => { addLineFromProduct(p); setPickerOpen(false) }}
|
onPick={(p) => { addLineFromProduct(p); setPickerOpen(false) }}
|
||||||
/>
|
/>
|
||||||
|
<ConfirmDialog {...dialogProps} />
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue