ИНН — российское поле. В Казахстане физлица используют ИИН (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 при импорте контрагента (как и было).
183 lines
9.3 KiB
TypeScript
183 lines
9.3 KiB
TypeScript
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>
|
||
)
|
||
}
|