feat(org-settings): Country↔Currency, Organization.DefaultCurrency/MultiCurrency/DefaultVat + UI настроек
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 27s
CI / Web (React + Vite) (push) Successful in 24s
Docker Images / API image (push) Successful in 38s
Docker Images / Web image (push) Successful in 24s
Docker Images / Deploy stage (push) Successful in 18s
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 27s
CI / Web (React + Vite) (push) Successful in 24s
Docker Images / API image (push) Successful in 38s
Docker Images / Web image (push) Successful in 24s
Docker Images / Deploy stage (push) Successful in 18s
Миграция Phase4_CountryCurrencyOrgDefaults: - countries.DefaultCurrencyId (FK → currencies) - organizations.DefaultCurrencyId, MultiCurrencyEnabled, DefaultVat - Seed: KZ→KZT, RU→RUB, BY→BYN, US→USD, DE→EUR, CN→CNY, TR→TRY - Default для org: KZT, vat=16 Backend: - Organization сущность получила DefaultCurrency/MultiCurrencyEnabled/DefaultVat. - OrganizationSettingsController: GET/PUT /api/organization/settings. - DevDataSeeder при создании/backfill орга выставляет KZT + vat=16. Web: - /settings/organization: форма с выбором страны (авто-подтягивает валюту), чекбоксом multi-currency, ставкой НДС по умолчанию. - useOrgSettings() хук. - SupplyEditPage / RetailSaleEditPage / ProductEditPage: select валюты показывается только если multiCurrencyEnabled=true, иначе подтягивается DefaultCurrency организации и рисуется символ валюты справа от цены. - ProductEditPage при создании нового товара берёт VAT из org.DefaultVat. - В sidebar добавлен раздел 'Настройки → Организация', убран Ставки НДС (сущность удалена раньше).
This commit is contained in:
parent
d86b6ba742
commit
773ecde6ba
|
|
@ -0,0 +1,79 @@
|
||||||
|
using foodmarket.Application.Common.Tenancy;
|
||||||
|
using foodmarket.Infrastructure.Persistence;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace foodmarket.Api.Controllers.Organizations;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/organization")]
|
||||||
|
public class OrganizationSettingsController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _db;
|
||||||
|
private readonly ITenantContext _tenant;
|
||||||
|
|
||||||
|
public OrganizationSettingsController(AppDbContext db, ITenantContext tenant)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_tenant = tenant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record OrgSettingsDto(
|
||||||
|
Guid Id,
|
||||||
|
string Name,
|
||||||
|
string CountryCode,
|
||||||
|
Guid? DefaultCurrencyId,
|
||||||
|
string? DefaultCurrencyCode,
|
||||||
|
string? DefaultCurrencySymbol,
|
||||||
|
bool MultiCurrencyEnabled,
|
||||||
|
int DefaultVat);
|
||||||
|
|
||||||
|
public record OrgSettingsInput(
|
||||||
|
string Name,
|
||||||
|
string CountryCode,
|
||||||
|
Guid? DefaultCurrencyId,
|
||||||
|
bool MultiCurrencyEnabled,
|
||||||
|
int DefaultVat);
|
||||||
|
|
||||||
|
[HttpGet("settings")]
|
||||||
|
public async Task<ActionResult<OrgSettingsDto>> Get(CancellationToken ct)
|
||||||
|
{
|
||||||
|
var orgId = _tenant.OrganizationId ?? throw new InvalidOperationException("No tenant.");
|
||||||
|
var o = await _db.Organizations
|
||||||
|
.Include(o => o.DefaultCurrency)
|
||||||
|
.FirstOrDefaultAsync(o => o.Id == orgId, ct);
|
||||||
|
if (o is null) return NotFound();
|
||||||
|
return Project(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("settings"), Authorize(Roles = "Admin,Manager")]
|
||||||
|
public async Task<ActionResult<OrgSettingsDto>> Update([FromBody] OrgSettingsInput input, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var orgId = _tenant.OrganizationId ?? throw new InvalidOperationException("No tenant.");
|
||||||
|
var o = await _db.Organizations
|
||||||
|
.Include(o => o.DefaultCurrency)
|
||||||
|
.FirstOrDefaultAsync(o => o.Id == orgId, ct);
|
||||||
|
if (o is null) return NotFound();
|
||||||
|
|
||||||
|
o.Name = input.Name;
|
||||||
|
o.CountryCode = input.CountryCode;
|
||||||
|
o.DefaultCurrencyId = input.DefaultCurrencyId;
|
||||||
|
o.MultiCurrencyEnabled = input.MultiCurrencyEnabled;
|
||||||
|
o.DefaultVat = input.DefaultVat;
|
||||||
|
await _db.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
// Re-read чтобы подтянуть DefaultCurrency.
|
||||||
|
await _db.Entry(o).Reference(x => x.DefaultCurrency).LoadAsync(ct);
|
||||||
|
return Project(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OrgSettingsDto Project(foodmarket.Domain.Organizations.Organization o) => new(
|
||||||
|
o.Id, o.Name, o.CountryCode,
|
||||||
|
o.DefaultCurrencyId,
|
||||||
|
o.DefaultCurrency?.Code,
|
||||||
|
o.DefaultCurrency?.Symbol,
|
||||||
|
o.MultiCurrencyEnabled,
|
||||||
|
o.DefaultVat);
|
||||||
|
}
|
||||||
|
|
@ -38,6 +38,7 @@ public async Task StartAsync(CancellationToken ct)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var kzt = await db.Currencies.FirstOrDefaultAsync(c => c.Code == "KZT", ct);
|
||||||
var demoOrg = await db.Organizations.FirstOrDefaultAsync(o => o.Name == "Demo Market", ct);
|
var demoOrg = await db.Organizations.FirstOrDefaultAsync(o => o.Name == "Demo Market", ct);
|
||||||
if (demoOrg is null)
|
if (demoOrg is null)
|
||||||
{
|
{
|
||||||
|
|
@ -48,11 +49,20 @@ public async Task StartAsync(CancellationToken ct)
|
||||||
Bin = "000000000000",
|
Bin = "000000000000",
|
||||||
Address = "Алматы, ул. Пример 1",
|
Address = "Алматы, ул. Пример 1",
|
||||||
Phone = "+7 (777) 000-00-00",
|
Phone = "+7 (777) 000-00-00",
|
||||||
Email = "demo@food-market.local"
|
Email = "demo@food-market.local",
|
||||||
|
DefaultCurrencyId = kzt?.Id,
|
||||||
|
DefaultVat = 16,
|
||||||
};
|
};
|
||||||
db.Organizations.Add(demoOrg);
|
db.Organizations.Add(demoOrg);
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
}
|
}
|
||||||
|
else if (demoOrg.DefaultCurrencyId is null && kzt is not null)
|
||||||
|
{
|
||||||
|
// backfill для существующей организации на стенде
|
||||||
|
demoOrg.DefaultCurrencyId = kzt.Id;
|
||||||
|
if (demoOrg.DefaultVat == 0) demoOrg.DefaultVat = 16;
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
await SeedTenantReferencesAsync(db, demoOrg.Id, ct);
|
await SeedTenantReferencesAsync(db, demoOrg.Id, ct);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,8 @@ public class Country : Entity
|
||||||
public string Code { get; set; } = null!; // ISO 3166-1 alpha-2, e.g. "KZ"
|
public string Code { get; set; } = null!; // ISO 3166-1 alpha-2, e.g. "KZ"
|
||||||
public string Name { get; set; } = null!;
|
public string Name { get; set; } = null!;
|
||||||
public int SortOrder { get; set; }
|
public int SortOrder { get; set; }
|
||||||
|
/// <summary>Валюта по умолчанию для этой страны — при выборе страны в настройках
|
||||||
|
/// организации её валюта подтягивается автоматически.</summary>
|
||||||
|
public Guid? DefaultCurrencyId { get; set; }
|
||||||
|
public Currency? DefaultCurrency { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using foodmarket.Domain.Catalog;
|
||||||
using foodmarket.Domain.Common;
|
using foodmarket.Domain.Common;
|
||||||
|
|
||||||
namespace foodmarket.Domain.Organizations;
|
namespace foodmarket.Domain.Organizations;
|
||||||
|
|
@ -15,4 +16,17 @@ public class Organization : Entity
|
||||||
/// <summary>Персональный API-токен MoySklad. Храним per-organization чтобы
|
/// <summary>Персональный API-токен MoySklad. Храним per-organization чтобы
|
||||||
/// пользователю не нужно было вводить его каждый раз при импорте.</summary>
|
/// пользователю не нужно было вводить его каждый раз при импорте.</summary>
|
||||||
public string? MoySkladToken { get; set; }
|
public string? MoySkladToken { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Валюта организации по умолчанию. Если MultiCurrencyEnabled=false,
|
||||||
|
/// в UI выбор валюты скрыт — всё в этой валюте.</summary>
|
||||||
|
public Guid? DefaultCurrencyId { get; set; }
|
||||||
|
public Currency? DefaultCurrency { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Разрешены ли продажи/закупки в нескольких валютах. По умолчанию
|
||||||
|
/// false — тогда UI не предлагает выбор валюты, всё в DefaultCurrency.</summary>
|
||||||
|
public bool MultiCurrencyEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Ставка НДС по умолчанию для новых товаров (KZ=16%, RU=20%).
|
||||||
|
/// Само значение применяется к товару при создании; пользователь может менять.</summary>
|
||||||
|
public int DefaultVat { get; set; } = 16;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ protected override void OnModelCreating(ModelBuilder builder)
|
||||||
b.Property(o => o.CountryCode).HasMaxLength(2).IsRequired();
|
b.Property(o => o.CountryCode).HasMaxLength(2).IsRequired();
|
||||||
b.Property(o => o.Bin).HasMaxLength(20);
|
b.Property(o => o.Bin).HasMaxLength(20);
|
||||||
b.Property(o => o.MoySkladToken).HasMaxLength(200);
|
b.Property(o => o.MoySkladToken).HasMaxLength(200);
|
||||||
|
b.HasOne(o => o.DefaultCurrency).WithMany().HasForeignKey(o => o.DefaultCurrencyId).OnDelete(DeleteBehavior.Restrict);
|
||||||
b.HasIndex(o => o.Name);
|
b.HasIndex(o => o.Name);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ private static void ConfigureCountry(EntityTypeBuilder<Country> b)
|
||||||
b.ToTable("countries");
|
b.ToTable("countries");
|
||||||
b.Property(x => x.Code).HasMaxLength(2).IsRequired();
|
b.Property(x => x.Code).HasMaxLength(2).IsRequired();
|
||||||
b.Property(x => x.Name).HasMaxLength(100).IsRequired();
|
b.Property(x => x.Name).HasMaxLength(100).IsRequired();
|
||||||
|
b.HasOne(x => x.DefaultCurrency).WithMany().HasForeignKey(x => x.DefaultCurrencyId).OnDelete(DeleteBehavior.Restrict);
|
||||||
b.HasIndex(x => x.Code).IsUnique();
|
b.HasIndex(x => x.Code).IsUnique();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,83 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace foodmarket.Infrastructure.Persistence.Migrations
|
||||||
|
{
|
||||||
|
/// <summary>Country ↔ Currency связка + дефолты организации:
|
||||||
|
/// - countries.DefaultCurrencyId (nullable FK → currencies.Id)
|
||||||
|
/// - organizations.DefaultCurrencyId (FK → currencies.Id)
|
||||||
|
/// - organizations.MultiCurrencyEnabled (bool, default false)
|
||||||
|
/// - organizations.DefaultVat (int, default 16)
|
||||||
|
/// Seed: KZ→KZT, RU→RUB; org → KZ+KZT.</summary>
|
||||||
|
public partial class Phase4_CountryCurrencyOrgDefaults : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder b)
|
||||||
|
{
|
||||||
|
b.AddColumn<Guid>(
|
||||||
|
name: "DefaultCurrencyId", schema: "public", table: "countries",
|
||||||
|
type: "uuid", nullable: true);
|
||||||
|
|
||||||
|
b.AddColumn<Guid>(
|
||||||
|
name: "DefaultCurrencyId", schema: "public", table: "organizations",
|
||||||
|
type: "uuid", nullable: true);
|
||||||
|
b.AddColumn<bool>(
|
||||||
|
name: "MultiCurrencyEnabled", schema: "public", table: "organizations",
|
||||||
|
type: "boolean", nullable: false, defaultValue: false);
|
||||||
|
b.AddColumn<int>(
|
||||||
|
name: "DefaultVat", schema: "public", table: "organizations",
|
||||||
|
type: "integer", nullable: false, defaultValue: 16);
|
||||||
|
|
||||||
|
b.CreateIndex(
|
||||||
|
name: "IX_countries_DefaultCurrencyId", schema: "public",
|
||||||
|
table: "countries", column: "DefaultCurrencyId");
|
||||||
|
b.CreateIndex(
|
||||||
|
name: "IX_organizations_DefaultCurrencyId", schema: "public",
|
||||||
|
table: "organizations", column: "DefaultCurrencyId");
|
||||||
|
|
||||||
|
b.AddForeignKey(
|
||||||
|
name: "FK_countries_currencies_DefaultCurrencyId",
|
||||||
|
schema: "public", table: "countries", column: "DefaultCurrencyId",
|
||||||
|
principalSchema: "public", principalTable: "currencies", principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
b.AddForeignKey(
|
||||||
|
name: "FK_organizations_currencies_DefaultCurrencyId",
|
||||||
|
schema: "public", table: "organizations", column: "DefaultCurrencyId",
|
||||||
|
principalSchema: "public", principalTable: "currencies", principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
// Backfill: привяжем валюты к странам по ISO-коду.
|
||||||
|
b.Sql("""
|
||||||
|
UPDATE public.countries SET "DefaultCurrencyId" = c."Id"
|
||||||
|
FROM public.currencies c
|
||||||
|
WHERE (public.countries."Code" = 'KZ' AND c."Code" = 'KZT')
|
||||||
|
OR (public.countries."Code" = 'RU' AND c."Code" = 'RUB')
|
||||||
|
OR (public.countries."Code" = 'BY' AND c."Code" = 'BYN')
|
||||||
|
OR (public.countries."Code" = 'US' AND c."Code" = 'USD')
|
||||||
|
OR (public.countries."Code" = 'DE' AND c."Code" = 'EUR')
|
||||||
|
OR (public.countries."Code" = 'CN' AND c."Code" = 'CNY')
|
||||||
|
OR (public.countries."Code" = 'TR' AND c."Code" = 'TRY');
|
||||||
|
""");
|
||||||
|
|
||||||
|
// Дефолт для организации — KZT, если существует.
|
||||||
|
b.Sql("""
|
||||||
|
UPDATE public.organizations SET "DefaultCurrencyId" = c."Id"
|
||||||
|
FROM public.currencies c
|
||||||
|
WHERE c."Code" = 'KZT' AND public.organizations."DefaultCurrencyId" IS NULL;
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder b)
|
||||||
|
{
|
||||||
|
b.DropForeignKey(name: "FK_countries_currencies_DefaultCurrencyId", schema: "public", table: "countries");
|
||||||
|
b.DropForeignKey(name: "FK_organizations_currencies_DefaultCurrencyId", schema: "public", table: "organizations");
|
||||||
|
b.DropIndex(name: "IX_countries_DefaultCurrencyId", schema: "public", table: "countries");
|
||||||
|
b.DropIndex(name: "IX_organizations_DefaultCurrencyId", schema: "public", table: "organizations");
|
||||||
|
b.DropColumn(name: "DefaultCurrencyId", schema: "public", table: "countries");
|
||||||
|
b.DropColumn(name: "DefaultCurrencyId", schema: "public", table: "organizations");
|
||||||
|
b.DropColumn(name: "MultiCurrencyEnabled", schema: "public", table: "organizations");
|
||||||
|
b.DropColumn(name: "DefaultVat", schema: "public", table: "organizations");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -434,6 +434,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
b.Property<DateTime>("CreatedAt")
|
b.Property<DateTime>("CreatedAt")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("DefaultCurrencyId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(100)
|
.HasMaxLength(100)
|
||||||
|
|
@ -450,6 +453,8 @@ protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
b.HasIndex("Code")
|
b.HasIndex("Code")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("DefaultCurrencyId");
|
||||||
|
|
||||||
b.ToTable("countries", "public");
|
b.ToTable("countries", "public");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1071,6 +1076,12 @@ protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
b.Property<string>("Email")
|
b.Property<string>("Email")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid?>("DefaultCurrencyId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("DefaultVat")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
b.Property<bool>("IsActive")
|
b.Property<bool>("IsActive")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
|
@ -1078,6 +1089,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
.HasMaxLength(200)
|
.HasMaxLength(200)
|
||||||
.HasColumnType("character varying(200)");
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<bool>("MultiCurrencyEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(200)
|
.HasMaxLength(200)
|
||||||
|
|
@ -1091,6 +1105,8 @@ protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("DefaultCurrencyId");
|
||||||
|
|
||||||
b.HasIndex("Name");
|
b.HasIndex("Name");
|
||||||
|
|
||||||
b.ToTable("organizations", "public");
|
b.ToTable("organizations", "public");
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { CounterpartiesPage } from '@/pages/CounterpartiesPage'
|
||||||
import { ProductsPage } from '@/pages/ProductsPage'
|
import { ProductsPage } from '@/pages/ProductsPage'
|
||||||
import { ProductEditPage } from '@/pages/ProductEditPage'
|
import { ProductEditPage } from '@/pages/ProductEditPage'
|
||||||
import { MoySkladImportPage } from '@/pages/MoySkladImportPage'
|
import { MoySkladImportPage } from '@/pages/MoySkladImportPage'
|
||||||
|
import { OrganizationSettingsPage } from '@/pages/OrganizationSettingsPage'
|
||||||
import { StockPage } from '@/pages/StockPage'
|
import { StockPage } from '@/pages/StockPage'
|
||||||
import { StockMovementsPage } from '@/pages/StockMovementsPage'
|
import { StockMovementsPage } from '@/pages/StockMovementsPage'
|
||||||
import { SuppliesPage } from '@/pages/SuppliesPage'
|
import { SuppliesPage } from '@/pages/SuppliesPage'
|
||||||
|
|
@ -60,6 +61,7 @@ export default function App() {
|
||||||
<Route path="/sales/retail/new" element={<RetailSaleEditPage />} />
|
<Route path="/sales/retail/new" element={<RetailSaleEditPage />} />
|
||||||
<Route path="/sales/retail/:id" element={<RetailSaleEditPage />} />
|
<Route path="/sales/retail/:id" element={<RetailSaleEditPage />} />
|
||||||
<Route path="/admin/import/moysklad" element={<MoySkladImportPage />} />
|
<Route path="/admin/import/moysklad" element={<MoySkladImportPage />} />
|
||||||
|
<Route path="/settings/organization" element={<OrganizationSettingsPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@ import { api } from '@/lib/api'
|
||||||
import { logout } from '@/lib/auth'
|
import { logout } from '@/lib/auth'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import {
|
import {
|
||||||
LayoutDashboard, Package, FolderTree, Ruler, Percent, Tag,
|
LayoutDashboard, Package, FolderTree, Ruler, Tag,
|
||||||
Users, Warehouse, Store as StoreIcon, Globe, Coins, LogOut, Download,
|
Users, Warehouse, Store as StoreIcon, Globe, Coins, LogOut, Download,
|
||||||
Boxes, History, TruckIcon, ShoppingCart,
|
Boxes, History, TruckIcon, ShoppingCart, Settings,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { Logo } from './Logo'
|
import { Logo } from './Logo'
|
||||||
|
|
||||||
|
|
@ -26,7 +26,6 @@ const nav = [
|
||||||
{ to: '/catalog/products', icon: Package, label: 'Товары' },
|
{ to: '/catalog/products', icon: Package, label: 'Товары' },
|
||||||
{ to: '/catalog/product-groups', icon: FolderTree, label: 'Группы' },
|
{ to: '/catalog/product-groups', icon: FolderTree, label: 'Группы' },
|
||||||
{ to: '/catalog/units', icon: Ruler, label: 'Ед. измерения' },
|
{ to: '/catalog/units', icon: Ruler, label: 'Ед. измерения' },
|
||||||
{ to: '/catalog/vat-rates', icon: Percent, label: 'Ставки НДС' },
|
|
||||||
{ to: '/catalog/price-types', icon: Tag, label: 'Типы цен' },
|
{ to: '/catalog/price-types', icon: Tag, label: 'Типы цен' },
|
||||||
]},
|
]},
|
||||||
{ group: 'Контрагенты', items: [
|
{ group: 'Контрагенты', items: [
|
||||||
|
|
@ -53,6 +52,9 @@ const nav = [
|
||||||
{ group: 'Импорт', items: [
|
{ group: 'Импорт', items: [
|
||||||
{ to: '/admin/import/moysklad', icon: Download, label: 'МойСклад' },
|
{ to: '/admin/import/moysklad', icon: Download, label: 'МойСклад' },
|
||||||
]},
|
]},
|
||||||
|
{ group: 'Настройки', items: [
|
||||||
|
{ to: '/settings/organization', icon: Settings, label: 'Организация' },
|
||||||
|
]},
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export function AppLayout() {
|
export function AppLayout() {
|
||||||
|
|
|
||||||
21
src/food-market.web/src/lib/useOrgSettings.ts
Normal file
21
src/food-market.web/src/lib/useOrgSettings.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { api } from '@/lib/api'
|
||||||
|
|
||||||
|
export interface OrgSettings {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
countryCode: string
|
||||||
|
defaultCurrencyId: string | null
|
||||||
|
defaultCurrencyCode: string | null
|
||||||
|
defaultCurrencySymbol: string | null
|
||||||
|
multiCurrencyEnabled: boolean
|
||||||
|
defaultVat: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOrgSettings() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['/api/organization/settings'],
|
||||||
|
queryFn: async () => (await api.get<OrgSettings>('/api/organization/settings')).data,
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
})
|
||||||
|
}
|
||||||
116
src/food-market.web/src/pages/OrganizationSettingsPage.tsx
Normal file
116
src/food-market.web/src/pages/OrganizationSettingsPage.tsx
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { Save } from 'lucide-react'
|
||||||
|
import { api } from '@/lib/api'
|
||||||
|
import { PageHeader } from '@/components/PageHeader'
|
||||||
|
import { Button } from '@/components/Button'
|
||||||
|
import { Field, TextInput, Select, Checkbox } from '@/components/Field'
|
||||||
|
import { useCurrencies, useCountries } from '@/lib/useLookups'
|
||||||
|
import { useOrgSettings, type OrgSettings } from '@/lib/useOrgSettings'
|
||||||
|
|
||||||
|
const vatChoices = [0, 10, 12, 16, 20]
|
||||||
|
|
||||||
|
export function OrganizationSettingsPage() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
const settings = useOrgSettings()
|
||||||
|
const currencies = useCurrencies()
|
||||||
|
const countries = useCountries()
|
||||||
|
|
||||||
|
const [form, setForm] = useState<OrgSettings | null>(null)
|
||||||
|
useEffect(() => { if (settings.data && !form) setForm(settings.data) }, [settings.data, form])
|
||||||
|
|
||||||
|
// При смене страны подтягиваем её дефолтную валюту.
|
||||||
|
const onCountryChange = (countryCode: string) => {
|
||||||
|
if (!form) return
|
||||||
|
const country = countries.data?.find((c) => c.code === countryCode)
|
||||||
|
const fallbackByCode: Record<string, string | undefined> = { KZ: 'KZT', RU: 'RUB', BY: 'BYN', US: 'USD' }
|
||||||
|
const targetCode = fallbackByCode[countryCode]
|
||||||
|
const currency = targetCode ? currencies.data?.find((c) => c.code === targetCode) : undefined
|
||||||
|
setForm({
|
||||||
|
...form,
|
||||||
|
countryCode,
|
||||||
|
defaultCurrencyId: currency?.id ?? form.defaultCurrencyId,
|
||||||
|
defaultCurrencyCode: currency?.code ?? form.defaultCurrencyCode,
|
||||||
|
defaultCurrencySymbol: currency?.symbol ?? form.defaultCurrencySymbol,
|
||||||
|
})
|
||||||
|
void country // reserved for future use (sortOrder etc.)
|
||||||
|
}
|
||||||
|
|
||||||
|
const save = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
if (!form) return
|
||||||
|
const payload = {
|
||||||
|
name: form.name,
|
||||||
|
countryCode: form.countryCode,
|
||||||
|
defaultCurrencyId: form.defaultCurrencyId,
|
||||||
|
multiCurrencyEnabled: form.multiCurrencyEnabled,
|
||||||
|
defaultVat: form.defaultVat,
|
||||||
|
}
|
||||||
|
return (await api.put<OrgSettings>('/api/organization/settings', payload)).data
|
||||||
|
},
|
||||||
|
onSuccess: (d) => {
|
||||||
|
if (d) setForm(d)
|
||||||
|
qc.invalidateQueries({ queryKey: ['/api/organization/settings'] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!form) return <div className="p-6 text-sm text-slate-500">Загрузка…</div>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full overflow-auto">
|
||||||
|
<div className="p-6 max-w-2xl">
|
||||||
|
<PageHeader title="Настройки организации" description="Страна, валюта, ставка НДС по умолчанию." />
|
||||||
|
|
||||||
|
<section className="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-5 space-y-4">
|
||||||
|
<Field label="Название организации">
|
||||||
|
<TextInput value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Field label="Страна">
|
||||||
|
<Select value={form.countryCode} onChange={(e) => onCountryChange(e.target.value)}>
|
||||||
|
{countries.data?.map((c) => <option key={c.code} value={c.code}>{c.name}</option>)}
|
||||||
|
</Select>
|
||||||
|
</Field>
|
||||||
|
<Field label="Валюта по умолчанию">
|
||||||
|
<Select
|
||||||
|
value={form.defaultCurrencyId ?? ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const id = e.target.value
|
||||||
|
const c = currencies.data?.find((x) => x.id === id)
|
||||||
|
setForm({ ...form, defaultCurrencyId: id || null, defaultCurrencyCode: c?.code ?? null, defaultCurrencySymbol: c?.symbol ?? null })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">—</option>
|
||||||
|
{currencies.data?.map((c) => <option key={c.id} value={c.id}>{c.code} ({c.symbol})</option>)}
|
||||||
|
</Select>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
label="Разрешить продажи и закупки в нескольких валютах"
|
||||||
|
checked={form.multiCurrencyEnabled}
|
||||||
|
onChange={(v) => setForm({ ...form, multiCurrencyEnabled: v })}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-slate-500 -mt-2">
|
||||||
|
Если выключено — в формах продаж и закупок выбор валюты не показывается, всё считается в валюте по умолчанию.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Field label="Ставка НДС по умолчанию для новых товаров">
|
||||||
|
<Select value={form.defaultVat} onChange={(e) => setForm({ ...form, defaultVat: Number(e.target.value) })}>
|
||||||
|
{vatChoices.map((v) => <option key={v} value={v}>{v}%</option>)}
|
||||||
|
</Select>
|
||||||
|
</Field>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="mt-4 flex gap-3 items-center">
|
||||||
|
<Button onClick={() => save.mutate()} disabled={save.isPending}>
|
||||||
|
<Save className="w-4 h-4" /> {save.isPending ? 'Сохраняю…' : 'Сохранить'}
|
||||||
|
</Button>
|
||||||
|
{save.isSuccess && <span className="text-sm text-emerald-600">Сохранено</span>}
|
||||||
|
{save.error && <span className="text-sm text-red-600">{(save.error as Error).message}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import { Field, TextInput, TextArea, Select, Checkbox } from '@/components/Field
|
||||||
import {
|
import {
|
||||||
useUnits, useProductGroups, useCountries, useCurrencies, usePriceTypes, useSuppliers,
|
useUnits, useProductGroups, useCountries, useCurrencies, usePriceTypes, useSuppliers,
|
||||||
} from '@/lib/useLookups'
|
} from '@/lib/useLookups'
|
||||||
|
import { useOrgSettings } from '@/lib/useOrgSettings'
|
||||||
import { BarcodeType, type Product } from '@/lib/types'
|
import { BarcodeType, type Product } from '@/lib/types'
|
||||||
|
|
||||||
interface PriceRow { id?: string; priceTypeId: string; amount: number; currencyId: string }
|
interface PriceRow { id?: string; priceTypeId: string; amount: number; currencyId: string }
|
||||||
|
|
@ -63,6 +64,7 @@ export function ProductEditPage() {
|
||||||
const countries = useCountries()
|
const countries = useCountries()
|
||||||
const currencies = useCurrencies()
|
const currencies = useCurrencies()
|
||||||
const priceTypes = usePriceTypes()
|
const priceTypes = usePriceTypes()
|
||||||
|
const org = useOrgSettings()
|
||||||
const suppliers = useSuppliers()
|
const suppliers = useSuppliers()
|
||||||
|
|
||||||
const existing = useQuery({
|
const existing = useQuery({
|
||||||
|
|
@ -100,9 +102,16 @@ export function ProductEditPage() {
|
||||||
setForm((f) => ({ ...f, unitOfMeasureId: units.data?.[0]?.id ?? '' }))
|
setForm((f) => ({ ...f, unitOfMeasureId: units.data?.[0]?.id ?? '' }))
|
||||||
}
|
}
|
||||||
if (isNew && form.purchaseCurrencyId === '' && currencies.data?.length) {
|
if (isNew && form.purchaseCurrencyId === '' && currencies.data?.length) {
|
||||||
setForm((f) => ({ ...f, purchaseCurrencyId: currencies.data?.find(c => c.code === 'KZT')?.id ?? currencies.data?.[0]?.id ?? '' }))
|
const def = org.data?.defaultCurrencyId
|
||||||
|
? currencies.data?.find(c => c.id === org.data!.defaultCurrencyId)
|
||||||
|
: currencies.data?.find(c => c.code === 'KZT')
|
||||||
|
setForm((f) => ({ ...f, purchaseCurrencyId: def?.id ?? currencies.data?.[0]?.id ?? '' }))
|
||||||
}
|
}
|
||||||
}, [isNew, units.data, currencies.data, form.unitOfMeasureId, form.purchaseCurrencyId])
|
// Default VAT для нового товара берём из настроек организации.
|
||||||
|
if (isNew && org.data?.defaultVat !== undefined && form.vat === 16 && org.data.defaultVat !== 16) {
|
||||||
|
setForm((f) => ({ ...f, vat: org.data!.defaultVat }))
|
||||||
|
}
|
||||||
|
}, [isNew, units.data, currencies.data, org.data?.defaultCurrencyId, org.data?.defaultVat, form.unitOfMeasureId, form.purchaseCurrencyId, form.vat])
|
||||||
|
|
||||||
const save = useMutation({
|
const save = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
|
|
@ -278,12 +287,14 @@ export function ProductEditPage() {
|
||||||
<Field label="Закупочная цена">
|
<Field label="Закупочная цена">
|
||||||
<TextInput type="number" step="0.01" value={form.purchasePrice} onChange={(e) => setForm({ ...form, purchasePrice: e.target.value })} />
|
<TextInput type="number" step="0.01" value={form.purchasePrice} onChange={(e) => setForm({ ...form, purchasePrice: e.target.value })} />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Валюта закупки">
|
{org.data?.multiCurrencyEnabled && (
|
||||||
<Select value={form.purchaseCurrencyId} onChange={(e) => setForm({ ...form, purchaseCurrencyId: e.target.value })}>
|
<Field label="Валюта закупки">
|
||||||
<option value="">—</option>
|
<Select value={form.purchaseCurrencyId} onChange={(e) => setForm({ ...form, purchaseCurrencyId: e.target.value })}>
|
||||||
{currencies.data?.map((c) => <option key={c.id} value={c.id}>{c.code}</option>)}
|
<option value="">—</option>
|
||||||
</Select>
|
{currencies.data?.map((c) => <option key={c.id} value={c.id}>{c.code}</option>)}
|
||||||
</Field>
|
</Select>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
|
@ -308,19 +319,24 @@ export function ProductEditPage() {
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{form.prices.map((p, i) => (
|
{form.prices.map((p, i) => (
|
||||||
<div key={i} className="grid grid-cols-12 gap-2 items-center">
|
<div key={i} className="grid grid-cols-12 gap-2 items-center">
|
||||||
<div className="col-span-6">
|
<div className={org.data?.multiCurrencyEnabled ? 'col-span-6' : 'col-span-8'}>
|
||||||
<Select value={p.priceTypeId} onChange={(e) => updatePrice(i, { priceTypeId: e.target.value })}>
|
<Select value={p.priceTypeId} onChange={(e) => updatePrice(i, { priceTypeId: e.target.value })}>
|
||||||
{priceTypes.data?.map((pt) => <option key={pt.id} value={pt.id}>{pt.name}</option>)}
|
{priceTypes.data?.map((pt) => <option key={pt.id} value={pt.id}>{pt.name}</option>)}
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-3">
|
<div className="col-span-3 flex items-center gap-2">
|
||||||
<TextInput type="number" step="0.01" value={p.amount} onChange={(e) => updatePrice(i, { amount: Number(e.target.value) })} />
|
<TextInput type="number" step="0.01" value={p.amount} onChange={(e) => updatePrice(i, { amount: Number(e.target.value) })} />
|
||||||
|
{!org.data?.multiCurrencyEnabled && (
|
||||||
|
<span className="text-sm text-slate-500">{org.data?.defaultCurrencySymbol ?? ''}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2">
|
{org.data?.multiCurrencyEnabled && (
|
||||||
<Select value={p.currencyId} onChange={(e) => updatePrice(i, { currencyId: e.target.value })}>
|
<div className="col-span-2">
|
||||||
{currencies.data?.map((c) => <option key={c.id} value={c.id}>{c.code}</option>)}
|
<Select value={p.currencyId} onChange={(e) => updatePrice(i, { currencyId: e.target.value })}>
|
||||||
</Select>
|
{currencies.data?.map((c) => <option key={c.id} value={c.id}>{c.code}</option>)}
|
||||||
</div>
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => removePrice(i)}
|
onClick={() => removePrice(i)}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { Button } from '@/components/Button'
|
||||||
import { Field, TextInput, TextArea, Select } from '@/components/Field'
|
import { Field, TextInput, TextArea, Select } from '@/components/Field'
|
||||||
import { ProductPicker } from '@/components/ProductPicker'
|
import { ProductPicker } from '@/components/ProductPicker'
|
||||||
import { useStores, useCurrencies, useSuppliers } from '@/lib/useLookups'
|
import { useStores, useCurrencies, useSuppliers } from '@/lib/useLookups'
|
||||||
|
import { useOrgSettings } from '@/lib/useOrgSettings'
|
||||||
import { RetailSaleStatus, PaymentMethod, type RetailSaleDto, type Product } from '@/lib/types'
|
import { RetailSaleStatus, PaymentMethod, type RetailSaleDto, type Product } from '@/lib/types'
|
||||||
|
|
||||||
interface LineRow {
|
interface LineRow {
|
||||||
|
|
@ -51,6 +52,7 @@ export function RetailSaleEditPage() {
|
||||||
|
|
||||||
const stores = useStores()
|
const stores = useStores()
|
||||||
const currencies = useCurrencies()
|
const currencies = useCurrencies()
|
||||||
|
const org = useOrgSettings()
|
||||||
const customers = useSuppliers() // we re-use the suppliers hook (returns all counterparties) — fine for MVP
|
const customers = useSuppliers() // we re-use the suppliers hook (returns all counterparties) — fine for MVP
|
||||||
|
|
||||||
const [form, setForm] = useState<Form>(empty)
|
const [form, setForm] = useState<Form>(empty)
|
||||||
|
|
@ -95,10 +97,13 @@ export function RetailSaleEditPage() {
|
||||||
setForm((f) => ({ ...f, storeId: stores.data!.find((s) => s.isMain)?.id ?? stores.data![0].id }))
|
setForm((f) => ({ ...f, storeId: stores.data!.find((s) => s.isMain)?.id ?? stores.data![0].id }))
|
||||||
}
|
}
|
||||||
if (!form.currencyId && currencies.data?.length) {
|
if (!form.currencyId && currencies.data?.length) {
|
||||||
setForm((f) => ({ ...f, currencyId: currencies.data!.find((c) => c.code === 'KZT')?.id ?? currencies.data![0].id }))
|
const def = org.data?.defaultCurrencyId
|
||||||
|
? currencies.data.find((c) => c.id === org.data!.defaultCurrencyId)
|
||||||
|
: currencies.data.find((c) => c.code === 'KZT')
|
||||||
|
if (def) setForm((f) => ({ ...f, currencyId: def.id }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isNew, stores.data, currencies.data, form.storeId, form.currencyId])
|
}, [isNew, stores.data, currencies.data, org.data?.defaultCurrencyId, form.storeId, form.currencyId])
|
||||||
|
|
||||||
const isDraft = isNew || existing.data?.status === RetailSaleStatus.Draft
|
const isDraft = isNew || existing.data?.status === RetailSaleStatus.Draft
|
||||||
const isPosted = existing.data?.status === RetailSaleStatus.Posted
|
const isPosted = existing.data?.status === RetailSaleStatus.Posted
|
||||||
|
|
@ -253,13 +258,15 @@ export function RetailSaleEditPage() {
|
||||||
{stores.data?.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
|
{stores.data?.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
|
||||||
</Select>
|
</Select>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Валюта *">
|
{org.data?.multiCurrencyEnabled && (
|
||||||
<Select value={form.currencyId} disabled={isPosted}
|
<Field label="Валюта *">
|
||||||
onChange={(e) => setForm({ ...form, currencyId: e.target.value })}>
|
<Select value={form.currencyId} disabled={isPosted}
|
||||||
<option value="">—</option>
|
onChange={(e) => setForm({ ...form, currencyId: e.target.value })}>
|
||||||
{currencies.data?.map((c) => <option key={c.id} value={c.id}>{c.code}</option>)}
|
<option value="">—</option>
|
||||||
</Select>
|
{currencies.data?.map((c) => <option key={c.id} value={c.id}>{c.code}</option>)}
|
||||||
</Field>
|
</Select>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
<Field label="Покупатель (опц.)">
|
<Field label="Покупатель (опц.)">
|
||||||
<Select value={form.customerId} disabled={isPosted}
|
<Select value={form.customerId} disabled={isPosted}
|
||||||
onChange={(e) => setForm({ ...form, customerId: e.target.value })}>
|
onChange={(e) => setForm({ ...form, customerId: e.target.value })}>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { Button } from '@/components/Button'
|
||||||
import { Field, TextInput, TextArea, Select } from '@/components/Field'
|
import { Field, TextInput, TextArea, Select } from '@/components/Field'
|
||||||
import { ProductPicker } from '@/components/ProductPicker'
|
import { ProductPicker } from '@/components/ProductPicker'
|
||||||
import { useStores, useCurrencies, useSuppliers } from '@/lib/useLookups'
|
import { useStores, useCurrencies, useSuppliers } from '@/lib/useLookups'
|
||||||
|
import { useOrgSettings } from '@/lib/useOrgSettings'
|
||||||
import { SupplyStatus, type SupplyDto, type Product } from '@/lib/types'
|
import { SupplyStatus, type SupplyDto, type Product } from '@/lib/types'
|
||||||
|
|
||||||
interface LineRow {
|
interface LineRow {
|
||||||
|
|
@ -48,6 +49,7 @@ export function SupplyEditPage() {
|
||||||
|
|
||||||
const stores = useStores()
|
const stores = useStores()
|
||||||
const currencies = useCurrencies()
|
const currencies = useCurrencies()
|
||||||
|
const org = useOrgSettings()
|
||||||
const suppliers = useSuppliers()
|
const suppliers = useSuppliers()
|
||||||
|
|
||||||
const [form, setForm] = useState<Form>(emptyForm)
|
const [form, setForm] = useState<Form>(emptyForm)
|
||||||
|
|
@ -92,14 +94,16 @@ export function SupplyEditPage() {
|
||||||
setForm((f) => ({ ...f, storeId: main.id }))
|
setForm((f) => ({ ...f, storeId: main.id }))
|
||||||
}
|
}
|
||||||
if (!form.currencyId && currencies.data?.length) {
|
if (!form.currencyId && currencies.data?.length) {
|
||||||
const kzt = currencies.data.find((c) => c.code === 'KZT') ?? currencies.data[0]
|
const def = org.data?.defaultCurrencyId
|
||||||
setForm((f) => ({ ...f, currencyId: kzt.id }))
|
? currencies.data.find((c) => c.id === org.data!.defaultCurrencyId)
|
||||||
|
: currencies.data.find((c) => c.code === 'KZT')
|
||||||
|
if (def) setForm((f) => ({ ...f, currencyId: def.id }))
|
||||||
}
|
}
|
||||||
if (!form.supplierId && suppliers.data?.length) {
|
if (!form.supplierId && suppliers.data?.length) {
|
||||||
setForm((f) => ({ ...f, supplierId: suppliers.data![0].id }))
|
setForm((f) => ({ ...f, supplierId: suppliers.data![0].id }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isNew, stores.data, currencies.data, suppliers.data, form.storeId, form.currencyId, form.supplierId])
|
}, [isNew, stores.data, currencies.data, suppliers.data, org.data?.defaultCurrencyId, form.storeId, form.currencyId, form.supplierId])
|
||||||
|
|
||||||
const isDraft = isNew || existing.data?.status === SupplyStatus.Draft
|
const isDraft = isNew || existing.data?.status === SupplyStatus.Draft
|
||||||
const isPosted = existing.data?.status === SupplyStatus.Posted
|
const isPosted = existing.data?.status === SupplyStatus.Posted
|
||||||
|
|
@ -252,13 +256,15 @@ export function SupplyEditPage() {
|
||||||
{stores.data?.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
|
{stores.data?.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
|
||||||
</Select>
|
</Select>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Валюта *">
|
{org.data?.multiCurrencyEnabled && (
|
||||||
<Select value={form.currencyId} disabled={isPosted}
|
<Field label="Валюта *">
|
||||||
onChange={(e) => setForm({ ...form, currencyId: e.target.value })}>
|
<Select value={form.currencyId} disabled={isPosted}
|
||||||
<option value="">—</option>
|
onChange={(e) => setForm({ ...form, currencyId: e.target.value })}>
|
||||||
{currencies.data?.map((c) => <option key={c.id} value={c.id}>{c.code}</option>)}
|
<option value="">—</option>
|
||||||
</Select>
|
{currencies.data?.map((c) => <option key={c.id} value={c.id}>{c.code}</option>)}
|
||||||
</Field>
|
</Select>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
<Field label="№ накладной поставщика">
|
<Field label="№ накладной поставщика">
|
||||||
<TextInput value={form.supplierInvoiceNumber} disabled={isPosted}
|
<TextInput value={form.supplierInvoiceNumber} disabled={isPosted}
|
||||||
onChange={(e) => setForm({ ...form, supplierInvoiceNumber: e.target.value })} />
|
onChange={(e) => setForm({ ...form, supplierInvoiceNumber: e.target.value })} />
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue