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 { useReadOnly } from '@/lib/useReadOnly'
|
||||
|
||||
|
|
@ -27,7 +27,10 @@ const sizes: Record<Size, string> = {
|
|||
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()
|
||||
// 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 (
|
||||
<button
|
||||
ref={ref}
|
||||
{...rest}
|
||||
disabled={disabled || blocked}
|
||||
title={blocked ? ro.reason : title}
|
||||
|
|
@ -51,4 +55,4 @@ export function Button({ variant = 'primary', size = 'md', mutating, className,
|
|||
{children}
|
||||
</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 { 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<HTMLInputElement>(null)
|
||||
const [lightboxIdx, setLightboxIdx] = useState<number | null>(null)
|
||||
const { confirm, dialogProps } = useConfirm()
|
||||
|
||||
const url = `/api/catalog/products/${productId}/images`
|
||||
|
||||
|
|
@ -105,7 +108,14 @@ export function ProductImageGallery({ productId }: Props) {
|
|||
)}
|
||||
<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"
|
||||
title="Удалить"
|
||||
>
|
||||
|
|
@ -159,6 +169,7 @@ export function ProductImageGallery({ productId }: Props) {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ConfirmDialog {...dialogProps} />
|
||||
</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 { Field, TextInput, TextArea, Select } from '@/components/Field'
|
||||
import { PhoneInput } from '@/components/PhoneInput'
|
||||
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
||||
import { useConfirm } from '@/lib/useConfirm'
|
||||
import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog'
|
||||
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 [form, setForm] = useState<Form | null>(null)
|
||||
const [fieldErrors, setFieldErrors] = useState<Partial<Record<'name' | 'email' | 'phone', string>>>({})
|
||||
const { confirm, dialogProps } = useConfirm()
|
||||
|
||||
const countries = useQuery({
|
||||
queryKey: ['countries-lookup'],
|
||||
|
|
@ -117,7 +120,11 @@ export function CounterpartiesPage() {
|
|||
<>
|
||||
{form?.id && (
|
||||
<Button variant="danger" size="sm" onClick={async () => {
|
||||
if (confirm('Удалить контрагента?')) {
|
||||
if (await confirm({
|
||||
title: 'Удалить контрагента?',
|
||||
description: <>Удалить <strong>«{form.name || 'без названия'}»</strong>? Действие необратимо.</>,
|
||||
confirmLabel: 'Удалить',
|
||||
})) {
|
||||
await remove.mutateAsync(form.id!)
|
||||
setForm(null); setFieldErrors({})
|
||||
}
|
||||
|
|
@ -191,6 +198,7 @@ export function CounterpartiesPage() {
|
|||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
<ConfirmDialog {...dialogProps} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import { SearchBar } from '@/components/SearchBar'
|
|||
import { Button } from '@/components/Button'
|
||||
import { Modal } from '@/components/Modal'
|
||||
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 { useCurrencies } from '@/lib/useLookups'
|
||||
import type { Country } from '@/lib/types'
|
||||
|
|
@ -28,6 +30,7 @@ export function CountriesPage() {
|
|||
const { create, update, remove } = useCatalogMutations(URL, URL)
|
||||
const currencies = useCurrencies()
|
||||
const [form, setForm] = useState<Form | null>(null)
|
||||
const { confirm, dialogProps } = useConfirm()
|
||||
|
||||
const save = async () => {
|
||||
if (!form) return
|
||||
|
|
@ -80,7 +83,11 @@ export function CountriesPage() {
|
|||
<>
|
||||
{form?.id && (
|
||||
<Button variant="danger" size="sm" onClick={async () => {
|
||||
if (confirm('Удалить страну?')) {
|
||||
if (await confirm({
|
||||
title: 'Удалить страну?',
|
||||
description: <>Удалить <strong>«{form.name || 'без названия'}»</strong>? Действие необратимо.</>,
|
||||
confirmLabel: 'Удалить',
|
||||
})) {
|
||||
await remove.mutateAsync(form.id!)
|
||||
setForm(null)
|
||||
}
|
||||
|
|
@ -121,6 +128,7 @@ export function CountriesPage() {
|
|||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
<ConfirmDialog {...dialogProps} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import { Button } from '@/components/Button'
|
|||
import { Field, TextArea, Select, AsyncSelect, 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 {
|
||||
|
|
@ -57,6 +59,7 @@ export function DemandEditPage() {
|
|||
const stores = useStores()
|
||||
const currencies = useCurrencies()
|
||||
const org = useOrgSettings()
|
||||
const { confirm, dialogProps } = useConfirm()
|
||||
|
||||
const [form, setForm] = useState<Form>(emptyForm)
|
||||
const [pickerOpen, setPickerOpen] = useState(false)
|
||||
|
|
@ -222,7 +225,13 @@ export function DemandEditPage() {
|
|||
</div>
|
||||
<div className="flex gap-3 flex-shrink-0 items-center">
|
||||
{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" /> Удалить
|
||||
</Button>
|
||||
)}
|
||||
|
|
@ -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) }}
|
||||
/>
|
||||
<ConfirmDialog {...dialogProps} />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string>('blank')
|
||||
const { confirm, dialogProps } = useConfirm()
|
||||
|
||||
const save = async () => {
|
||||
if (!form) return
|
||||
|
|
@ -244,7 +247,11 @@ export function EmployeeRolesPage() {
|
|||
<>
|
||||
{form?.id && !form.isSystem && (
|
||||
<Button variant="danger" size="sm" onClick={async () => {
|
||||
if (confirm('Удалить роль?')) {
|
||||
if (await confirm({
|
||||
title: 'Удалить роль?',
|
||||
description: <>Удалить <strong>«{form.name || 'без названия'}»</strong>? Действие необратимо.</>,
|
||||
confirmLabel: 'Удалить',
|
||||
})) {
|
||||
await remove.mutateAsync(form.id!)
|
||||
setForm(null); setNameErr(null)
|
||||
}
|
||||
|
|
@ -304,6 +311,7 @@ export function EmployeeRolesPage() {
|
|||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
<ConfirmDialog {...dialogProps} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import { Button } from '@/components/Button'
|
|||
import { Modal } from '@/components/Modal'
|
||||
import { Field, TextInput, TextArea, Checkbox, MoneyInput } from '@/components/Field'
|
||||
import { PhoneInput } from '@/components/PhoneInput'
|
||||
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
||||
import { useConfirm } from '@/lib/useConfirm'
|
||||
import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog'
|
||||
import type { PagedResult, RetailPoint } from '@/lib/types'
|
||||
import type { EmployeeRoleDto } from '@/pages/EmployeeRolesPage'
|
||||
|
|
@ -93,6 +95,7 @@ export function EmployeesPage() {
|
|||
// над любой ошибкой 4xx/5xx с сервера.
|
||||
const [blockedDelete, setBlockedDelete] = useState<{ title: string; body: string } | null>(null)
|
||||
const [fieldErrors, setFieldErrors] = useState<Partial<Record<'lastName' | 'firstName' | 'email' | 'phone', string>>>({})
|
||||
const { confirm, dialogProps } = useConfirm()
|
||||
|
||||
const roles = useQuery({
|
||||
queryKey: ['employee-roles-lookup'],
|
||||
|
|
@ -283,10 +286,20 @@ export function EmployeesPage() {
|
|||
return
|
||||
}
|
||||
const fullName = `${activeEmployee.lastName ?? ''} ${activeEmployee.firstName ?? ''}`.trim()
|
||||
const confirmText = activeEmployee.status === 'active'
|
||||
? `Уволить сотрудника «${fullName}»?\n\nЕго учётная запись потеряет доступ, документы и история останутся в системе. Восстановить можно в любой момент.`
|
||||
: `Удалить запись о сотруднике «${fullName}»?\n\nСотрудник уже уволен. После удаления он скрывается из обычных списков, но во всех связанных документах остаётся подпись «${fullName} (удалён)».`
|
||||
if (!confirm(confirmText)) return
|
||||
const confirmReq = activeEmployee.status === 'active'
|
||||
? {
|
||||
title: 'Уволить сотрудника?',
|
||||
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 {
|
||||
await remove.mutateAsync(form.id!)
|
||||
list.refetch?.()
|
||||
|
|
@ -476,6 +489,7 @@ export function EmployeesPage() {
|
|||
{blockedDelete?.body}
|
||||
</p>
|
||||
</Modal>
|
||||
<ConfirmDialog {...dialogProps} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 { EnterStatus, type EnterDto, type Product } from '@/lib/types'
|
||||
|
|
@ -49,6 +51,7 @@ export function EnterEditPage() {
|
|||
const stores = useStores()
|
||||
const currencies = useCurrencies()
|
||||
const org = useOrgSettings()
|
||||
const { confirm, dialogProps } = useConfirm()
|
||||
|
||||
const [form, setForm] = useState<Form>(emptyForm)
|
||||
const [pickerOpen, setPickerOpen] = useState(false)
|
||||
|
|
@ -202,7 +205,13 @@ export function EnterEditPage() {
|
|||
</div>
|
||||
<div className="flex gap-3 flex-shrink-0 items-center">
|
||||
{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" /> Удалить
|
||||
</Button>
|
||||
)}
|
||||
|
|
@ -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) }}
|
||||
/>
|
||||
<ConfirmDialog {...dialogProps} />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Form>(emptyForm)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const csvInputRef = useRef<HTMLInputElement>(null)
|
||||
|
|
@ -223,7 +226,13 @@ export function InventoryEditPage() {
|
|||
<Button type="button" variant="secondary" size="sm" onClick={() => csvInputRef.current?.click()}>
|
||||
<Upload className="w-4 h-4" /> Импорт CSV
|
||||
</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" /> Удалить
|
||||
</Button>
|
||||
</>
|
||||
|
|
@ -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() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmDialog {...dialogProps} />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Form>(emptyForm)
|
||||
const [pickerOpen, setPickerOpen] = useState(false)
|
||||
|
|
@ -205,7 +208,13 @@ export function LossEditPage() {
|
|||
</div>
|
||||
<div className="flex gap-3 flex-shrink-0 items-center">
|
||||
{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" /> Удалить
|
||||
</Button>
|
||||
)}
|
||||
|
|
@ -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) }}
|
||||
/>
|
||||
<ConfirmDialog {...dialogProps} />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Form | null>(null)
|
||||
const [nameErr, setNameErr] = useState<string | null>(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 && (
|
||||
<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 qc.invalidateQueries({ queryKey: ['lookup:price-types'] })
|
||||
setForm(null); setNameErr(null)
|
||||
|
|
@ -134,6 +141,7 @@ export function PriceTypesPage() {
|
|||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
<ConfirmDialog {...dialogProps} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: <>Удалить <strong>«{form.name || 'без названия'}»</strong>? Действие необратимо.</>,
|
||||
confirmLabel: 'Удалить',
|
||||
})) remove.mutate()
|
||||
}}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" /> Удалить
|
||||
</Button>
|
||||
|
|
@ -534,6 +543,7 @@ export function ProductEditPage() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmDialog {...dialogProps} />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Form | null>(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 && (
|
||||
<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) }
|
||||
catch (e) { alert((e as Error).message) }
|
||||
}
|
||||
|
|
@ -151,6 +158,7 @@ export function ProductGroupsPage() {
|
|||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
<ConfirmDialog {...dialogProps} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import { SearchBar } from '@/components/SearchBar'
|
|||
import { Button } from '@/components/Button'
|
||||
import { Modal } from '@/components/Modal'
|
||||
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 type { PagedResult, RetailPoint, Store } from '@/lib/types'
|
||||
|
||||
|
|
@ -36,6 +38,7 @@ export function RetailPointsPage() {
|
|||
const { create, update, remove } = useCatalogMutations(URL, URL)
|
||||
const [form, setForm] = useState<Form | null>(null)
|
||||
const [nameErr, setNameErr] = useState<string | null>(null)
|
||||
const { confirm, dialogProps } = useConfirm()
|
||||
|
||||
const stores = useQuery({
|
||||
queryKey: ['stores-lookup'],
|
||||
|
|
@ -102,7 +105,11 @@ export function RetailPointsPage() {
|
|||
<>
|
||||
{form?.id && (
|
||||
<Button variant="danger" size="sm" onClick={async () => {
|
||||
if (confirm('Удалить кассу?')) {
|
||||
if (await confirm({
|
||||
title: 'Удалить кассу?',
|
||||
description: <>Удалить <strong>«{form.name || 'без названия'}»</strong>? Действие необратимо.</>,
|
||||
confirmLabel: 'Удалить',
|
||||
})) {
|
||||
await remove.mutateAsync(form.id!)
|
||||
setForm(null); setNameErr(null)
|
||||
}
|
||||
|
|
@ -152,6 +159,7 @@ export function RetailPointsPage() {
|
|||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
<ConfirmDialog {...dialogProps} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { api } from '@/lib/api'
|
|||
import { Button } from '@/components/Button'
|
||||
import { Field, TextInput, TextArea, Select, AsyncSelect, MoneyInput, NumberInput } from '@/components/Field'
|
||||
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 { RetailSaleStatus, PaymentMethod, type RetailSaleDto, type Product } from '@/lib/types'
|
||||
|
|
@ -53,6 +55,7 @@ export function RetailSaleEditPage() {
|
|||
const stores = useStores()
|
||||
const currencies = useCurrencies()
|
||||
const org = useOrgSettings()
|
||||
const { confirm, dialogProps } = useConfirm()
|
||||
|
||||
const [form, setForm] = useState<Form>(empty)
|
||||
const [pickerOpen, setPickerOpen] = useState(false)
|
||||
|
|
@ -225,7 +228,12 @@ export function RetailSaleEditPage() {
|
|||
type="button"
|
||||
variant="secondary"
|
||||
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`)
|
||||
navigate(`/sales/retail/${r.data.id}`)
|
||||
}}
|
||||
|
|
@ -239,7 +247,13 @@ export function RetailSaleEditPage() {
|
|||
</Button>
|
||||
)}
|
||||
{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" /> Удалить
|
||||
</Button>
|
||||
)}
|
||||
|
|
@ -417,6 +431,7 @@ export function RetailSaleEditPage() {
|
|||
</div>
|
||||
|
||||
<ProductPicker open={pickerOpen} onClose={() => setPickerOpen(false)} onPick={addLineFromProduct} />
|
||||
<ConfirmDialog {...dialogProps} />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Form | null>(null)
|
||||
const [nameErr, setNameErr] = useState<string | null>(null)
|
||||
const { confirm, dialogProps } = useConfirm()
|
||||
|
||||
const save = async () => {
|
||||
if (!form) return
|
||||
|
|
@ -87,7 +90,11 @@ export function StoresPage() {
|
|||
<>
|
||||
{form?.id && (
|
||||
<Button variant="danger" size="sm" onClick={async () => {
|
||||
if (confirm('Удалить склад?')) {
|
||||
if (await confirm({
|
||||
title: 'Удалить склад?',
|
||||
description: <>Удалить <strong>«{form.name || 'без названия'}»</strong>? Действие необратимо.</>,
|
||||
confirmLabel: 'Удалить',
|
||||
})) {
|
||||
await remove.mutateAsync(form.id!)
|
||||
setForm(null); setNameErr(null)
|
||||
}
|
||||
|
|
@ -128,6 +135,7 @@ export function StoresPage() {
|
|||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
<ConfirmDialog {...dialogProps} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import { SearchBar } from '@/components/SearchBar'
|
|||
import { Button } from '@/components/Button'
|
||||
import { Modal } from '@/components/Modal'
|
||||
import { Field, TextInput } from '@/components/Field'
|
||||
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
||||
import { useConfirm } from '@/lib/useConfirm'
|
||||
import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog'
|
||||
import type { UnitOfMeasure } from '@/lib/types'
|
||||
|
||||
|
|
@ -25,6 +27,7 @@ export function SuperAdminUnitsOfMeasurePage() {
|
|||
const { create, update, remove } = useCatalogMutations(URL, URL)
|
||||
const [form, setForm] = useState<Form | null>(null)
|
||||
const [submitError, setSubmitError] = useState<string | null>(null)
|
||||
const { confirm, dialogProps } = useConfirm()
|
||||
|
||||
const save = async () => {
|
||||
if (!form) return
|
||||
|
|
@ -43,7 +46,11 @@ export function SuperAdminUnitsOfMeasurePage() {
|
|||
|
||||
const onDelete = async () => {
|
||||
if (!form?.id) return
|
||||
if (!confirm('Деактивировать единицу? Если на неё ссылаются товары или орги — операция не пройдёт.')) return
|
||||
if (!await confirm({
|
||||
title: 'Деактивировать единицу?',
|
||||
description: <>Деактивировать <strong>«{form.name || 'без названия'}»</strong>? Если на неё ссылаются товары или орги — операция не пройдёт.</>,
|
||||
confirmLabel: 'Деактивировать',
|
||||
})) return
|
||||
try {
|
||||
await remove.mutateAsync(form.id)
|
||||
setForm(null)
|
||||
|
|
@ -125,6 +132,7 @@ export function SuperAdminUnitsOfMeasurePage() {
|
|||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
<ConfirmDialog {...dialogProps} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import { Button } from '@/components/Button'
|
|||
import { Field, TextArea, Select, AsyncSelect, 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 { SupplierReturnStatus, type SupplierReturnDto, type Product } from '@/lib/types'
|
||||
|
|
@ -50,6 +52,7 @@ export function SupplierReturnEditPage() {
|
|||
const stores = useStores()
|
||||
const currencies = useCurrencies()
|
||||
const org = useOrgSettings()
|
||||
const { confirm, dialogProps } = useConfirm()
|
||||
|
||||
const [form, setForm] = useState<Form>(emptyForm)
|
||||
const [pickerOpen, setPickerOpen] = useState(false)
|
||||
|
|
@ -208,7 +211,13 @@ export function SupplierReturnEditPage() {
|
|||
</div>
|
||||
<div className="flex gap-3 flex-shrink-0 items-center">
|
||||
{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" /> Удалить
|
||||
</Button>
|
||||
)}
|
||||
|
|
@ -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) }}
|
||||
/>
|
||||
<ConfirmDialog {...dialogProps} />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</div>
|
||||
<div className="flex gap-3 flex-shrink-0 items-center">
|
||||
{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" /> Удалить
|
||||
</Button>
|
||||
)}
|
||||
|
|
@ -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() {
|
|||
)}
|
||||
|
||||
<ProductPicker open={pickerOpen} onClose={() => setPickerOpen(false)} onPick={addLineFromProduct} />
|
||||
<ConfirmDialog {...dialogProps} />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Form>(emptyForm)
|
||||
const [pickerOpen, setPickerOpen] = useState(false)
|
||||
|
|
@ -190,7 +193,13 @@ export function TransferEditPage() {
|
|||
</div>
|
||||
<div className="flex gap-3 flex-shrink-0 items-center">
|
||||
{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" /> Удалить
|
||||
</Button>
|
||||
)}
|
||||
|
|
@ -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) }}
|
||||
/>
|
||||
<ConfirmDialog {...dialogProps} />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue