feat(org-settings): галки «Услуга»/«Маркируемый» скрываются по умолчанию
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 24s
CI / Web (React + Vite) (push) Successful in 25s
Docker Images / API image (push) Successful in 38s
Docker Images / Web image (push) Successful in 25s
Docker Images / Deploy stage (push) Successful in 19s
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 24s
CI / Web (React + Vite) (push) Successful in 25s
Docker Images / API image (push) Successful in 38s
Docker Images / Web image (push) Successful in 25s
Docker Images / Deploy stage (push) Successful in 19s
Добавлены 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:
parent
e60cd928d2
commit
143f9d5330
|
|
@ -30,14 +30,18 @@ public record OrgSettingsDto(
|
||||||
bool MultiCurrencyEnabled,
|
bool MultiCurrencyEnabled,
|
||||||
// VAT read-only: из страны организации (countries.VatRate). Источник правды — справочник стран.
|
// VAT read-only: из страны организации (countries.VatRate). Источник правды — справочник стран.
|
||||||
decimal VatRate,
|
decimal VatRate,
|
||||||
bool ShowVatEnabledOnProduct);
|
bool ShowVatEnabledOnProduct,
|
||||||
|
bool ShowServiceOnProduct,
|
||||||
|
bool ShowMarkedOnProduct);
|
||||||
|
|
||||||
// DefaultCurrencyId не принимается — он read-only, выводится из страны (Country.DefaultCurrencyId).
|
// DefaultCurrencyId не принимается — он read-only, выводится из страны (Country.DefaultCurrencyId).
|
||||||
public record OrgSettingsInput(
|
public record OrgSettingsInput(
|
||||||
string Name,
|
string Name,
|
||||||
string CountryCode,
|
string CountryCode,
|
||||||
bool MultiCurrencyEnabled,
|
bool MultiCurrencyEnabled,
|
||||||
bool ShowVatEnabledOnProduct);
|
bool ShowVatEnabledOnProduct,
|
||||||
|
bool ShowServiceOnProduct,
|
||||||
|
bool ShowMarkedOnProduct);
|
||||||
|
|
||||||
[HttpGet("settings")]
|
[HttpGet("settings")]
|
||||||
public async Task<ActionResult<OrgSettingsDto>> Get(CancellationToken ct)
|
public async Task<ActionResult<OrgSettingsDto>> Get(CancellationToken ct)
|
||||||
|
|
@ -69,6 +73,8 @@ public async Task<ActionResult<OrgSettingsDto>> Update([FromBody] OrgSettingsInp
|
||||||
.FirstOrDefaultAsync(ct);
|
.FirstOrDefaultAsync(ct);
|
||||||
o.MultiCurrencyEnabled = input.MultiCurrencyEnabled;
|
o.MultiCurrencyEnabled = input.MultiCurrencyEnabled;
|
||||||
o.ShowVatEnabledOnProduct = input.ShowVatEnabledOnProduct;
|
o.ShowVatEnabledOnProduct = input.ShowVatEnabledOnProduct;
|
||||||
|
o.ShowServiceOnProduct = input.ShowServiceOnProduct;
|
||||||
|
o.ShowMarkedOnProduct = input.ShowMarkedOnProduct;
|
||||||
await _db.SaveChangesAsync(ct);
|
await _db.SaveChangesAsync(ct);
|
||||||
|
|
||||||
await _db.Entry(o).Reference(x => x.DefaultCurrency).LoadAsync(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.DefaultCurrency?.Symbol,
|
||||||
o.MultiCurrencyEnabled,
|
o.MultiCurrencyEnabled,
|
||||||
vat,
|
vat,
|
||||||
o.ShowVatEnabledOnProduct);
|
o.ShowVatEnabledOnProduct,
|
||||||
|
o.ShowServiceOnProduct,
|
||||||
|
o.ShowMarkedOnProduct);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,4 +31,14 @@ public class Organization : Entity
|
||||||
/// скрыта, все товары считаются с НДС. Если true — можно для отдельных товаров
|
/// скрыта, все товары считаются с НДС. Если true — можно для отдельных товаров
|
||||||
/// (хлеб, медикаменты) снимать галку.</summary>
|
/// (хлеб, медикаменты) снимать галку.</summary>
|
||||||
public bool ShowVatEnabledOnProduct { get; set; }
|
public bool ShowVatEnabledOnProduct { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Показывать ли на форме товара и в фильтрах галку «Услуга».
|
||||||
|
/// Большинство магазинов продают только физические товары — флаг выключен
|
||||||
|
/// по умолчанию, чтобы не захламлять UI.</summary>
|
||||||
|
public bool ShowServiceOnProduct { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Показывать ли на форме товара и в фильтрах галку «Маркируемый».
|
||||||
|
/// Маркировка требуется только в нишевых категориях (алкоголь, лекарства,
|
||||||
|
/// табак) — по умолчанию выключено.</summary>
|
||||||
|
public bool ShowMarkedOnProduct { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1092,6 +1092,12 @@ protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
b.Property<bool>("MultiCurrencyEnabled")
|
b.Property<bool>("MultiCurrencyEnabled")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("ShowMarkedOnProduct")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("ShowServiceOnProduct")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<bool>("ShowVatEnabledOnProduct")
|
b.Property<bool>("ShowVatEnabledOnProduct")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ export interface OrgSettings {
|
||||||
multiCurrencyEnabled: boolean
|
multiCurrencyEnabled: boolean
|
||||||
vatRate: number
|
vatRate: number
|
||||||
showVatEnabledOnProduct: boolean
|
showVatEnabledOnProduct: boolean
|
||||||
|
showServiceOnProduct: boolean
|
||||||
|
showMarkedOnProduct: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useOrgSettings() {
|
export function useOrgSettings() {
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@ export function OrganizationSettingsPage() {
|
||||||
countryCode: form.countryCode,
|
countryCode: form.countryCode,
|
||||||
multiCurrencyEnabled: form.multiCurrencyEnabled,
|
multiCurrencyEnabled: form.multiCurrencyEnabled,
|
||||||
showVatEnabledOnProduct: form.showVatEnabledOnProduct,
|
showVatEnabledOnProduct: form.showVatEnabledOnProduct,
|
||||||
|
showServiceOnProduct: form.showServiceOnProduct,
|
||||||
|
showMarkedOnProduct: form.showMarkedOnProduct,
|
||||||
}
|
}
|
||||||
return (await api.put<OrgSettings>('/api/organization/settings', payload)).data
|
return (await api.put<OrgSettings>('/api/organization/settings', payload)).data
|
||||||
},
|
},
|
||||||
|
|
@ -103,6 +105,26 @@ export function OrganizationSettingsPage() {
|
||||||
все новые товары получают ставку из страны организации. Если включено — у каждого
|
все новые товары получают ставку из страны организации. Если включено — у каждого
|
||||||
товара можно задать ставку вручную (хлеб = 0%, лекарства = 0% и т.п.).
|
товара можно задать ставку вручную (хлеб = 0%, лекарства = 0% и т.п.).
|
||||||
</p>
|
</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>
|
</section>
|
||||||
|
|
||||||
<div className="mt-4 flex gap-3 items-center">
|
<div className="mt-4 flex gap-3 items-center">
|
||||||
|
|
|
||||||
|
|
@ -285,8 +285,12 @@ export function ProductEditPage() {
|
||||||
{org.data?.showVatEnabledOnProduct && (
|
{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 })} />
|
||||||
)}
|
)}
|
||||||
<Checkbox label="Услуга" checked={form.isService} onChange={(v) => setForm({ ...form, isService: v })} />
|
{org.data?.showServiceOnProduct && (
|
||||||
<Checkbox label="Маркируемый (Честный знак / Datamatrix)" checked={form.isMarked} onChange={(v) => setForm({ ...form, isMarked: v })} />
|
<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 })} />
|
<Checkbox label="Активен" checked={form.isActive} onChange={(v) => setForm({ ...form, isActive: v })} />
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,8 @@ export function ProductsPage() {
|
||||||
const { data, isLoading, page, setPage, search, setSearch, sortKey, sortOrder, setSort } = useCatalogList<Product>(URL, toExtra(filters))
|
const { data, isLoading, page, setPage, search, setSearch, sortKey, sortOrder, setSort } = useCatalogList<Product>(URL, toExtra(filters))
|
||||||
const org = useOrgSettings()
|
const org = useOrgSettings()
|
||||||
const showVat = org.data?.showVatEnabledOnProduct ?? false
|
const showVat = org.data?.showVatEnabledOnProduct ?? false
|
||||||
|
const showService = org.data?.showServiceOnProduct ?? false
|
||||||
|
const showMarked = org.data?.showMarkedOnProduct ?? false
|
||||||
const activeCount = activeFilterCount(filters)
|
const activeCount = activeFilterCount(filters)
|
||||||
|
|
||||||
type Col = {
|
type Col = {
|
||||||
|
|
@ -164,7 +166,9 @@ export function ProductsPage() {
|
||||||
{filtersOpen && (
|
{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">
|
<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.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">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
<span className="text-slate-500">Фасовка</span>
|
<span className="text-slate-500">Фасовка</span>
|
||||||
<select
|
<select
|
||||||
|
|
@ -178,7 +182,9 @@ export function ProductsPage() {
|
||||||
<option value="3">разливной</option>
|
<option value="3">разливной</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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="нет" />
|
<Tri label="Со штрихкодом" value={filters.hasBarcode} onChange={(v) => { setFilters({ ...filters, hasBarcode: v }); setPage(1) }} yesLabel="есть" noLabel="нет" />
|
||||||
{activeCount > 0 && (
|
{activeCount > 0 && (
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue