sozsoft-platform/ui/src/components/orders/TenantForm.tsx

876 lines
36 KiB
TypeScript
Raw Normal View History

2026-02-24 20:44:16 +00:00
import { getTenantByNameDetail } from '@/services/tenant.service'
import { CustomTenantDto } from '@/proxy/config/models'
import {
FaArrowLeft,
FaArrowRight,
FaDollarSign,
FaMoneyBillWave,
FaBuilding,
FaGlobe,
FaEnvelope,
FaMap,
FaMapPin,
FaPhone,
FaSearch,
FaUser,
FaUserPlus,
} from 'react-icons/fa'
import React, { useEffect, useState } from 'react'
2026-02-24 20:44:16 +00:00
import { useNavigate } from 'react-router-dom'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { Button, Select } from '@/components/ui'
import {
CityDto,
CountryDto,
DistrictDto,
getCities,
getCountries,
getDistricts,
} from '@/services/location.service'
interface LocationOption<T> {
label: string
value: string
data: T
}
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
const DEFAULT_PHONE_FORMAT = '000 000 000 000 000'
const TURKEY_PHONE_FORMAT = '(000) 000-0000'
const REQUIRED_NEW_CUSTOMER_FIELDS: Array<keyof CustomTenantDto> = [
'organizationName',
'founder',
'country',
'city',
'district',
'postalCode',
'address1',
'phoneNumber',
'email',
'taxOffice',
'vknTckn',
]
const REQUIRED_EXISTING_CUSTOMER_FIELDS: Array<keyof CustomTenantDto> = [
'id',
'name',
'organizationName',
'founder',
'country',
'city',
'district',
'address1',
'email',
]
const getPhoneFormat = (country?: CountryDto) =>
country?.phoneNumberFormat ||
(country?.phoneCode === 90 ? TURKEY_PHONE_FORMAT : DEFAULT_PHONE_FORMAT)
const getPhoneMaxLength = (country?: CountryDto) =>
country?.phoneNumberMaxLength ?? getPhoneFormat(country).replace(/[^0]/g, '').length
const getPhoneMinLength = (country?: CountryDto) =>
country?.phoneNumberMinLength ?? getPhoneMaxLength(country)
const getNationalPhoneDigits = (value: string, country?: CountryDto) => {
let digits = value.replace(/\D/g, '')
if (!country) return digits
const code = String(country.phoneCode)
if (value.trimStart().startsWith('+') && digits.startsWith(code)) {
digits = digits.slice(code.length)
}
return digits
}
const getPhoneValidationError = (value: string, country?: CountryDto) => {
if (!country) return 'Lütfen önce ülke seçin.'
const length = getNationalPhoneDigits(value, country).length
const minLength = getPhoneMinLength(country)
const maxLength = getPhoneMaxLength(country)
if (length >= minLength && length <= maxLength) return ''
return minLength === maxLength
? `Telefon numarası ${minLength} haneli olmalıdır.`
: `Telefon numarası ${minLength}-${maxLength} hane arasında olmalıdır.`
}
const formatPhoneNumber = (value: string, country?: CountryDto) => {
if (!country) return value.replace(/\D/g, '').slice(0, 15)
const maxLength = getPhoneMaxLength(country)
const digits = getNationalPhoneDigits(value, country).slice(0, maxLength)
const format = getPhoneFormat(country)
let digitIndex = 0
let nationalNumber = ''
for (const character of format) {
if (character === '0') {
if (digitIndex >= digits.length) break
nationalNumber += digits[digitIndex]
digitIndex += 1
} else if (digitIndex === 0) {
if (digits.length > 0) nationalNumber += character
} else if (digitIndex < digits.length || character === ')') {
nationalNumber += character
} else {
break
}
}
return `+${country.phoneCode}${nationalNumber ? ` ${nationalNumber}` : ''}`
}
const isBlankValue = (value: unknown) =>
value === undefined || value === null || String(value).trim() === ''
const hasRequiredFields = (
tenant: Partial<CustomTenantDto>,
fields: Array<keyof CustomTenantDto>,
) => fields.every((field) => !isBlankValue(tenant[field]))
const normalizeCode = (value?: string) => value?.trim().toLowerCase() || ''
2026-02-24 20:44:16 +00:00
interface TenantFormProps {
onSubmit: (tenant: CustomTenantDto) => void
onBack: () => void
}
export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
const [isExisting, setIsExisting] = useState<boolean>(true)
const [formData, setFormData] = useState<Partial<CustomTenantDto>>({})
const [countries, setCountries] = useState<CountryDto[]>([])
const [cities, setCities] = useState<CityDto[]>([])
const [districts, setDistricts] = useState<DistrictDto[]>([])
const [isLoadingCountries, setIsLoadingCountries] = useState(false)
const [isLoadingCities, setIsLoadingCities] = useState(false)
const [isLoadingDistricts, setIsLoadingDistricts] = useState(false)
const [isLoadingTenant, setIsLoadingTenant] = useState(false)
const [emailError, setEmailError] = useState('')
const [phoneError, setPhoneError] = useState('')
const [existingTenantError, setExistingTenantError] = useState('')
const [formError, setFormError] = useState('')
const [foundTenantName, setFoundTenantName] = useState('')
2026-02-24 20:44:16 +00:00
const navigate = useNavigate()
const { translate } = useLocalization()
useEffect(() => {
const loadCountries = async () => {
setIsLoadingCountries(true)
try {
const response = await getCountries()
setCountries(response.data)
const defaultCountry = response.data.find((country) => country.name === 'Türkiye')
if (defaultCountry) {
setFormData((prev) =>
prev.country
? prev
: {
...prev,
country: defaultCountry.name,
city: '',
district: '',
phoneNumber: `+${defaultCountry.phoneCode}`,
},
)
setIsLoadingCities(true)
try {
const cityResponse = await getCities(defaultCountry.name)
setCities(cityResponse.data)
} catch (error) {
console.error('Şehirler alınırken hata:', error)
} finally {
setIsLoadingCities(false)
}
}
} catch (error) {
console.error('Ülkeler alınırken hata:', error)
} finally {
setIsLoadingCountries(false)
}
}
void loadCountries()
}, [])
const selectedCountry = countries.find((country) => country.name === formData.country)
const phoneMask = selectedCountry
? `+${selectedCountry.phoneCode} ${getPhoneFormat(selectedCountry)}`
: DEFAULT_PHONE_FORMAT
const canSubmitExistingTenant =
Boolean(
formData.id &&
formData.organizationName &&
normalizeCode(foundTenantName) === normalizeCode(formData.name),
) && !isLoadingTenant
2026-02-24 20:44:16 +00:00
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
let hasValidationError = false
setFormError('')
if (isExisting) {
if (!formData.name?.trim()) {
setExistingTenantError('Lütfen önce organization code girin.')
return
}
if (
!formData.id ||
normalizeCode(foundTenantName) !== normalizeCode(formData.name) ||
!formData.organizationName
) {
setExistingTenantError('Devam etmek için önce organization code ile kurum bilgisini bulun.')
return
}
if (!hasRequiredFields(formData, REQUIRED_EXISTING_CUSTOMER_FIELDS)) {
setExistingTenantError('Kurum bilgileri eksik geldi. Lütfen kurum kodunu tekrar aratın.')
return
}
onSubmit({ ...formData, isExisting: true } as CustomTenantDto)
return
}
if (!hasRequiredFields(formData, REQUIRED_NEW_CUSTOMER_FIELDS)) {
setFormError('Lütfen zorunlu alanları eksiksiz doldurun.')
hasValidationError = true
}
const nextPhoneError = getPhoneValidationError(formData.phoneNumber || '', selectedCountry)
setPhoneError(nextPhoneError)
if (nextPhoneError) {
hasValidationError = true
}
if (!isExisting && !EMAIL_PATTERN.test(formData.email || '')) {
setEmailError('Lütfen geçerli bir e-posta adresi girin.')
hasValidationError = true
}
if (hasValidationError) return
onSubmit({ ...formData, isExisting: false } as CustomTenantDto)
2026-02-24 20:44:16 +00:00
}
const handleInputChange = (field: keyof CustomTenantDto, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }))
}
const resetExistingTenantDetails = (name: string) => {
setFoundTenantName('')
setExistingTenantError('')
setFormError('')
setFormData({ name })
}
const handleCountryChange = async (option: LocationOption<CountryDto> | null) => {
const country = option?.data
setFormData((prev) => ({
...prev,
country: country?.name || '',
city: '',
district: '',
phoneNumber: country ? `+${country.phoneCode}` : '',
}))
setCities([])
setDistricts([])
setPhoneError('')
if (!country) return
setIsLoadingCities(true)
try {
const response = await getCities(country.name)
setCities(response.data)
} catch (error) {
console.error('Şehirler alınırken hata:', error)
} finally {
setIsLoadingCities(false)
}
}
const handleCityChange = async (option: LocationOption<CityDto> | null) => {
const city = option?.data
setFormData((prev) => ({ ...prev, city: city?.name || '', district: '' }))
setDistricts([])
if (!city || !formData.country) return
setIsLoadingDistricts(true)
try {
const response = await getDistricts(formData.country, city.name)
setDistricts(response.data)
} catch (error) {
console.error('İlçeler alınırken hata:', error)
} finally {
setIsLoadingDistricts(false)
}
}
const countryOptions: LocationOption<CountryDto>[] = countries.map((country) => ({
label: country.name,
value: country.name,
data: country,
}))
const cityOptions: LocationOption<CityDto>[] = cities.map((city) => ({
label: city.name,
value: city.name,
data: city,
}))
const districtOptions: LocationOption<DistrictDto>[] = districts.map((district) => ({
label: district.name,
value: district.name,
data: district,
}))
2026-02-24 20:44:16 +00:00
const getTenantInfo = async () => {
const tenantName = formData.name?.trim()
if (!tenantName) {
setExistingTenantError('Lütfen önce organization code girin.')
return
}
setIsLoadingTenant(true)
setExistingTenantError('')
setFormError('')
2026-02-24 20:44:16 +00:00
try {
const tenant = await getTenantByNameDetail(tenantName)
2026-02-24 20:44:16 +00:00
setFormData((prev) => ({
...prev,
id: tenant.data.id,
name: tenant.data.name,
isActive: tenant.data.isActive,
organizationName: tenant.data.organizationName,
founder: tenant.data.founder,
vknTckn: tenant.data.vknTckn,
taxOffice: tenant.data.taxOffice,
address1: tenant.data.address1,
address2: tenant.data.address2,
district: tenant.data.district,
country: tenant.data.country,
city: tenant.data.city,
postalCode: tenant.data.postalCode,
phoneNumber: tenant.data.phoneNumber,
mobileNumber: tenant.data.mobileNumber,
faxNumber: tenant.data.faxNumber,
email: tenant.data.email,
website: tenant.data.website,
menuGroup: tenant.data.menuGroup,
}))
setFoundTenantName(tenant.data.name)
2026-02-24 20:44:16 +00:00
} catch (error) {
setFoundTenantName('')
setExistingTenantError('Kurum bulunamadı. Lütfen organization code bilgisini kontrol edin.')
2026-02-24 20:44:16 +00:00
console.error('Kurum bilgisi alınırken hata:', error)
} finally {
setIsLoadingTenant(false)
2026-02-24 20:44:16 +00:00
}
}
return (
<div className="flex flex-col lg:flex-row gap-6 mb-6">
2026-05-26 16:09:26 +00:00
<div className="w-full lg:w-1/3 bg-white rounded-xl shadow-lg border border-gray-200 p-6 dark:border-gray-700 dark:bg-gray-900 dark:shadow-gray-950/40">
2026-02-24 20:44:16 +00:00
<div className="mb-6">
<h2 className="text-lg font-semibold mb-4 flex items-center">
<FaUser className="w-5 h-5 text-green-600 mr-2" />{' '}
{translate('::Public.payment.customerInfo')}
</h2>
</div>
<div className="space-y-4">
<div className="grid grid-cols-1 gap-4">
<Button
2026-02-24 20:44:16 +00:00
onClick={() => setIsExisting(true)}
variant="plain"
className={`block h-auto w-full p-4 border-2 rounded-xl transition-all text-left select-none ${
2026-02-24 20:44:16 +00:00
isExisting === true
2026-05-26 16:09:26 +00:00
? 'border-blue-500 bg-blue-50 shadow-md scale-[1.02] dark:bg-blue-950/30'
: 'border-gray-200 hover:border-blue-300 dark:border-gray-700 dark:hover:border-blue-500'
2026-02-24 20:44:16 +00:00
}`}
>
2026-05-26 16:09:26 +00:00
<div className="font-semibold text-gray-900 dark:text-gray-100 mb-2">
2026-02-24 20:44:16 +00:00
{translate('::Public.products.tenantForm.existing.title')}
</div>
2026-05-26 16:09:26 +00:00
<div className="text-sm text-gray-600 dark:text-gray-400">
2026-02-24 20:44:16 +00:00
{translate('::Public.products.tenantForm.existing.desc')}
</div>
</Button>
2026-02-24 20:44:16 +00:00
<Button
2026-02-24 20:44:16 +00:00
onClick={() => setIsExisting(false)}
variant="plain"
className={`block h-auto w-full p-4 border-2 rounded-xl transition-all text-left select-none ${
2026-02-24 20:44:16 +00:00
isExisting === false
2026-05-26 16:09:26 +00:00
? 'border-blue-500 bg-blue-50 shadow-md scale-[1.02] dark:bg-blue-950/30'
: 'border-gray-200 hover:border-blue-300 dark:border-gray-700 dark:hover:border-blue-500'
2026-02-24 20:44:16 +00:00
}`}
>
2026-05-26 16:09:26 +00:00
<div className="font-semibold text-gray-900 dark:text-gray-100 mb-2">
2026-02-24 20:44:16 +00:00
{translate('::Public.products.tenantForm.new.title')}
</div>
2026-05-26 16:09:26 +00:00
<div className="text-sm text-gray-600 dark:text-gray-400">
2026-02-24 20:44:16 +00:00
{translate('::Public.products.tenantForm.new.desc')}
</div>
</Button>
2026-02-24 20:44:16 +00:00
</div>
</div>
</div>
2026-05-26 16:09:26 +00:00
<div className="w-full lg:w-2/3 bg-white rounded-xl shadow-lg border border-gray-200 p-6 dark:border-gray-700 dark:bg-gray-900 dark:shadow-gray-950/40">
2026-02-24 20:44:16 +00:00
{isExisting !== null && (
<form onSubmit={handleSubmit} className="space-y-6">
{isExisting ? (
<div>
2026-05-26 16:09:26 +00:00
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
2026-02-24 20:44:16 +00:00
{translate('::Public.payment.customer.code')}
</label>
<div className="relative flex flex-col sm:flex-row sm:items-stretch gap-3">
2026-02-24 20:44:16 +00:00
<div className="relative w-full">
<FaBuilding className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type="text"
required
autoFocus
placeholder="Enter your organization code"
value={formData.name || ''}
onChange={(e) => resetExistingTenantDetails(e.target.value)}
2026-02-24 20:44:16 +00:00
onKeyDown={async (e) => {
if (e.key === 'Enter') {
e.preventDefault()
await getTenantInfo()
}
}}
className="h-[40px] w-full pl-10 pr-4 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 sm:rounded-l-lg sm:rounded-r-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
2026-02-24 20:44:16 +00:00
/>
</div>
<Button
2026-02-24 20:44:16 +00:00
type="button"
disabled={isLoadingTenant || !formData.name?.trim()}
2026-02-24 20:44:16 +00:00
onClick={getTenantInfo}
icon={<FaSearch className="w-4 h-4" />}
variant="solid"
size="sm"
className="!h-[40px] min-w-[180px] sm:mr-2 sm:rounded-r-lg sm:rounded-l-none [&>span]:gap-2"
2026-02-24 20:44:16 +00:00
>
{isLoadingTenant
? 'Aranıyor...'
: translate('::Public.products.tenantForm.searchOrg')}
</Button>
2026-02-24 20:44:16 +00:00
</div>
{existingTenantError && (
<p className="mt-2 text-sm text-red-600">{existingTenantError}</p>
)}
{!existingTenantError &&
formData.name &&
!canSubmitExistingTenant &&
!isLoadingTenant && (
<p className="mt-2 text-sm text-amber-600">
Devam etmek için önce Find Organization ile kurum bilgilerini getirin.
</p>
)}
2026-02-24 20:44:16 +00:00
{formData.organizationName && (
2026-05-26 16:09:26 +00:00
<div className="grid grid-cols-1 gap-y-3 text-sm text-gray-700 dark:text-gray-300 p-3">
2026-02-24 20:44:16 +00:00
<div className="flex items-center gap-2">
<FaUser className="w-4 h-4 text-gray-500" />
<span className="font-medium">{translate('::LoginPanel.Profil')}</span>
2026-02-24 20:44:16 +00:00
<span>{formData.founder}</span>
</div>
<div className="flex items-center gap-2">
<FaBuilding className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Public.payment.customer.company')}
</span>
<span>{formData.organizationName}</span>
</div>
<div className="flex items-center gap-2">
<FaEnvelope className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Abp.Account.EmailAddress')}:
</span>
<span>{formData.email}</span>
</div>
<div className="flex items-center gap-2">
<FaPhone className="w-4 h-4 text-gray-500" />
<span className="font-medium">{translate('::blog.categories.mobile')}:</span>
2026-02-24 20:44:16 +00:00
<span>{formData.mobileNumber}</span>
</div>
<div className="flex items-start gap-2">
<FaMapPin className="w-4 h-4 text-gray-500 mt-0.5" />
<div>
<span className="font-medium">{translate('::App.Address')}:</span>
2026-02-24 20:44:16 +00:00
<div>{formData.address1}</div>
</div>
</div>
<div className="flex items-center gap-2">
<FaGlobe className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Public.payment.customer.country')}:
</span>
<span>{formData.country}</span>
</div>
<div className="flex items-center gap-2">
<FaGlobe className="w-4 h-4 text-gray-500" />
<span className="font-medium">{translate('::Public.common.city')}:</span>
2026-02-24 20:44:16 +00:00
<span>{formData.city}</span>
</div>
<div className="flex items-center gap-2">
<FaMap className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Public.payment.customer.district')}:
</span>
<span>{formData.district}</span>
</div>
<div className="flex items-center gap-2">
<FaMapPin className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Public.payment.customer.postalCode')}:
</span>
<span>{formData.postalCode}</span>
</div>
<div className="flex items-center gap-2">
<FaMoneyBillWave className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Public.contact.taxOffice')}:
</span>
<span>{formData.taxOffice}</span>
</div>
<div className="flex items-center gap-2">
<FaDollarSign className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Public.contact.taxNumber')}:
</span>
<span>{formData.vknTckn}</span>
</div>
{formData.reference && (
<div className="flex items-center gap-2">
<FaUserPlus className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Public.payment.customer.reference')}:
</span>
<span>{formData.reference}</span>
</div>
)}
</div>
)}
</div>
) : (
<div className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
2026-05-26 16:09:26 +00:00
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
2026-02-24 20:44:16 +00:00
{translate('::Public.products.tenantForm.orgName')}
</label>
<div className="relative">
<FaBuilding className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type="text"
required
autoFocus
placeholder="Enter your organization name"
value={formData.organizationName || ''}
onChange={(e) => handleInputChange('organizationName', e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
2026-02-24 20:44:16 +00:00
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
2026-02-24 20:44:16 +00:00
{translate('::LoginPanel.Profil')}
</label>
<div className="relative">
<FaUser className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type="text"
required
placeholder="Enter founder's name"
value={formData.founder || ''}
onChange={(e) => handleInputChange('founder', e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
2026-02-24 20:44:16 +00:00
/>
</div>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{translate('::Public.payment.customer.country')}
2026-02-24 20:44:16 +00:00
</label>
<div className="relative">
<Select<LocationOption<CountryDto>>
2026-02-24 20:44:16 +00:00
required
isClearable
isLoading={isLoadingCountries}
options={countryOptions}
value={
countryOptions.find((option) => option.value === formData.country) || null
}
onChange={handleCountryChange}
placeholder="Ülke seçin"
2026-02-24 20:44:16 +00:00
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{translate('::Public.common.city')}
2026-02-24 20:44:16 +00:00
</label>
<div className="relative">
<Select<LocationOption<CityDto>>
2026-02-24 20:44:16 +00:00
required
isClearable
isDisabled={!formData.country}
isLoading={isLoadingCities}
options={cityOptions}
value={cityOptions.find((option) => option.value === formData.city) || null}
onChange={handleCityChange}
placeholder={formData.country ? 'Şehir seçin' : 'Önce ülke seçin'}
2026-02-24 20:44:16 +00:00
/>
</div>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{translate('::Public.payment.customer.district')}
2026-02-24 20:44:16 +00:00
</label>
<div className="relative">
<Select<LocationOption<DistrictDto>>
required
isClearable
isDisabled={!formData.city}
isLoading={isLoadingDistricts}
options={districtOptions}
value={
districtOptions.find((option) => option.value === formData.district) ||
null
}
onChange={(option) =>
handleInputChange('district', option?.data.name || '')
}
placeholder={formData.city ? 'İlçe seçin' : 'Önce şehir seçin'}
2026-02-24 20:44:16 +00:00
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{translate('::Public.payment.customer.postalCode')}
2026-02-24 20:44:16 +00:00
</label>
<div className="relative">
<FaMapPin className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
2026-02-24 20:44:16 +00:00
<input
type="text"
required
placeholder="34782"
value={formData.postalCode || ''}
onChange={(e) => handleInputChange('postalCode', e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
2026-02-24 20:44:16 +00:00
/>
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{translate('::App.Address')}
</label>
<div className="relative">
<FaMapPin className="absolute left-3 top-3 text-gray-400 w-5 h-5" />
<textarea
required
placeholder="Enter your address"
value={formData.address1 || ''}
onChange={(e) => handleInputChange('address1', e.target.value)}
rows={3}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
</div>
2026-02-24 20:44:16 +00:00
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{translate('::Abp.Identity.User.UserInformation.PhoneNumber')}
2026-02-24 20:44:16 +00:00
</label>
<div className="relative">
<FaPhone className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
2026-02-24 20:44:16 +00:00
<input
type="tel"
2026-02-24 20:44:16 +00:00
required
inputMode="numeric"
placeholder={phoneMask.replace(/0/g, '_')}
aria-label={`Telefon numarası, format: ${phoneMask}`}
value={formData.phoneNumber || ''}
onChange={(e) => {
handleInputChange(
'phoneNumber',
formatPhoneNumber(e.target.value, selectedCountry),
)
if (phoneError) setPhoneError('')
}}
onBlur={() =>
setPhoneError(
getPhoneValidationError(formData.phoneNumber || '', selectedCountry),
)
}
aria-invalid={Boolean(phoneError)}
aria-describedby={phoneError ? 'tenant-phone-error' : undefined}
title={`Beklenen format: ${phoneMask}`}
className={`w-full pl-10 pr-4 py-2 border rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
phoneError
? 'border-red-500 dark:border-red-500'
: 'border-gray-300 dark:border-gray-700'
}`}
2026-02-24 20:44:16 +00:00
/>
</div>
{phoneError && (
<p id="tenant-phone-error" className="mt-1 text-sm text-red-600">
{phoneError}
</p>
)}
2026-02-24 20:44:16 +00:00
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{translate('::Abp.Account.EmailAddress')}
2026-02-24 20:44:16 +00:00
</label>
<div className="relative">
<FaEnvelope className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
2026-02-24 20:44:16 +00:00
<input
type="email"
2026-02-24 20:44:16 +00:00
required
placeholder="sample@email.com"
value={formData.email || ''}
onChange={(e) => {
handleInputChange('email', e.target.value)
if (emailError) {
setEmailError(
EMAIL_PATTERN.test(e.target.value)
? ''
: 'Lütfen geçerli bir e-posta adresi girin.',
)
}
}}
onBlur={() =>
setEmailError(
EMAIL_PATTERN.test(formData.email || '')
? ''
: 'Lütfen geçerli bir e-posta adresi girin.',
)
}
aria-invalid={Boolean(emailError)}
aria-describedby={emailError ? 'tenant-email-error' : undefined}
className={`w-full pl-10 pr-4 py-2 border rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
emailError
? 'border-red-500 dark:border-red-500'
: 'border-gray-300 dark:border-gray-700'
}`}
2026-02-24 20:44:16 +00:00
/>
</div>
{emailError && (
<p id="tenant-email-error" className="mt-1 text-sm text-red-600">
{emailError}
</p>
)}
2026-02-24 20:44:16 +00:00
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
2026-02-24 20:44:16 +00:00
{translate('::Public.contact.taxOffice')}
</label>
<div className="relative">
<FaBuilding className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type="text"
required
placeholder="Kozyatağı"
value={formData.taxOffice}
onChange={(e) => handleInputChange('taxOffice', e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
2026-02-24 20:44:16 +00:00
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
2026-02-24 20:44:16 +00:00
{translate('::Public.contact.taxNumber')}
</label>
<div className="relative">
<FaDollarSign className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type="text"
required
placeholder="1234567890"
value={formData.vknTckn}
onChange={(e) => handleInputChange('vknTckn', e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
2026-02-24 20:44:16 +00:00
/>
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
2026-02-24 20:44:16 +00:00
{translate('::Public.payment.customer.reference')}
</label>
<div className="relative">
<FaUserPlus className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type="text"
placeholder={translate('::Public.payment.customer.reference')}
value={formData.reference || ''}
onChange={(e) => handleInputChange('reference', e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg bg-white text-gray-900 placeholder-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
2026-02-24 20:44:16 +00:00
/>
</div>
</div>
</div>
)}
{formError && <p className="mt-2 text-sm text-red-600">{formError}</p>}
2026-02-24 20:44:16 +00:00
<div className="flex justify-between items-center mt-6">
<Button
2026-02-24 20:44:16 +00:00
type="button"
onClick={() => navigate(ROUTES_ENUM.public.products)}
icon={<FaArrowLeft className="w-4 h-4" />}
variant="default"
size="sm"
2026-02-24 20:44:16 +00:00
>
2026-03-29 08:59:07 +00:00
{translate('::Back')}
</Button>
<Button
2026-02-24 20:44:16 +00:00
type="submit"
disabled={isExisting && !canSubmitExistingTenant}
icon={<FaArrowRight className="w-5 h-5" />}
variant="solid"
color="green-600"
size="sm"
2026-02-24 20:44:16 +00:00
>
{translate('::Abp.Account.ResetPassword.Continue')}
</Button>
2026-02-24 20:44:16 +00:00
</div>
</form>
)}
</div>
</div>
)
}