sozsoft-platform/ui/src/components/orders/TenantForm.tsx
2026-08-31 17:17:50 +03:00

1161 lines
47 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 { 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,
FaCheckCircle,
FaSitemap,
} from 'react-icons/fa'
import React, { useEffect, useRef, useState } from 'react'
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'
import { useStoreActions, useStoreState } from '@/store/store'
import type { TenantFormDraft } from '@/store/client.model'
import Input from '@/components/ui/Input'
interface LocationOption<T> {
label: string
value: string
data: T
}
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
const WEBSITE_PATTERN = /^(https?:\/\/)?(www\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(\/[^\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',
'township',
'postalCode',
'address1',
'phoneNumber',
'email',
'taxOffice',
'vknTckn',
]
const REQUIRED_EXISTING_CUSTOMER_FIELDS: Array<keyof CustomTenantDto> = [
'id',
'name',
'organizationName',
'founder',
'country',
'city',
'district',
'township',
'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 | undefined,
translate: (key: string, params?: Record<string, string | number>) => string,
) => {
if (!country) return translate('::App.Tenant.SelectCountryFirst')
const length = getNationalPhoneDigits(value, country).length
const minLength = getPhoneMinLength(country)
const maxLength = getPhoneMaxLength(country)
if (length >= minLength && length <= maxLength) return ''
return minLength === maxLength
? translate('::App.Tenant.PhoneExactLength', { length: minLength })
: translate('::App.Tenant.PhoneRangeLength', { min: minLength, max: maxLength })
}
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 normalizePhoneForStorage = (value?: string, country?: CountryDto) =>
value ? getNationalPhoneDigits(value, country) : value
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() || ''
const getDistrictPostalCode = (district?: DistrictDto) =>
district?.postalCode ||
(district as (DistrictDto & { PostalCode?: string }) | undefined)?.PostalCode ||
''
const getUniqueLocationOptions = <T,>(
items: T[],
getKey: (item: T) => string,
getLabel: (item: T) => string,
): LocationOption<T>[] => {
const seen = new Set<string>()
return items.reduce<LocationOption<T>[]>((options, item) => {
const key = getKey(item)
if (!key || seen.has(key)) return options
seen.add(key)
options.push({
label: getLabel(item),
value: key,
data: item,
})
return options
}, [])
}
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 [websiteError, setWebsiteError] = useState('')
const [phoneError, setPhoneError] = useState('')
const [existingTenantError, setExistingTenantError] = useState('')
const [formError, setFormError] = useState('')
const [foundTenantName, setFoundTenantName] = useState('')
const navigate = useNavigate()
const { translate } = useLocalization()
const { tenantData, tenantFormDraft } = useStoreState((state) => state.client.order)
const { setTenantFormDraft, clearTenant } = useStoreActions((actions) => actions.client.order)
const hasRestoredTenantData = useRef(false)
useEffect(() => {
if (hasRestoredTenantData.current) return
hasRestoredTenantData.current = true
const savedData: TenantFormDraft | null = tenantFormDraft || tenantData
if (!savedData) return
setIsExisting(savedData.isExisting ?? true)
setFoundTenantName(savedData.foundTenantName || savedData.name || '')
setFormData(savedData)
}, [tenantData, tenantFormDraft])
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: '',
township: '',
postalCode: '',
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()
}, [])
useEffect(() => {
if (Object.keys(formData).length === 0) return
setTenantFormDraft({
...formData,
isExisting,
foundTenantName,
})
}, [formData, foundTenantName, isExisting, setTenantFormDraft])
useEffect(() => {
const loadLocationOptions = async () => {
if (!formData.country) return
setIsLoadingCities(true)
try {
const cityResponse = await getCities(formData.country)
setCities(cityResponse.data)
} catch (error) {
console.error('Şehirler alınırken hata:', error)
} finally {
setIsLoadingCities(false)
}
if (!formData.city) return
setIsLoadingDistricts(true)
try {
const districtResponse = await getDistricts(formData.country, formData.city)
setDistricts(districtResponse.data)
} catch (error) {
console.error('İlçeler alınırken hata:', error)
} finally {
setIsLoadingDistricts(false)
}
}
void loadLocationOptions()
}, [formData.country, formData.city])
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
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
let hasValidationError = false
setFormError('')
if (isExisting) {
if (!formData.name?.trim()) {
setExistingTenantError(translate('::App.Tenant.EnterOrgCodeFirst'))
return
}
if (
!formData.id ||
normalizeCode(foundTenantName) !== normalizeCode(formData.name) ||
!formData.organizationName
) {
setExistingTenantError(translate('::App.Tenant.FindOrgBeforeContinue'))
return
}
if (!hasRequiredFields(formData, REQUIRED_EXISTING_CUSTOMER_FIELDS)) {
setExistingTenantError(translate('::App.Tenant.OrgDataIncomplete'))
return
}
onSubmit({
...formData,
phoneNumber: normalizePhoneForStorage(formData.phoneNumber, selectedCountry),
mobileNumber: normalizePhoneForStorage(formData.mobileNumber, selectedCountry),
faxNumber: normalizePhoneForStorage(formData.faxNumber, selectedCountry),
isExisting: true,
} as CustomTenantDto)
return
}
if (!hasRequiredFields(formData, REQUIRED_NEW_CUSTOMER_FIELDS)) {
setFormError(translate('::App.Tenant.FillRequiredFields'))
hasValidationError = true
}
const nextPhoneError = getPhoneValidationError(
formData.phoneNumber || '',
selectedCountry,
translate,
)
setPhoneError(nextPhoneError)
if (nextPhoneError) {
hasValidationError = true
}
if (!isExisting && !EMAIL_PATTERN.test(formData.email || '')) {
setEmailError(translate('::App.Platform.InvalidEmail'))
hasValidationError = true
}
if (!isExisting && !WEBSITE_PATTERN.test(formData.website || '')) {
setWebsiteError(translate('::App.Tenant.InvalidWebsite'))
hasValidationError = true
}
if (hasValidationError) return
onSubmit({
...formData,
phoneNumber: normalizePhoneForStorage(formData.phoneNumber, selectedCountry),
mobileNumber: normalizePhoneForStorage(formData.mobileNumber, selectedCountry),
faxNumber: normalizePhoneForStorage(formData.faxNumber, selectedCountry),
isExisting: false,
} as CustomTenantDto)
}
const handleInputChange = (field: keyof CustomTenantDto, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }))
}
const getDefaultNewCustomerData = (): Partial<CustomTenantDto> => {
const defaultCountry = countries.find((country) => country.name === 'Türkiye')
return defaultCountry
? {
country: defaultCountry.name,
city: '',
district: '',
township: '',
postalCode: '',
phoneNumber: `+${defaultCountry.phoneCode}`,
}
: {}
}
const handleCustomerTypeChange = (nextIsExisting: boolean) => {
if (nextIsExisting === isExisting) return
setIsExisting(nextIsExisting)
setFoundTenantName('')
setExistingTenantError('')
setFormError('')
setEmailError('')
setWebsiteError('')
setPhoneError('')
setDistricts([])
setFormData(nextIsExisting ? {} : getDefaultNewCustomerData())
clearTenant()
}
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: '',
township: '',
postalCode: '',
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: '',
township: '',
postalCode: '',
}))
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 handleDistrictChange = (option: LocationOption<DistrictDto> | null) => {
setFormData((prev) => ({
...prev,
district: option?.data.name || '',
township: '',
postalCode: '',
}))
}
const handleTownshipChange = (option: LocationOption<DistrictDto> | null) => {
setFormData((prev) => ({
...prev,
township: option?.data.township || '',
postalCode: getDistrictPostalCode(option?.data),
}))
}
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 = getUniqueLocationOptions(
districts,
(district) => district.name,
(district) => district.name,
)
const townshipOptions = getUniqueLocationOptions(
districts.filter((district) => district.name === formData.district && district.township),
(district) => `${district.township || ''}:${getDistrictPostalCode(district)}`,
(district) => district.township || '',
)
const getTenantInfo = async () => {
const tenantName = formData.name?.trim()
if (!tenantName) {
setExistingTenantError(translate('::App.Tenant.EnterOrgCodeFirst'))
return
}
setIsLoadingTenant(true)
setExistingTenantError('')
setFormError('')
try {
const tenant = await getTenantByNameDetail(tenantName)
setFormData((prev) => ({
...prev,
id: tenant.data.id,
name: tenant.data.name,
organizationName: tenant.data.organizationName,
founder: tenant.data.founder,
vknTckn: tenant.data.vknTckn,
taxOffice: tenant.data.taxOffice,
country: tenant.data.country,
city: tenant.data.city,
district: tenant.data.district,
township: tenant.data.township,
postalCode: tenant.data.postalCode,
phoneNumber: tenant.data.phoneNumber,
mobileNumber: tenant.data.mobileNumber,
faxNumber: tenant.data.faxNumber,
address1: tenant.data.address1,
address2: tenant.data.address2,
email: tenant.data.email,
website: tenant.data.website,
modules: tenant.data.modules,
}))
if (tenant.data.name) setFoundTenantName(tenant.data.name)
} catch (error) {
setFoundTenantName('')
setExistingTenantError(translate('::App.Tenant.OrgNotFound'))
console.error('Kurum bilgisi alınırken hata:', error)
} finally {
setIsLoadingTenant(false)
}
}
return (
<div className="flex flex-col lg:flex-row gap-6 mb-6">
<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">
<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('::App.PublicPayment.CustomerInfo')}
</h2>
</div>
<div className="space-y-4">
<div className="grid grid-cols-1 gap-4">
<Button
onClick={() => handleCustomerTypeChange(true)}
variant="plain"
className={`relative block h-auto w-full p-4 border-2 rounded-xl transition-all text-left select-none shadow-sm ${
isExisting === true
? 'border-blue-600 bg-blue-100 shadow-lg ring-4 ring-blue-200 scale-[1.02] dark:border-sky-400 dark:bg-sky-950/60 dark:ring-sky-500/30'
: 'border-gray-200 bg-white hover:border-blue-400 hover:bg-blue-50/60 dark:border-gray-700 dark:bg-gray-800/70 dark:hover:border-sky-500 dark:hover:bg-sky-950/30'
}`}
>
{isExisting === true && (
<FaCheckCircle className="absolute right-4 top-4 h-5 w-5 text-blue-600 dark:text-sky-300" />
)}
<div className="font-semibold text-gray-900 dark:text-gray-100 mb-2">
{translate('::App.Listform.ListformField.IsExisting')}
</div>
<div className="text-sm text-gray-700 dark:text-gray-300">
{translate('::App.TenantFormExisting.ExistingDesc')}
</div>
</Button>
<Button
onClick={() => handleCustomerTypeChange(false)}
variant="plain"
className={`relative block h-auto w-full p-4 border-2 rounded-xl transition-all text-left select-none shadow-sm ${
isExisting === false
? 'border-blue-600 bg-blue-100 shadow-lg ring-4 ring-blue-200 scale-[1.02] dark:border-sky-400 dark:bg-sky-950/60 dark:ring-sky-500/30'
: 'border-gray-200 bg-white hover:border-blue-400 hover:bg-blue-50/60 dark:border-gray-700 dark:bg-gray-800/70 dark:hover:border-sky-500 dark:hover:bg-sky-950/30'
}`}
>
{isExisting === false && (
<FaCheckCircle className="absolute right-4 top-4 h-5 w-5 text-blue-600 dark:text-sky-300" />
)}
<div className="font-semibold text-gray-900 dark:text-gray-100 mb-2">
{translate('::App.TenantFormNew.NewTitle')}
</div>
<div className="text-sm text-gray-700 dark:text-gray-300">
{translate('::App.TenantFormNew.NewDesc')}
</div>
</Button>
</div>
</div>
</div>
<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">
{isExisting !== null && (
<form onSubmit={handleSubmit} className="space-y-6">
{isExisting ? (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{translate('::App.Platform.Organization')}
</label>
<div className="relative flex flex-col sm:flex-row sm:items-stretch gap-3">
<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
unstyle
type="text"
required
autoFocus
placeholder="Enter your organization code"
value={formData.name || ''}
onChange={(e) => resetExistingTenantDetails(e.target.value)}
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"
/>
</div>
<Button
type="button"
disabled={isLoadingTenant || !formData.name?.trim()}
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"
>
{isLoadingTenant
? translate('::App.Tenant.Searching')
: translate('::App.ProductsTenantForm.SearchOrg')}
</Button>
</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">
{translate('::App.Tenant.FindOrgHint')}
</p>
)}
{formData.organizationName && (
<div className="grid grid-cols-1 gap-y-3 text-sm text-gray-700 dark:text-gray-300 p-3">
<div className="flex items-center gap-2">
<FaBuilding className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::App.Platform.Company')}
</span>
<span>{formData.organizationName}</span>
</div>
<div className="flex items-center gap-2">
<FaUser className="w-4 h-4 text-gray-500" />
<span className="font-medium">{translate('::App.Listform.ListformField.Founder')}</span>
<span>{formData.founder}</span>
</div>
<div className="flex items-center gap-2">
<FaEnvelope className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::App.Listform.ListformField.Email')}:
</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('::Abp.Identity.User.UserInformation.PhoneNumber')}:
</span>
<span>{formData.phoneNumber}</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.Listform.ListformField.Address')}:</span>
<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('::App.Listform.ListformField.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('::App.Listform.ListformField.City')}:</span>
<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('::App.Listform.ListformField.District')}:
</span>
<span>{formData.district}</span>
</div>
<div className="flex items-center gap-2">
<FaMap className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::App.Listform.ListformField.Township')}:
</span>
<span>{formData.township}</span>
</div>
<div className="flex items-center gap-2">
<FaMapPin className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::App.Listform.ListformField.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('::App.PublicContact.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('::App.PublicContact.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('::App.StaticLookup.Reference')}:
</span>
<span>{formData.reference}</span>
</div>
)}
</div>
)}
</div>
) : (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{translate('::App.Platform.Company')}
</label>
<div className="relative">
<FaBuilding className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<Input
unstyle
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"
/>
</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('::App.Listform.ListformField.Founder')}
</label>
<div className="relative">
<FaUser className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<Input
unstyle
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"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{translate('::App.Listform.ListformField.Country')}
</label>
<div className="relative">
<Select<LocationOption<CountryDto>>
required
isClearable
isLoading={isLoadingCountries}
options={countryOptions}
value={
countryOptions.find((option) => option.value === formData.country) || null
}
onChange={handleCountryChange}
placeholder={translate('::App.Tenant.SelectCountry')}
/>
</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('::App.Listform.ListformField.City')}
</label>
<div className="relative">
<Select<LocationOption<CityDto>>
required
isClearable
isDisabled={!formData.country}
isLoading={isLoadingCities}
options={cityOptions}
value={cityOptions.find((option) => option.value === formData.city) || null}
onChange={handleCityChange}
placeholder={translate(
formData.country
? '::App.Tenant.SelectCity'
: '::App.Tenant.SelectCountryFirstShort',
)}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{translate('::App.Listform.ListformField.District')}
</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={handleDistrictChange}
placeholder={translate(
formData.city
? '::App.Tenant.SelectDistrict'
: '::App.Tenant.SelectCityFirst',
)}
/>
</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('::App.Listform.ListformField.Township')}
</label>
<div className="relative">
<Select<LocationOption<DistrictDto>>
required
isClearable
isDisabled={!formData.district}
options={townshipOptions}
value={
townshipOptions.find(
(option) => option.data.township === formData.township,
) || null
}
onChange={handleTownshipChange}
placeholder={translate(
formData.district
? '::App.Tenant.SelectTownship'
: '::App.Tenant.SelectDistrictFirst',
)}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{translate('::App.Listform.ListformField.PostalCode')}
</label>
<div className="relative">
<FaMapPin className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<Input
unstyle
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"
/>
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{translate('::App.Listform.ListformField.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>
<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')}
</label>
<div className="relative">
<FaPhone className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<Input
unstyle
type="tel"
required
inputMode="numeric"
placeholder={phoneMask.replace(/0/g, '_')}
aria-label={translate('::App.Tenant.PhoneAria', { mask: phoneMask })}
value={formData.phoneNumber || ''}
onChange={(e) => {
handleInputChange(
'phoneNumber',
formatPhoneNumber(e.target.value, selectedCountry),
)
if (phoneError) setPhoneError('')
}}
onBlur={() =>
setPhoneError(
getPhoneValidationError(
formData.phoneNumber || '',
selectedCountry,
translate,
),
)
}
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'
}`}
/>
</div>
{phoneError && (
<p id="tenant-phone-error" className="mt-1 text-sm text-red-600">
{phoneError}
</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{translate('::App.Listform.ListformField.Email')}
</label>
<div className="relative">
<FaEnvelope className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<Input
unstyle
type="email"
required
placeholder="sample@email.com"
value={formData.email || ''}
onChange={(e) => {
handleInputChange('email', e.target.value)
if (emailError) {
setEmailError(
EMAIL_PATTERN.test(e.target.value)
? ''
: translate('::App.Email.PatternError'),
)
}
}}
onBlur={() =>
setEmailError(
EMAIL_PATTERN.test(formData.email || '')
? ''
: translate('::App.Email.PatternError'),
)
}
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'
}`}
/>
</div>
{emailError && (
<p id="tenant-email-error" className="mt-1 text-sm text-red-600">
{emailError}
</p>
)}
</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('::App.Listform.ListformField.Website')}
</label>
<div className="relative">
<FaSitemap className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<Input
unstyle
type="text"
required
placeholder={translate('::App.Listform.ListformField.Website')}
value={formData.website || ''}
onChange={(e) => {
handleInputChange('website', e.target.value)
if (websiteError) {
setWebsiteError(
WEBSITE_PATTERN.test(e.target.value)
? ''
: translate('::App.WebSite.PatternError'),
)
}
}}
onBlur={() =>
setWebsiteError(
WEBSITE_PATTERN.test(formData.website || '')
? ''
: translate('::App.WebSite.PatternError'),
)
}
aria-invalid={Boolean(websiteError)}
aria-describedby={websiteError ? 'tenant-website-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 ${
websiteError
? 'border-red-500 dark:border-red-500'
: 'border-gray-300 dark:border-gray-700'
}`}
/>
</div>
{websiteError && (
<p id="tenant-website-error" className="mt-1 text-sm text-red-600">
{websiteError}
</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{translate('::App.StaticLookup.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
unstyle
type="text"
placeholder={translate('::App.StaticLookup.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"
/>
</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('::App.PublicContact.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
unstyle
type="text"
required
placeholder="Sarıgazi"
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"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{translate('::App.PublicContact.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
unstyle
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"
/>
</div>
</div>
</div>
</div>
)}
{formError && <p className="mt-2 text-sm text-red-600">{formError}</p>}
<div className="flex justify-between items-center mt-6">
<Button
type="button"
onClick={() => navigate(ROUTES_ENUM.public.products)}
icon={<FaArrowLeft className="w-4 h-4" />}
variant="default"
size="sm"
>
{translate('::App.Platform.Back')}
</Button>
<Button
type="submit"
disabled={isExisting && !canSubmitExistingTenant}
icon={<FaArrowRight className="w-5 h-5" />}
variant="solid"
color="green-600"
size="sm"
>
{translate('::Abp.Account.ResetPassword.Continue')}
</Button>
</div>
</form>
)}
</div>
</div>
)
}