food-market/src/food-market.web/src/pages/SuperAdminSetupPage.tsx
nns dcb28a9811 feat(localization): убрать «ИНН» из UI — РК использует ИИН/БИН
ИНН — российское поле. В Казахстане физлица используют ИИН (12 цифр),
юрлица — БИН (12 цифр). Колонок «inn» в БД у нас нет (есть Bin для
юрлица + TaxNumber для ИИН физлица), миграция drop column не нужна —
только лейблы и комментарии.

Поправлено:
- EmployeesPage: «ИИН/ИНН» → «ИИН» (12 цифр, inputMode=numeric).
- CounterpartiesPage: убрано поле «ИНН / другой» — оставлены БИН и ИИН
  с правильными ограничениями (12 цифр, numeric).
- SuperAdminOrgCreatePage / SuperAdminSetupPage: «БИН/ИНН» → «БИН».
- MoySkladImportPage: «(ИНН ...)» в ответе test → «(идентификатор ...)».
- Domain comments в Employee.cs / Counterparty.cs обновлены.

Внутренний MoySklad-DTO `Inn` оставлен — это входящий JSON из российского
API МойСклад, поле там действительно так называется. Маппится в Bin
при импорте контрагента (как и было).
2026-05-06 11:31:42 +05:00

183 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { ArrowRight, ArrowLeft, Check, Copy } from 'lucide-react'
import { api } from '@/lib/api'
import { Button } from '@/components/Button'
import { Field, TextInput, Select } from '@/components/Field'
import { useCountries, useCurrencies } from '@/lib/useLookups'
interface SetupStatus { needsSetup: boolean; orgCount: number }
export function SuperAdminSetupPage() {
const navigate = useNavigate()
const [step, setStep] = useState(1)
const [name, setName] = useState('')
const [countryCode, setCountryCode] = useState('KZ')
const [defaultCurrencyId, setDefaultCurrencyId] = useState('')
const [bin, setBin] = useState('')
const [phone, setPhone] = useState('')
const [adminLast, setAdminLast] = useState('')
const [adminFirst, setAdminFirst] = useState('')
const [adminEmail, setAdminEmail] = useState('')
const [adminPosition, setAdminPosition] = useState('Директор')
const [busy, setBusy] = useState(false)
const [error, setError] = useState<string | null>(null)
const [done, setDone] = useState<{ email: string; password: string } | null>(null)
const countries = useCountries()
const currencies = useCurrencies()
useEffect(() => {
// Если уже есть организации — не показываем wizard, выкидываем на дашборд.
api.get<SetupStatus>('/api/super-admin/setup-status').then((r) => {
if (!r.data.needsSetup) navigate('/super-admin', { replace: true })
}).catch(() => {})
}, [navigate])
const onCountryChange = (cc: string) => {
setCountryCode(cc)
const c = countries.data?.find((x) => x.code === cc)
if (c?.defaultCurrencyId) setDefaultCurrencyId(c.defaultCurrencyId)
}
const finish = async () => {
setError(null); setBusy(true)
try {
const res = await api.post<{ adminEmail: string; adminTempPassword: string }>(
'/api/super-admin/organizations',
{
org: { name, countryCode, bin: bin || null, address: null, phone: phone || null, email: null,
defaultCurrencyId: defaultCurrencyId || null, accountOwnerUserId: null },
adminLastName: adminLast, adminFirstName: adminFirst,
adminEmail, adminPosition: adminPosition || null,
})
setDone({ email: res.data.adminEmail, password: res.data.adminTempPassword })
} catch (e) {
const msg = (e as { response?: { data?: { error?: string } } }).response?.data?.error ?? (e as Error).message
setError(msg)
} finally { setBusy(false) }
}
if (done) {
return (
<div className="h-full overflow-auto">
<div className="max-w-xl mx-auto p-4 sm:p-8 space-y-5">
<div className="text-center space-y-2">
<div className="w-16 h-16 mx-auto rounded-full bg-emerald-100 text-emerald-600 flex items-center justify-center">
<Check className="w-8 h-8" />
</div>
<h1 className="text-2xl font-bold">Готово!</h1>
<p className="text-slate-600 dark:text-slate-400">Организация создана. Сохраните данные для входа администратора.</p>
</div>
<div className="rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 p-5 space-y-3">
<Field label="Логин (email)">
<div className="flex gap-2">
<TextInput value={done.email} readOnly />
<Button variant="secondary" size="sm" onClick={() => navigator.clipboard?.writeText(done.email)}>
<Copy className="w-4 h-4" />
</Button>
</div>
</Field>
<Field label="Временный пароль">
<div className="flex gap-2">
<TextInput value={done.password} readOnly className="font-mono" />
<Button variant="secondary" size="sm" onClick={() => navigator.clipboard?.writeText(done.password)}>
<Copy className="w-4 h-4" />
</Button>
</div>
</Field>
</div>
<Button onClick={() => navigate('/super-admin')}>Перейти к консоли</Button>
</div>
</div>
)
}
const canStep2 = name.trim() && countryCode && defaultCurrencyId
const canStep3 = adminLast.trim() && adminFirst.trim() && adminEmail.trim()
return (
<div className="h-full overflow-auto">
<div className="max-w-xl mx-auto p-4 sm:p-8 space-y-5">
<div>
<div className="text-xs text-slate-500 mb-1">Шаг {step} из 3</div>
<div className="h-1.5 rounded-full bg-slate-100 dark:bg-slate-800 overflow-hidden">
<div className="h-full bg-[var(--color-brand)] transition-all" style={{ width: `${(step / 3) * 100}%` }} />
</div>
</div>
{error && <div className="p-3 rounded-md bg-red-50 text-red-700 text-sm border border-red-200">{error}</div>}
{step === 1 && (
<section className="rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 p-6 space-y-3">
<h2 className="text-xl font-bold">Добро пожаловать в Food Market</h2>
<p className="text-slate-600 dark:text-slate-400">
Этот установщик поможет создать первую организацию и подготовить систему к работе.
Займёт минуту: укажете данные магазина, заведёте первого администратора и можно работать.
</p>
<Button onClick={() => setStep(2)}>
Поехали <ArrowRight className="w-4 h-4" />
</Button>
</section>
)}
{step === 2 && (
<section className="rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 p-6 space-y-3">
<h2 className="text-xl font-bold">Данные организации</h2>
<Field label="Название организации *">
<TextInput value={name} onChange={(e) => setName(e.target.value)} placeholder="Например, Магазин у дома" />
</Field>
<div className="grid grid-cols-2 gap-3">
<Field label="Страна *">
<Select value={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={defaultCurrencyId} onChange={(e) => setDefaultCurrencyId(e.target.value)}>
<option value=""></option>
{currencies.data?.map((c) => <option key={c.id} value={c.id}>{c.code} ({c.symbol})</option>)}
</Select>
</Field>
</div>
<div className="grid grid-cols-2 gap-3">
<Field label="БИН"><TextInput value={bin} onChange={(e) => setBin(e.target.value)} placeholder="12 цифр" maxLength={12} inputMode="numeric" /></Field>
<Field label="Телефон"><TextInput value={phone} onChange={(e) => setPhone(e.target.value)} /></Field>
</div>
<div className="flex gap-2">
<Button variant="secondary" onClick={() => setStep(1)}><ArrowLeft className="w-4 h-4" /> Назад</Button>
<Button onClick={() => setStep(3)} disabled={!canStep2}>Далее <ArrowRight className="w-4 h-4" /></Button>
</div>
</section>
)}
{step === 3 && (
<section className="rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 p-6 space-y-3">
<h2 className="text-xl font-bold">Первый администратор</h2>
<p className="text-sm text-slate-500">
Этот пользователь получит полный доступ к организации. Временный пароль будет
сгенерирован и показан на следующем шаге.
</p>
<div className="grid grid-cols-2 gap-3">
<Field label="Фамилия *"><TextInput value={adminLast} onChange={(e) => setAdminLast(e.target.value)} /></Field>
<Field label="Имя *"><TextInput value={adminFirst} onChange={(e) => setAdminFirst(e.target.value)} /></Field>
</div>
<Field label="Email (логин) *">
<TextInput type="email" value={adminEmail} onChange={(e) => setAdminEmail(e.target.value)} />
</Field>
<Field label="Должность">
<TextInput value={adminPosition} onChange={(e) => setAdminPosition(e.target.value)} />
</Field>
<div className="flex gap-2">
<Button variant="secondary" onClick={() => setStep(2)}><ArrowLeft className="w-4 h-4" /> Назад</Button>
<Button onClick={finish} disabled={!canStep3 || busy}>
{busy ? 'Создаю…' : <>Готово <Check className="w-4 h-4" /></>}
</Button>
</div>
</section>
)}
</div>
</div>
)
}