Some checks failed
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 1m1s
CI / Web (React + Vite) (push) Failing after 37s
Docker Public / Build + push Public (push) Successful in 27s
Docker Web / Build + push Web (push) Failing after 25s
Docker Web / Deploy Web on stage (push) Has been skipped
Docker Public / Deploy Public on stage (push) Successful in 10s
Поле телефона во всех формах (web + public) теперь использует общий компонент: - Префикс «+7 » всегда виден и не удаляется (Backspace/Delete на позиции ≤3 блокируется). - Принимаются только цифры (буквы и спецсимволы автоматически отфильтровываются). - Авто-форматирование при вводе: «+7 7XX XXX XX XX». - Paste произвольного формата нормализуется (поддерживается ведущая «8», «+7…», скобки, дефисы). - Наружу через onChange отдаётся каноничное «+7XXXXXXXXXX». Подключено в: - food-market.public/SignupForm - food-market.web/CounterpartiesPage (контрагенты) - food-market.web/EmployeesPage (сотрудники) - food-market.web/SuperAdminOrgEmployeesPage (управление сотрудниками SuperAdmin) - food-market.web/SuperAdminOrgCreatePage (создание организации SuperAdmin)
144 lines
7.4 KiB
TypeScript
144 lines
7.4 KiB
TypeScript
import { useState } from 'react'
|
||
import { useNavigate } from 'react-router-dom'
|
||
import { Save, Copy } from 'lucide-react'
|
||
import { api } from '@/lib/api'
|
||
import { Button } from '@/components/Button'
|
||
import { Modal } from '@/components/Modal'
|
||
import { Field, TextInput, Select } from '@/components/Field'
|
||
import { PhoneInput } from '@/components/PhoneInput'
|
||
import { useCountries, useCurrencies } from '@/lib/useLookups'
|
||
import { PageHeader } from '@/components/PageHeader'
|
||
|
||
export function SuperAdminOrgCreatePage() {
|
||
const navigate = useNavigate()
|
||
const countries = useCountries()
|
||
const currencies = useCurrencies()
|
||
const [name, setName] = useState('')
|
||
const [countryCode, setCountryCode] = useState('KZ')
|
||
const [bin, setBin] = useState('')
|
||
const [address, setAddress] = useState('')
|
||
const [phone, setPhone] = useState('')
|
||
const [email, setEmail] = useState('')
|
||
const [defaultCurrencyId, setDefaultCurrencyId] = 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 onCountryChange = (cc: string) => {
|
||
setCountryCode(cc)
|
||
const c = countries.data?.find((x) => x.code === cc)
|
||
if (c?.defaultCurrencyId) setDefaultCurrencyId(c.defaultCurrencyId)
|
||
}
|
||
|
||
const submit = async () => {
|
||
setError(null); setBusy(true)
|
||
try {
|
||
const res = await api.post<{ adminEmail: string; adminTempPassword: string; organization: { id: string } }>(
|
||
'/api/super-admin/organizations',
|
||
{
|
||
org: { name, countryCode, bin: bin || null, address: address || null,
|
||
phone: phone || null, email: 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) }
|
||
}
|
||
|
||
const canSubmit = name.trim() && countryCode && adminLast.trim() && adminFirst.trim() && adminEmail.trim()
|
||
|
||
return (
|
||
<div className="h-full overflow-auto">
|
||
<div className="max-w-2xl mx-auto p-4 sm:p-6 space-y-5">
|
||
<PageHeader title="Новая организация" description="Создаётся вместе с Администратором (AppUser + Employee запись)." />
|
||
{error && <div className="p-3 rounded-md bg-red-50 text-red-700 text-sm border border-red-200">{error}</div>}
|
||
<section className="rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 p-5 space-y-3">
|
||
<h3 className="text-sm font-semibold">Реквизиты организации</h3>
|
||
<Field label="Название *">
|
||
<TextInput value={name} onChange={(e) => setName(e.target.value)} />
|
||
</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)} /></Field>
|
||
<Field label="Телефон"><PhoneInput value={phone} onChange={setPhone} /></Field>
|
||
</div>
|
||
<Field label="Адрес"><TextInput value={address} onChange={(e) => setAddress(e.target.value)} /></Field>
|
||
<Field label="Email организации"><TextInput type="email" value={email} onChange={(e) => setEmail(e.target.value)} /></Field>
|
||
</section>
|
||
|
||
<section className="rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 p-5 space-y-3">
|
||
<h3 className="text-sm font-semibold">Главный администратор</h3>
|
||
<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>
|
||
<p className="text-xs text-slate-500">
|
||
Будет сгенерирован временный пароль и показан один раз — передайте его сотруднику.
|
||
</p>
|
||
</section>
|
||
|
||
<div className="flex gap-3">
|
||
<Button variant="secondary" onClick={() => navigate('/super-admin/organizations')}>Отмена</Button>
|
||
<Button onClick={submit} disabled={!canSubmit || busy}>
|
||
<Save className="w-4 h-4" /> {busy ? 'Создаю…' : 'Создать организацию'}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<Modal open={!!done} onClose={() => { setDone(null); navigate('/super-admin/organizations') }}
|
||
title="Организация создана"
|
||
footer={<Button onClick={() => { setDone(null); navigate('/super-admin/organizations') }}>Готово</Button>}>
|
||
{done && (
|
||
<div className="space-y-3">
|
||
<p className="text-sm text-slate-600 dark:text-slate-400">
|
||
Передайте администратору данные для входа. Это окно показывается один раз.
|
||
</p>
|
||
<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>
|
||
)}
|
||
</Modal>
|
||
</div>
|
||
)
|
||
}
|