feat(org-settings): галки «Услуга»/«Маркируемый» скрываются по умолчанию

Добавлены organizations.ShowServiceOnProduct и ShowMarkedOnProduct
(оба default false). В UI карточки товара чекбоксы «Услуга» и
«Маркируемый» рендерятся только если соответствующий флаг включен;
в фильтрах списка товаров Tri-фильтры тоже прячутся. В БД поля
IsService/IsMarked у Product сохраняются как обычно — просто UI их
не показывает.

Это параллель к ShowVatEnabledOnProduct: по умолчанию UI максимально
простой, а нишевые фичи включаются через настройки магазина.

Миграция Phase5c_ShowServiceMarkedOnProduct добавляет обе колонки.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
nns 2026-04-24 16:39:06 +05:00
parent 42a3d2aa50
commit 781f268089
9 changed files with 1978 additions and 7 deletions

View file

@ -30,14 +30,18 @@ public record OrgSettingsDto(
bool MultiCurrencyEnabled,
// VAT read-only: из страны организации (countries.VatRate). Источник правды — справочник стран.
decimal VatRate,
bool ShowVatEnabledOnProduct);
bool ShowVatEnabledOnProduct,
bool ShowServiceOnProduct,
bool ShowMarkedOnProduct);
// DefaultCurrencyId не принимается — он read-only, выводится из страны (Country.DefaultCurrencyId).
public record OrgSettingsInput(
string Name,
string CountryCode,
bool MultiCurrencyEnabled,
bool ShowVatEnabledOnProduct);
bool ShowVatEnabledOnProduct,
bool ShowServiceOnProduct,
bool ShowMarkedOnProduct);
[HttpGet("settings")]
public async Task<ActionResult<OrgSettingsDto>> Get(CancellationToken ct)
@ -69,6 +73,8 @@ public async Task<ActionResult<OrgSettingsDto>> Update([FromBody] OrgSettingsInp
.FirstOrDefaultAsync(ct);
o.MultiCurrencyEnabled = input.MultiCurrencyEnabled;
o.ShowVatEnabledOnProduct = input.ShowVatEnabledOnProduct;
o.ShowServiceOnProduct = input.ShowServiceOnProduct;
o.ShowMarkedOnProduct = input.ShowMarkedOnProduct;
await _db.SaveChangesAsync(ct);
await _db.Entry(o).Reference(x => x.DefaultCurrency).LoadAsync(ct);
@ -92,5 +98,7 @@ private async Task<decimal> ReadVatRateAsync(string countryCode, CancellationTok
o.DefaultCurrency?.Symbol,
o.MultiCurrencyEnabled,
vat,
o.ShowVatEnabledOnProduct);
o.ShowVatEnabledOnProduct,
o.ShowServiceOnProduct,
o.ShowMarkedOnProduct);
}

View file

@ -31,4 +31,14 @@ public class Organization : Entity
/// скрыта, все товары считаются с НДС. Если true — можно для отдельных товаров
/// (хлеб, медикаменты) снимать галку.</summary>
public bool ShowVatEnabledOnProduct { get; set; }
/// <summary>Показывать ли на форме товара и в фильтрах галку «Услуга».
/// Большинство магазинов продают только физические товары — флаг выключен
/// по умолчанию, чтобы не захламлять UI.</summary>
public bool ShowServiceOnProduct { get; set; }
/// <summary>Показывать ли на форме товара и в фильтрах галку «Маркируемый».
/// Маркировка требуется только в нишевых категориях (алкоголь, лекарства,
/// табак) — по умолчанию выключено.</summary>
public bool ShowMarkedOnProduct { get; set; }
}

View file

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace foodmarket.Infrastructure.Persistence.Migrations
{
/// <summary>Добавляет organizations.ShowServiceOnProduct и ShowMarkedOnProduct —
/// флаги видимости чекбоксов «Услуга» / «Маркируемый» на форме товара и
/// в фильтрах списка. Оба по умолчанию false: большинство магазинов
/// продают только физические товары без маркировки.</summary>
public partial class Phase5c_ShowServiceMarkedOnProduct : Migration
{
protected override void Up(MigrationBuilder b)
{
b.AddColumn<bool>(
name: "ShowServiceOnProduct", schema: "public", table: "organizations",
type: "boolean", nullable: false, defaultValue: false);
b.AddColumn<bool>(
name: "ShowMarkedOnProduct", schema: "public", table: "organizations",
type: "boolean", nullable: false, defaultValue: false);
}
protected override void Down(MigrationBuilder b)
{
b.DropColumn(name: "ShowServiceOnProduct", schema: "public", table: "organizations");
b.DropColumn(name: "ShowMarkedOnProduct", schema: "public", table: "organizations");
}
}
}

View file

@ -1092,6 +1092,12 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Property<bool>("MultiCurrencyEnabled")
.HasColumnType("boolean");
b.Property<bool>("ShowMarkedOnProduct")
.HasColumnType("boolean");
b.Property<bool>("ShowServiceOnProduct")
.HasColumnType("boolean");
b.Property<bool>("ShowVatEnabledOnProduct")
.HasColumnType("boolean");

View file

@ -11,6 +11,8 @@ export interface OrgSettings {
multiCurrencyEnabled: boolean
vatRate: number
showVatEnabledOnProduct: boolean
showServiceOnProduct: boolean
showMarkedOnProduct: boolean
}
export function useOrgSettings() {

View file

@ -38,6 +38,8 @@ export function OrganizationSettingsPage() {
countryCode: form.countryCode,
multiCurrencyEnabled: form.multiCurrencyEnabled,
showVatEnabledOnProduct: form.showVatEnabledOnProduct,
showServiceOnProduct: form.showServiceOnProduct,
showMarkedOnProduct: form.showMarkedOnProduct,
}
return (await api.put<OrgSettings>('/api/organization/settings', payload)).data
},
@ -103,6 +105,26 @@ export function OrganizationSettingsPage() {
все новые товары получают ставку из страны организации. Если включено у каждого
товара можно задать ставку вручную (хлеб = 0%, лекарства = 0% и т.п.).
</p>
<Checkbox
label='Показывать чекбокс «Услуга» на товаре'
checked={form.showServiceOnProduct}
onChange={(v) => setForm({ ...form, showServiceOnProduct: v })}
/>
<p className="text-xs text-slate-500 -mt-2">
Нужно, если помимо физических товаров продаются услуги (доставка, сборка и т.п.).
По умолчанию галка скрыта.
</p>
<Checkbox
label='Показывать чекбокс «Маркируемый» на товаре'
checked={form.showMarkedOnProduct}
onChange={(v) => setForm({ ...form, showMarkedOnProduct: v })}
/>
<p className="text-xs text-slate-500 -mt-2">
Включать, только если продаётся маркируемая категория (алкоголь, табак, лекарства).
По умолчанию галка скрыта.
</p>
</section>
<div className="mt-4 flex gap-3 items-center">

View file

@ -285,8 +285,12 @@ export function ProductEditPage() {
{org.data?.showVatEnabledOnProduct && (
<Checkbox label="В том числе НДС" checked={form.vatEnabled} onChange={(v) => setForm({ ...form, vatEnabled: v })} />
)}
<Checkbox label="Услуга" checked={form.isService} onChange={(v) => setForm({ ...form, isService: v })} />
<Checkbox label="Маркируемый (Честный знак / Datamatrix)" checked={form.isMarked} onChange={(v) => setForm({ ...form, isMarked: v })} />
{org.data?.showServiceOnProduct && (
<Checkbox label="Услуга" checked={form.isService} onChange={(v) => setForm({ ...form, isService: v })} />
)}
{org.data?.showMarkedOnProduct && (
<Checkbox label="Маркируемый (Честный знак / Datamatrix)" checked={form.isMarked} onChange={(v) => setForm({ ...form, isMarked: v })} />
)}
<Checkbox label="Активен" checked={form.isActive} onChange={(v) => setForm({ ...form, isActive: v })} />
</div>
</Section>

View file

@ -99,6 +99,8 @@ export function ProductsPage() {
const { data, isLoading, page, setPage, search, setSearch, sortKey, sortOrder, setSort } = useCatalogList<Product>(URL, toExtra(filters))
const org = useOrgSettings()
const showVat = org.data?.showVatEnabledOnProduct ?? false
const showService = org.data?.showServiceOnProduct ?? false
const showMarked = org.data?.showMarkedOnProduct ?? false
const activeCount = activeFilterCount(filters)
type Col = {
@ -164,7 +166,9 @@ export function ProductsPage() {
{filtersOpen && (
<div className="px-6 py-3 border-b border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-900/60 flex flex-wrap gap-4 items-center">
<Tri label="Активные" value={filters.isActive} onChange={(v) => { setFilters({ ...filters, isActive: v }); setPage(1) }} />
<Tri label="Услуга" value={filters.isService} onChange={(v) => { setFilters({ ...filters, isService: v }); setPage(1) }} />
{showService && (
<Tri label="Услуга" value={filters.isService} onChange={(v) => { setFilters({ ...filters, isService: v }); setPage(1) }} />
)}
<div className="flex items-center gap-2 text-xs">
<span className="text-slate-500">Фасовка</span>
<select
@ -178,7 +182,9 @@ export function ProductsPage() {
<option value="3">разливной</option>
</select>
</div>
<Tri label="Маркируемый" value={filters.isMarked} onChange={(v) => { setFilters({ ...filters, isMarked: v }); setPage(1) }} />
{showMarked && (
<Tri label="Маркируемый" value={filters.isMarked} onChange={(v) => { setFilters({ ...filters, isMarked: v }); setPage(1) }} />
)}
<Tri label="Со штрихкодом" value={filters.hasBarcode} onChange={(v) => { setFilters({ ...filters, hasBarcode: v }); setPage(1) }} yesLabel="есть" noLabel="нет" />
{activeCount > 0 && (
<button