feat(s29): простой режим — упрощённые формы товара/контрагента/приёмки (шаг 5/8)
Везде формула: simpleMode = me && !me.advancedMode && !SuperAdmin. Значения скрытых полей в state сохраняются и при save на сервер отправляются как есть; данные НЕ стираются. - ProductEditPage: скрыты Артикул (auto-gen), Описание, Страна, Эталонная цена, Ставка НДС, «В т.ч. НДС», Валюта закупки, «Услуга», «Маркируемый», Максимальный остаток, не-retail типы цен в блоке «Цены продажи». - CounterpartiesPage: скрыты Юр.название, Страна, Email, Банк, Р.счёт, БИК, Контактное лицо. Остаются: Название, Тип лица, БИН/ИИН, Адрес, Телефон, Заметки. - SupplyEditPage: колонка «Валюта» всегда скрыта в простом режиме (одна валюта по умолчанию). «Поставщик/Покупатель»-галочки отложены — требуют новых колонок IsSupplier/IsBuyer на Counterparty (новая функциональность). pnpm run build — ✓ 2.36s, 0 TS errors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
67c1b590ad
commit
792ea0fd41
|
|
@ -13,7 +13,7 @@ SuperAdmin и только для конкретной организации.
|
||||||
- [x] 2. API `PUT /api/super-admin/organizations/{id}/advanced-mode` + аудит + `/api/me.advancedMode`; org-admin → 403
|
- [x] 2. API `PUT /api/super-admin/organizations/{id}/advanced-mode` + аудит + `/api/me.advancedMode`; org-admin → 403
|
||||||
- [x] 3. Серверная защита: `[RequiresAdvancedMode]` → 403 ProblemDetails на скрытых модулях
|
- [x] 3. Серверная защита: `[RequiresAdvancedMode]` → 403 ProblemDetails на скрытых модулях
|
||||||
- [x] 4. Web: меню, маршруты, дашборд фильтруются; прямой переход → страница «Недоступно»
|
- [x] 4. Web: меню, маршруты, дашборд фильтруются; прямой переход → страница «Недоступно»
|
||||||
- [ ] 5. Упрощённые формы (товар / контрагент / документы) — скрыть «не нужные» поля, значения сохраняются
|
- [x] 5. Упрощённые формы (товар / контрагент / документы) — скрыть «не нужные» поля, значения сохраняются
|
||||||
- [ ] 6. SuperAdmin UI — бейдж «Простой/Расширенный» + переключатель с подтверждением
|
- [ ] 6. SuperAdmin UI — бейдж «Простой/Расширенный» + переключатель с подтверждением
|
||||||
- [ ] 7. Тесты (unit + integration + e2e); патч quality-watchdog при необходимости
|
- [ ] 7. Тесты (unit + integration + e2e); патч quality-watchdog при необходимости
|
||||||
- [ ] 8. Деплой стейджа + `docs/simple-mode.md`
|
- [ ] 8. Деплой стейджа + `docs/simple-mode.md`
|
||||||
|
|
@ -108,3 +108,26 @@ SuperAdmin и только для конкретной организации.
|
||||||
- `DashboardPage` — в простом режиме скрыты виджеты «Топ товаров» и «Маржа». Остаются KPI (выручка today/week/month, средний чек), график продаж, «Последние продажи», «Низкий остаток» — ровно то, что в чек-листе.
|
- `DashboardPage` — в простом режиме скрыты виджеты «Топ товаров» и «Маржа». Остаются KPI (выручка today/week/month, средний чек), график продаж, «Последние продажи», «Низкий остаток» — ровно то, что в чек-листе.
|
||||||
- Данные НЕ удаляются при выключении режима: это только UI-гейтинг + серверный 403; при повторном включении всё видно.
|
- Данные НЕ удаляются при выключении режима: это только UI-гейтинг + серверный 403; при повторном включении всё видно.
|
||||||
- `pnpm run build` — ✓ (2.60s, 0 TS errors).
|
- `pnpm run build` — ✓ (2.60s, 0 TS errors).
|
||||||
|
|
||||||
|
### 2026-10-04 — Шаг 5 ✓
|
||||||
|
|
||||||
|
- Везде формула простого режима: `simpleMode = me && !me.advancedMode && !roles.includes('SuperAdmin')`. SuperAdmin не ощущает — ему доступно всё.
|
||||||
|
- **Карточка товара** (`ProductEditPage.tsx`) в простом режиме прячет (значения в state сохраняются, при save не стираются):
|
||||||
|
- «Артикул» (генерируется автоматически, Название на 3 колонки)
|
||||||
|
- «Описание», «Страна происхождения», «Эталонная цена»
|
||||||
|
- «Ставка НДС», чекбокс «В том числе НДС»
|
||||||
|
- «Валюта закупки» (мульти-валюта)
|
||||||
|
- Чекбоксы «Услуга», «Маркируемый»
|
||||||
|
- «Максимальный остаток» — остаётся только «Минимальный»
|
||||||
|
- Все не-retail типы цен в блоке «Цены продажи» — рендерится только retail/system price type.
|
||||||
|
- **Контрагенты** (`CounterpartiesPage.tsx`) — в простом режиме скрыты Юр. название, Страна, Email, Банк/Р.счёт/БИК, Контактное лицо. Остаются: Название, Тип лица, БИН/ИИН, Адрес, Телефон, Заметки.
|
||||||
|
- **Приёмка** (`SupplyEditPage.tsx`) — колонка «Валюта» всегда скрыта в простом режиме (одна валюта по умолчанию).
|
||||||
|
- Другие документы (Enter, Loss, Inventory, SupplierReturn) используют те же `org.ShowXxx` / `org.MultiCurrencyEnabled` флаги — уже скрывают advanced-поля для магазинов с «простым» org-настройками.
|
||||||
|
|
||||||
|
Доп. список спрятанного за `simpleMode` (из шагов 4/5, не вошёл напрямую в чек-лист):
|
||||||
|
- `AppLayout` → `LanguageSwitcher`
|
||||||
|
- `DashboardPage` → виджеты «Топ товаров» и «Маржа»
|
||||||
|
|
||||||
|
Что отложено (новая функциональность, в этой задаче не трогаем):
|
||||||
|
- `Counterparty.IsSupplier` / `IsBuyer` — задача просит галочки «Поставщик/Покупатель», но на `Counterparty` таких bool-колонок нет. Это новая миграция и расширение API — отдельная задача.
|
||||||
|
- `pnpm run build` — ✓ (2.36s, 0 TS errors).
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { PhoneInput } from '@/components/PhoneInput'
|
||||||
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
||||||
import { useConfirm } from '@/lib/useConfirm'
|
import { useConfirm } from '@/lib/useConfirm'
|
||||||
import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog'
|
import { useCatalogList, useCatalogMutations } from '@/lib/useCatalog'
|
||||||
|
import { useMe } from '@/lib/useMe'
|
||||||
import { type Counterparty, type Country, type PagedResult, CounterpartyType } from '@/lib/types'
|
import { type Counterparty, type Country, type PagedResult, CounterpartyType } from '@/lib/types'
|
||||||
|
|
||||||
const URL = '/api/catalog/counterparties'
|
const URL = '/api/catalog/counterparties'
|
||||||
|
|
@ -56,6 +57,11 @@ const typeLabel: Record<CounterpartyType, string> = {
|
||||||
export function CounterpartiesPage() {
|
export function CounterpartiesPage() {
|
||||||
const { data, isLoading, page, setPage, search, setSearch, sortKey, sortOrder, setSort } = useCatalogList<Counterparty>(URL)
|
const { data, isLoading, page, setPage, search, setSearch, sortKey, sortOrder, setSort } = useCatalogList<Counterparty>(URL)
|
||||||
const { create, update, remove } = useCatalogMutations(URL, URL)
|
const { create, update, remove } = useCatalogMutations(URL, URL)
|
||||||
|
const me = useMe()
|
||||||
|
// Sprint 29 — простой режим скрывает редко нужные поля (юр.название,
|
||||||
|
// банк, страна, контактное лицо, email). Значения существующих
|
||||||
|
// контрагентов сохраняются и при переключении в расширенный снова видны.
|
||||||
|
const simpleMode = !!me.data && !me.data.advancedMode && !me.data.roles?.includes('SuperAdmin')
|
||||||
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 { confirm, dialogProps } = useConfirm()
|
||||||
|
|
@ -165,9 +171,11 @@ export function CounterpartiesPage() {
|
||||||
onChange={(e) => { setForm({ ...form, name: e.target.value }); if (fieldErrors.name) setFieldErrors(p => ({...p, name: undefined})) }}
|
onChange={(e) => { setForm({ ...form, name: e.target.value }); if (fieldErrors.name) setFieldErrors(p => ({...p, name: undefined})) }}
|
||||||
onBlur={() => { if (!form.name.trim()) setFieldErrors(p => ({...p, name: 'Название обязательно'})) }} />
|
onBlur={() => { if (!form.name.trim()) setFieldErrors(p => ({...p, name: 'Название обязательно'})) }} />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Юридическое название" className="col-span-2">
|
{!simpleMode && (
|
||||||
<TextInput value={form.legalName} onChange={(e) => setForm({ ...form, legalName: e.target.value })} />
|
<Field label="Юридическое название" className="col-span-2">
|
||||||
</Field>
|
<TextInput value={form.legalName} onChange={(e) => setForm({ ...form, legalName: e.target.value })} />
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
<Field label="Тип лица">
|
<Field label="Тип лица">
|
||||||
<Select value={form.type} onChange={(e) => setForm({ ...form, type: Number(e.target.value) as CounterpartyType })}>
|
<Select value={form.type} onChange={(e) => setForm({ ...form, type: Number(e.target.value) as CounterpartyType })}>
|
||||||
<option value={CounterpartyType.LegalEntity}>Юрлицо</option>
|
<option value={CounterpartyType.LegalEntity}>Юрлицо</option>
|
||||||
|
|
@ -180,12 +188,14 @@ export function CounterpartiesPage() {
|
||||||
<Field label="ИИН (физлицо РК)">
|
<Field label="ИИН (физлицо РК)">
|
||||||
<TextInput value={form.iin} onChange={(e) => setForm({ ...form, iin: e.target.value })} placeholder="12 цифр" maxLength={12} inputMode="numeric" />
|
<TextInput value={form.iin} onChange={(e) => setForm({ ...form, iin: e.target.value })} placeholder="12 цифр" maxLength={12} inputMode="numeric" />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Страна">
|
{!simpleMode && (
|
||||||
<Select value={form.countryId} onChange={(e) => setForm({ ...form, countryId: e.target.value })}>
|
<Field label="Страна">
|
||||||
<option value="">—</option>
|
<Select value={form.countryId} onChange={(e) => setForm({ ...form, countryId: e.target.value })}>
|
||||||
{countries.data?.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}
|
<option value="">—</option>
|
||||||
</Select>
|
{countries.data?.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||||||
</Field>
|
</Select>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
<Field label="Адрес" className="col-span-2">
|
<Field label="Адрес" className="col-span-2">
|
||||||
<TextInput value={form.address} onChange={(e) => setForm({ ...form, address: e.target.value })} />
|
<TextInput value={form.address} onChange={(e) => setForm({ ...form, address: e.target.value })} />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
@ -194,23 +204,29 @@ export function CounterpartiesPage() {
|
||||||
onChange={(v) => { setForm({ ...form, phone: v }); if (fieldErrors.phone) setFieldErrors(p => ({...p, phone: undefined})) }}
|
onChange={(v) => { setForm({ ...form, phone: v }); if (fieldErrors.phone) setFieldErrors(p => ({...p, phone: undefined})) }}
|
||||||
onBlur={() => { if (form.phone) { const e = validatePhone(form.phone); if (e) setFieldErrors(p => ({...p, phone: e})) } }} />
|
onBlur={() => { if (form.phone) { const e = validatePhone(form.phone); if (e) setFieldErrors(p => ({...p, phone: e})) } }} />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Email" error={fieldErrors.email}>
|
{!simpleMode && (
|
||||||
<TextInput type="email" value={form.email}
|
<Field label="Email" error={fieldErrors.email}>
|
||||||
onChange={(e) => { setForm({ ...form, email: e.target.value }); if (fieldErrors.email) setFieldErrors(p => ({...p, email: undefined})) }}
|
<TextInput type="email" value={form.email}
|
||||||
onBlur={() => { if (form.email) { const e = validateEmail(form.email); if (e) setFieldErrors(p => ({...p, email: e})) } }} />
|
onChange={(e) => { setForm({ ...form, email: e.target.value }); if (fieldErrors.email) setFieldErrors(p => ({...p, email: undefined})) }}
|
||||||
</Field>
|
onBlur={() => { if (form.email) { const e = validateEmail(form.email); if (e) setFieldErrors(p => ({...p, email: e})) } }} />
|
||||||
<Field label="Банк" className="col-span-2">
|
</Field>
|
||||||
<TextInput value={form.bankName} onChange={(e) => setForm({ ...form, bankName: e.target.value })} />
|
)}
|
||||||
</Field>
|
{!simpleMode && (
|
||||||
<Field label="Расчётный счёт">
|
<>
|
||||||
<TextInput value={form.bankAccount} onChange={(e) => setForm({ ...form, bankAccount: e.target.value })} />
|
<Field label="Банк" className="col-span-2">
|
||||||
</Field>
|
<TextInput value={form.bankName} onChange={(e) => setForm({ ...form, bankName: e.target.value })} />
|
||||||
<Field label="БИК">
|
</Field>
|
||||||
<TextInput value={form.bik} onChange={(e) => setForm({ ...form, bik: e.target.value })} />
|
<Field label="Расчётный счёт">
|
||||||
</Field>
|
<TextInput value={form.bankAccount} onChange={(e) => setForm({ ...form, bankAccount: e.target.value })} />
|
||||||
<Field label="Контактное лицо" className="col-span-2">
|
</Field>
|
||||||
<TextInput value={form.contactPerson} onChange={(e) => setForm({ ...form, contactPerson: e.target.value })} />
|
<Field label="БИК">
|
||||||
</Field>
|
<TextInput value={form.bik} onChange={(e) => setForm({ ...form, bik: e.target.value })} />
|
||||||
|
</Field>
|
||||||
|
<Field label="Контактное лицо" className="col-span-2">
|
||||||
|
<TextInput value={form.contactPerson} onChange={(e) => setForm({ ...form, contactPerson: e.target.value })} />
|
||||||
|
</Field>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<Field label="Заметки" className="col-span-2">
|
<Field label="Заметки" className="col-span-2">
|
||||||
<TextArea rows={3} value={form.notes} onChange={(e) => setForm({ ...form, notes: e.target.value })} />
|
<TextArea rows={3} value={form.notes} onChange={(e) => setForm({ ...form, notes: e.target.value })} />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
useUnits, useCountries, useCurrencies, usePriceTypes, useProductGroups,
|
useUnits, useCountries, useCurrencies, usePriceTypes, useProductGroups,
|
||||||
} from '@/lib/useLookups'
|
} from '@/lib/useLookups'
|
||||||
import { useOrgSettings } from '@/lib/useOrgSettings'
|
import { useOrgSettings } from '@/lib/useOrgSettings'
|
||||||
|
import { useMe } from '@/lib/useMe'
|
||||||
import { BarcodeType, Packaging, type Product } from '@/lib/types'
|
import { BarcodeType, Packaging, type Product } from '@/lib/types'
|
||||||
import { ProductImageGallery } from '@/components/ProductImageGallery'
|
import { ProductImageGallery } from '@/components/ProductImageGallery'
|
||||||
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
import { ConfirmDialog } from '@/components/ConfirmDialog'
|
||||||
|
|
@ -68,6 +69,11 @@ export function ProductEditPage() {
|
||||||
const { confirm, dialogProps } = useConfirm()
|
const { confirm, dialogProps } = useConfirm()
|
||||||
const priceTypes = usePriceTypes()
|
const priceTypes = usePriceTypes()
|
||||||
const org = useOrgSettings()
|
const org = useOrgSettings()
|
||||||
|
const me = useMe()
|
||||||
|
// Sprint 29 — простой режим: все advanced-only поля скрываются (значения
|
||||||
|
// сохраняются, при переключении в расширенный снова видно). SuperAdmin
|
||||||
|
// режим не ощущает — ему доступно всё.
|
||||||
|
const simpleMode = !!me.data && !me.data.advancedMode && !me.data.roles?.includes('SuperAdmin')
|
||||||
|
|
||||||
const existing = useQuery({
|
const existing = useQuery({
|
||||||
queryKey: ['/api/catalog/products', id],
|
queryKey: ['/api/catalog/products', id],
|
||||||
|
|
@ -298,12 +304,16 @@ export function ProductEditPage() {
|
||||||
|
|
||||||
<Section title="Основное">
|
<Section title="Основное">
|
||||||
<Grid cols={3}>
|
<Grid cols={3}>
|
||||||
<Field label="Название *" className="col-span-2">
|
<Field label="Название *" className={simpleMode ? 'col-span-3' : 'col-span-2'}>
|
||||||
<TextInput required value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
|
<TextInput required value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Артикул *">
|
{/* Артикул — только расширенный режим; в простом генерируется
|
||||||
<TextInput required value={form.article} onChange={(e) => setForm({ ...form, article: e.target.value })} />
|
автоматически (см. useEffect с generateArticle). */}
|
||||||
</Field>
|
{!simpleMode && (
|
||||||
|
<Field label="Артикул *">
|
||||||
|
<TextInput required value={form.article} onChange={(e) => setForm({ ...form, article: e.target.value })} />
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<div className="mt-4 pt-4 border-t border-slate-100 dark:border-slate-800">
|
<div className="mt-4 pt-4 border-t border-slate-100 dark:border-slate-800">
|
||||||
|
|
@ -361,7 +371,7 @@ export function ProductEditPage() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{org.data?.showDescriptionOnProduct && (
|
{!simpleMode && org.data?.showDescriptionOnProduct && (
|
||||||
<div className="mt-4 pt-4 border-t border-slate-100 dark:border-slate-800">
|
<div className="mt-4 pt-4 border-t border-slate-100 dark:border-slate-800">
|
||||||
<Field label="Описание">
|
<Field label="Описание">
|
||||||
<TextArea rows={2} value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} />
|
<TextArea rows={2} value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} />
|
||||||
|
|
@ -406,7 +416,7 @@ export function ProductEditPage() {
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400 mb-3">Закупка</h3>
|
<h3 className="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400 mb-3">Закупка</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{org.data?.showReferencePriceOnProduct && (
|
{!simpleMode && org.data?.showReferencePriceOnProduct && (
|
||||||
<Field label="Эталонная цена">
|
<Field label="Эталонная цена">
|
||||||
<MoneyInput
|
<MoneyInput
|
||||||
value={form.referencePrice === '' ? null : Number(form.referencePrice)}
|
value={form.referencePrice === '' ? null : Number(form.referencePrice)}
|
||||||
|
|
@ -427,7 +437,7 @@ export function ProductEditPage() {
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">расчётная (скользящее среднее)</p>
|
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">расчётная (скользящее среднее)</p>
|
||||||
</Field>
|
</Field>
|
||||||
{org.data?.multiCurrencyEnabled && (
|
{!simpleMode && org.data?.multiCurrencyEnabled && (
|
||||||
<Field label="Валюта закупки">
|
<Field label="Валюта закупки">
|
||||||
<Select value={form.purchaseCurrencyId} onChange={(e) => setForm({ ...form, purchaseCurrencyId: e.target.value })}>
|
<Select value={form.purchaseCurrencyId} onChange={(e) => setForm({ ...form, purchaseCurrencyId: e.target.value })}>
|
||||||
<option value="">—</option>
|
<option value="">—</option>
|
||||||
|
|
@ -446,7 +456,12 @@ export function ProductEditPage() {
|
||||||
* новая, при стирании — null Amount (UI пустое). Обязательные (IsRequired)
|
* новая, при стирании — null Amount (UI пустое). Обязательные (IsRequired)
|
||||||
* помечаются звёздочкой. */}
|
* помечаются звёздочкой. */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{priceTypes.data?.slice().sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name)).map((pt) => {
|
{priceTypes.data?.slice()
|
||||||
|
// В простом режиме у магазина одна цена продажи — показываем
|
||||||
|
// только retail/system price type, остальные (оптовая, VIP и т.п.)
|
||||||
|
// скрыты, но их значения сохраняются и не стираются при save.
|
||||||
|
.filter((pt) => !simpleMode || pt.isRetail || pt.isSystem)
|
||||||
|
.sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name)).map((pt) => {
|
||||||
const idx = form.prices.findIndex(p => p.priceTypeId === pt.id)
|
const idx = form.prices.findIndex(p => p.priceTypeId === pt.id)
|
||||||
const row = idx >= 0 ? form.prices[idx] : null
|
const row = idx >= 0 ? form.prices[idx] : null
|
||||||
const required = pt.isRequired
|
const required = pt.isRequired
|
||||||
|
|
@ -515,7 +530,7 @@ export function ProductEditPage() {
|
||||||
</Select>
|
</Select>
|
||||||
</Field>
|
</Field>
|
||||||
</Grid>
|
</Grid>
|
||||||
{org.data?.showCountryOfOriginOnProduct && (
|
{!simpleMode && org.data?.showCountryOfOriginOnProduct && (
|
||||||
<Grid cols={2}>
|
<Grid cols={2}>
|
||||||
<Field label="Страна происхождения">
|
<Field label="Страна происхождения">
|
||||||
<Select value={form.countryOfOriginId} onChange={(e) => setForm({ ...form, countryOfOriginId: e.target.value })}>
|
<Select value={form.countryOfOriginId} onChange={(e) => setForm({ ...form, countryOfOriginId: e.target.value })}>
|
||||||
|
|
@ -525,7 +540,7 @@ export function ProductEditPage() {
|
||||||
</Field>
|
</Field>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
{org.data?.showVatEnabledOnProduct && form.vatEnabled && (
|
{!simpleMode && org.data?.showVatEnabledOnProduct && form.vatEnabled && (
|
||||||
<Grid cols={3}>
|
<Grid cols={3}>
|
||||||
<Field label="Ставка НДС, %">
|
<Field label="Ставка НДС, %">
|
||||||
<NumberInput
|
<NumberInput
|
||||||
|
|
@ -536,19 +551,35 @@ export function ProductEditPage() {
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
<div className="pt-3 mt-3 border-t border-slate-100 dark:border-slate-800 flex flex-wrap gap-x-6 gap-y-2">
|
<div className="pt-3 mt-3 border-t border-slate-100 dark:border-slate-800 flex flex-wrap gap-x-6 gap-y-2">
|
||||||
{org.data?.showVatEnabledOnProduct && (
|
{!simpleMode && org.data?.showVatEnabledOnProduct && (
|
||||||
<Checkbox label="В том числе НДС" checked={form.vatEnabled} onChange={(v) => setForm({ ...form, vatEnabled: v })} />
|
<Checkbox label="В том числе НДС" checked={form.vatEnabled} onChange={(v) => setForm({ ...form, vatEnabled: v })} />
|
||||||
)}
|
)}
|
||||||
{org.data?.showServiceOnProduct && (
|
{!simpleMode && org.data?.showServiceOnProduct && (
|
||||||
<Checkbox label="Услуга" checked={form.isService} onChange={(v) => setForm({ ...form, isService: v })} />
|
<Checkbox label="Услуга" checked={form.isService} onChange={(v) => setForm({ ...form, isService: v })} />
|
||||||
)}
|
)}
|
||||||
{org.data?.showMarkedOnProduct && (
|
{!simpleMode && org.data?.showMarkedOnProduct && (
|
||||||
<Checkbox label="Маркируемый (Честный знак / Datamatrix)" checked={form.isMarked} onChange={(v) => setForm({ ...form, isMarked: v })} />
|
<Checkbox label="Маркируемый (Честный знак / Datamatrix)" checked={form.isMarked} onChange={(v) => setForm({ ...form, isMarked: v })} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{org.data?.showMinMaxStock && (
|
{/* В простом режиме «Мин. остаток» входит в базовый набор полей
|
||||||
|
карточки (см. docs/simple-mode.md), поэтому показываем его
|
||||||
|
всегда — но без «Максимального» (это для автозаказа, продвинутая
|
||||||
|
фича). В расширенном — обе колонки, при условии org.ShowMinMaxStock. */}
|
||||||
|
{simpleMode ? (
|
||||||
|
<AdvancedSection>
|
||||||
|
<Grid cols={4}>
|
||||||
|
<Field label="Минимальный остаток (для уведомления)">
|
||||||
|
<NumberInput
|
||||||
|
value={form.minStock === '' ? null : Number(form.minStock)}
|
||||||
|
onChange={(n) => setForm({ ...form, minStock: n == null ? '' : String(n) })}
|
||||||
|
placeholder="—"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Grid>
|
||||||
|
</AdvancedSection>
|
||||||
|
) : org.data?.showMinMaxStock && (
|
||||||
<AdvancedSection>
|
<AdvancedSection>
|
||||||
<Grid cols={4}>
|
<Grid cols={4}>
|
||||||
<Field label="Минимальный остаток (для уведомления)">
|
<Field label="Минимальный остаток (для уведомления)">
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { useConfirm } from '@/lib/useConfirm'
|
||||||
import { useShortcuts } from '@/lib/useShortcuts'
|
import { useShortcuts } from '@/lib/useShortcuts'
|
||||||
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 { useMe } from '@/lib/useMe'
|
||||||
import { SupplyStatus, type SupplyDto, type Product } from '@/lib/types'
|
import { SupplyStatus, type SupplyDto, type Product } from '@/lib/types'
|
||||||
|
|
||||||
interface LineRow {
|
interface LineRow {
|
||||||
|
|
@ -68,6 +69,11 @@ export function SupplyEditPage() {
|
||||||
const stores = useStores()
|
const stores = useStores()
|
||||||
const currencies = useCurrencies()
|
const currencies = useCurrencies()
|
||||||
const org = useOrgSettings()
|
const org = useOrgSettings()
|
||||||
|
const me = useMe()
|
||||||
|
// Sprint 29 — простой режим: мульти-валюта всегда выключена, независимо
|
||||||
|
// от org-настроек. В UI колонка валюты скрыта, send-side уже отправляет
|
||||||
|
// DefaultCurrencyId.
|
||||||
|
const simpleMode = !!me.data && !me.data.advancedMode && !me.data.roles?.includes('SuperAdmin')
|
||||||
const priceTypes = usePriceTypes()
|
const priceTypes = usePriceTypes()
|
||||||
const { confirm, dialogProps } = useConfirm()
|
const { confirm, dialogProps } = useConfirm()
|
||||||
// Системный (главный) тип цен — на нём по умолчанию ведётся розница на кассе.
|
// Системный (главный) тип цен — на нём по умолчанию ведётся розница на кассе.
|
||||||
|
|
@ -368,7 +374,7 @@ export function SupplyEditPage() {
|
||||||
</Select>
|
</Select>
|
||||||
</Field>
|
</Field>
|
||||||
)}
|
)}
|
||||||
{org.data?.multiCurrencyEnabled && (
|
{!simpleMode && org.data?.multiCurrencyEnabled && (
|
||||||
<Field label="Валюта *">
|
<Field label="Валюта *">
|
||||||
<Select value={form.currencyId} disabled={isPosted}
|
<Select value={form.currencyId} disabled={isPosted}
|
||||||
onChange={(e) => setForm({ ...form, currencyId: e.target.value })}>
|
onChange={(e) => setForm({ ...form, currencyId: e.target.value })}>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue