Yeni Müşteri ve Eski Müşteri Sipariş ayrımı

This commit is contained in:
Sedat Öztürk 2026-07-07 00:53:45 +03:00
parent bab639ce63
commit 5c9c99206c
20 changed files with 1819 additions and 120 deletions

View file

@ -9,6 +9,9 @@ public class CountryDto : AuditedEntityDto<string>
public string GroupName { get; set; }
public string Currency { get; set; }
public int PhoneCode { get; set; }
public int? PhoneNumberMinLength { get; set; }
public int? PhoneNumberMaxLength { get; set; }
public string? PhoneNumberFormat { get; set; }
public string TaxLabel { get; set; }
public bool ZipRequired { get; set; }
public bool StateRequired { get; set; }

View file

@ -11,6 +11,11 @@ public class OrderDto : EntityDto<Guid>
public List<OrderItemDto> Items { get; set; }
public string BillingCycle { get; set; }
public int Period { get; set; }
public DateTime? LicenseStartTime { get; set; }
public DateTime? LicenseStopTime { get; set; }
public decimal Subtotal { get; set; }
public decimal Commission { get; set; }
public decimal Total { get; set; }

View file

@ -6,6 +6,7 @@ public class CustomTenantDto
{
public string Name { get; set; }
public bool IsActive { get; set; }
public bool IsExisting { get; set; }
public Guid? Id { get; set; }
public string OrganizationName { get; set; }

View file

@ -76,8 +76,10 @@ public partial class CreateUpdateOrganizationUnitDtoToOrganizationUnitMapper : M
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
public partial class TenantToCustomTenantDtoMapper : MapperBase<Tenant, CustomTenantDto>
{
[MapperIgnoreTarget(nameof(CustomTenantDto.IsExisting))]
public override partial CustomTenantDto Map(Tenant source);
[MapperIgnoreTarget(nameof(CustomTenantDto.IsExisting))]
public override partial void Map(Tenant source, CustomTenantDto destination);
public override void BeforeMap(Tenant source)

View file

@ -34,6 +34,8 @@ public class PublicAppService : PlatformAppService
private readonly IRepository<Home, Guid> _homeRepository;
private readonly IRepository<Contact, Guid> _contactRepository;
private readonly IRepository<Country, string> _countryRepository;
private readonly IRepository<City, Guid> _cityRepository;
private readonly IRepository<District, Guid> _districtRepository;
private readonly IIdentityUserRepository _identityUserRepository;
private readonly IRepository<LanguageKey, Guid> _languageKeyRepository;
private readonly IRepository<LanguageText, Guid> _languageTextRepository;
@ -54,6 +56,8 @@ public class PublicAppService : PlatformAppService
IRepository<Home, Guid> homeRepository,
IRepository<Contact, Guid> contactRepository,
IRepository<Country, string> countryRepository,
IRepository<City, Guid> cityRepository,
IRepository<District, Guid> districtRepository,
IIdentityUserRepository identityUserRepository,
IRepository<LanguageKey, Guid> languageKeyRepository,
IRepository<LanguageText, Guid> languageTextRepository,
@ -78,6 +82,8 @@ public class PublicAppService : PlatformAppService
_languageTextRepository = languageTextRepository;
_languageTextAppService = languageTextAppService;
_countryRepository = countryRepository;
_cityRepository = cityRepository;
_districtRepository = districtRepository;
}
public async Task<List<ServiceDto>> GetServicesListAsync()
@ -457,10 +463,14 @@ public class PublicAppService : PlatformAppService
public async Task<OrderDto> CreateOrderAsync(OrderDto input)
{
var billingCycle = input.BillingCycle.IsNullOrWhiteSpace() ? "yearly" : input.BillingCycle;
var period = input.Period > 0 ? input.Period : 1;
var entity = new Order()
{
Name = input.Tenant.Name,
IsActive = input.Tenant.IsActive,
IsExisting = input.Tenant.IsExisting,
OrganizationName = input.Tenant.OrganizationName,
Founder = input.Tenant.Founder,
@ -479,6 +489,10 @@ public class PublicAppService : PlatformAppService
Website = input.Tenant.Website,
MenuGroup = input.Tenant.MenuGroup,
BillingCycle = billingCycle,
Period = period,
LicenseStartTime = input.LicenseStartTime,
LicenseStopTime = input.LicenseStopTime,
Subtotal = input.Subtotal,
Commission = input.Commission,
Total = input.Total,
@ -526,9 +540,75 @@ public class PublicAppService : PlatformAppService
public async Task<List<CountryDto>> GetCountryAsync()
{
var entities = await _countryRepository.GetListAsync() ?? throw new EntityNotFoundException(typeof(Country));
var queryable = await _countryRepository.GetQueryableAsync();
return ObjectMapper.Map<List<Country>, List<CountryDto>>(entities);
return await AsyncExecuter.ToListAsync(
queryable
.OrderBy(country => country.Name)
.Select(country => new CountryDto
{
Id = country.Id,
Name = country.Name,
GroupName = country.GroupName,
Currency = country.Currency,
PhoneCode = country.PhoneCode,
PhoneNumberMinLength = country.PhoneNumberMinLength,
PhoneNumberMaxLength = country.PhoneNumberMaxLength,
PhoneNumberFormat = country.PhoneNumberFormat,
TaxLabel = country.TaxLabel,
ZipRequired = country.ZipRequired,
StateRequired = country.StateRequired,
Cities = new List<CityDto>()
}));
}
public async Task<List<CityDto>> GetCityAsync(string country)
{
if (string.IsNullOrWhiteSpace(country))
{
return [];
}
var queryable = await _cityRepository.GetQueryableAsync();
return await AsyncExecuter.ToListAsync(
queryable
.Where(city => city.Country == country)
.OrderBy(city => city.Name)
.Select(city => new CityDto
{
Id = city.Id,
Country = city.Country,
Name = city.Name,
PlateCode = city.PlateCode
}));
}
public async Task<List<DistrictDto>> GetDistrictAsync(string country, string city)
{
if (string.IsNullOrWhiteSpace(country) || string.IsNullOrWhiteSpace(city))
{
return [];
}
var queryable = await _districtRepository.GetQueryableAsync();
var entities = await AsyncExecuter.ToListAsync(
queryable
.Where(district => district.Country == country && district.City == city)
.OrderBy(district => district.Name));
return entities
.GroupBy(district => district.Name, StringComparer.OrdinalIgnoreCase)
.Select(group => group.First())
.Select(district => new DistrictDto
{
Id = district.Id,
Country = district.Country,
City = district.City,
Name = district.Name,
Township = district.Township
})
.ToList();
}
public async Task SaveContactPageAsync(SaveContactPageInput input)

File diff suppressed because it is too large Load diff

View file

@ -248,6 +248,9 @@ public class HostDataSeeder : IDataSeedContributor, ITransientDependency
groupName,
item.Currency,
item.PhoneCode,
item.PhoneNumberMinLength,
item.PhoneNumberMaxLength,
item.PhoneNumberFormat,
item.TaxLabel
));

View file

@ -13758,6 +13758,24 @@
"en": "Billing Cycle",
"tr": "Fatura Döngüsü"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.Period",
"en": "Period",
"tr": "Periyot"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.LicenseStartTime",
"en": "License Start Time",
"tr": "Lisans Başlangıç Tarihi"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.LicenseStopTime",
"en": "License Stop Time",
"tr": "Lisans Bitiş Tarihi"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.Abbreviation",
@ -16068,6 +16086,24 @@
"en": "Phone Code",
"tr": "Telefon Kodu"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.PhoneNumberMinLength",
"en": "Phone Number Min Length",
"tr": "Telefon Numarası Minimum Uzunluk"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.PhoneNumberMaxLength",
"en": "Phone Number Max Length",
"tr": "Telefon Numarası Maksimum Uzunluk"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.PhoneNumberFormat",
"en": "Phone Number Format",
"tr": "Telefon Numarası Formatı"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.Place",
@ -16140,6 +16176,12 @@
"en": "Postal Code",
"tr": "Posta Kodu"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.IsExisting",
"en": "Existing Customer",
"tr": "Mevcut Musteri"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.Priority",

View file

@ -1766,9 +1766,12 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
new EditingFormItemDto { Order = 3, DataField = "GroupName", ColSpan = 1, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
new EditingFormItemDto { Order = 4, DataField = "Currency", ColSpan = 1, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
new EditingFormItemDto { Order = 5, DataField = "PhoneCode", ColSpan = 1, EditorType2 = EditorTypes.dxNumberBox },
new EditingFormItemDto { Order = 6, DataField = "TaxLabel", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox },
new EditingFormItemDto { Order = 7, DataField = "ZipRequired", ColSpan = 1, EditorType2 = EditorTypes.dxCheckBox },
new EditingFormItemDto { Order = 8, DataField = "StateRequired", ColSpan = 1, EditorType2 = EditorTypes.dxCheckBox }
new EditingFormItemDto { Order = 6, DataField = "PhoneNumberMinLength", ColSpan = 1, EditorType2 = EditorTypes.dxNumberBox },
new EditingFormItemDto { Order = 7, DataField = "PhoneNumberMaxLength", ColSpan = 1, EditorType2 = EditorTypes.dxNumberBox },
new EditingFormItemDto { Order = 8, DataField = "PhoneNumberFormat", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox },
new EditingFormItemDto { Order = 9, DataField = "TaxLabel", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox },
new EditingFormItemDto { Order = 10, DataField = "ZipRequired", ColSpan = 1, EditorType2 = EditorTypes.dxCheckBox },
new EditingFormItemDto { Order = 11, DataField = "StateRequired", ColSpan = 1, EditorType2 = EditorTypes.dxCheckBox }
]
}
}),
@ -1866,6 +1869,57 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Int32,
FieldName = "PhoneNumberMinLength",
CaptionName = "App.Listform.ListformField.PhoneNumberMinLength",
Width = 0,
ListOrderNo = 7,
Visible = true,
IsActive = true,
AllowSearch = false,
ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Int32,
FieldName = "PhoneNumberMaxLength",
CaptionName = "App.Listform.ListformField.PhoneNumberMaxLength",
Width = 0,
ListOrderNo = 8,
Visible = true,
IsActive = true,
AllowSearch = false,
ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "PhoneNumberFormat",
CaptionName = "App.Listform.ListformField.PhoneNumberFormat",
Width = 0,
ListOrderNo = 9,
Visible = true,
IsActive = true,
AllowSearch = false,
ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
@ -1873,7 +1927,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "TaxLabel",
CaptionName = "App.Listform.ListformField.TaxLabel",
Width = 0,
ListOrderNo = 7,
ListOrderNo = 10,
Visible = true,
IsActive = true,
@ -1889,7 +1943,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "ZipRequired",
CaptionName = "App.Listform.ListformField.ZipRequired",
Width = 0,
ListOrderNo = 8,
ListOrderNo = 11,
Visible = true,
IsActive = true,
@ -1904,7 +1958,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "StateRequired",
CaptionName = "App.Listform.ListformField.StateRequired",
Width = 0,
ListOrderNo = 9,
ListOrderNo = 12,
Visible = true,
IsActive = true,
@ -6931,13 +6985,21 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
new EditingFormItemDto { Order = 18, DataField = "Total", ColSpan = 1, EditorType2 = EditorTypes.dxNumberBox, EditorOptions=EditorOptionValues.NumberStandartFormat() },
new EditingFormItemDto { Order = 19, DataField = "PaymentMethodId", ColSpan = 1, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
new EditingFormItemDto { Order = 20, DataField = "Installment", ColSpan = 1, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
new EditingFormItemDto { Order = 21, DataField = "IsActive", ColSpan = 1, EditorType2 = EditorTypes.dxCheckBox },
new EditingFormItemDto { Order = 21, DataField = "BillingCycle", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox },
new EditingFormItemDto { Order = 22, DataField = "Period", ColSpan = 1, EditorType2 = EditorTypes.dxNumberBox },
new EditingFormItemDto { Order = 23, DataField = "LicenseStartTime", ColSpan = 1, EditorType2 = EditorTypes.dxDateBox },
new EditingFormItemDto { Order = 24, DataField = "LicenseStopTime", ColSpan = 1, EditorType2 = EditorTypes.dxDateBox },
new EditingFormItemDto { Order = 25, DataField = "IsExisting", ColSpan = 1, EditorType2 = EditorTypes.dxCheckBox },
new EditingFormItemDto { Order = 26, DataField = "IsActive", ColSpan = 1, EditorType2 = EditorTypes.dxCheckBox },
]
}
}),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
new() { FieldName = "Country", FieldDbType = DbType.String, Value = "Türkiye", CustomValueType = FieldCustomValueTypeEnum.Value },
new() { FieldName = "BillingCycle", FieldDbType = DbType.String, Value = "yearly", CustomValueType = FieldCustomValueTypeEnum.Value },
new() { FieldName = "Period", FieldDbType = DbType.Int32, Value = "1", CustomValueType = FieldCustomValueTypeEnum.Value },
new() { FieldName = "IsExisting", FieldDbType = DbType.Boolean, Value = "false", CustomValueType = FieldCustomValueTypeEnum.Value },
new() { FieldName = "IsActive", FieldDbType = DbType.Boolean, Value = "true", CustomValueType = FieldCustomValueTypeEnum.Value },
new() { FieldName = "Commission", FieldDbType = DbType.Decimal, Value = "0", CustomValueType = FieldCustomValueTypeEnum.Value }
}),
@ -7013,6 +7075,91 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PivotSettingsJson = DefaultPivotSettingsJson,
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Boolean,
FieldName = "IsExisting",
CaptionName = "App.Listform.ListformField.IsExisting",
Width = 0,
ListOrderNo = 4,
Visible = true,
IsActive = true,
AllowSearch = true,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson,
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "BillingCycle",
CaptionName = "App.Listform.ListformField.BillingCycle",
Width = 0,
ListOrderNo = 5,
Visible = true,
IsActive = true,
AllowSearch = true,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson,
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Int32,
FieldName = "Period",
CaptionName = "App.Listform.ListformField.Period",
Width = 0,
ListOrderNo = 6,
Visible = true,
IsActive = true,
AllowSearch = true,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson,
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.DateTime,
FieldName = "LicenseStartTime",
CaptionName = "App.Listform.ListformField.LicenseStartTime",
Width = 0,
ListOrderNo = 7,
Visible = true,
IsActive = true,
AllowSearch = true,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson,
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.DateTime,
FieldName = "LicenseStopTime",
CaptionName = "App.Listform.ListformField.LicenseStopTime",
Width = 0,
ListOrderNo = 8,
Visible = true,
IsActive = true,
AllowSearch = true,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson,
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
@ -7020,7 +7167,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "VknTckn",
CaptionName = "App.Listform.ListformField.VknTckn",
Width = 0,
ListOrderNo = 4,
ListOrderNo = 9,
Visible = true,
IsActive = true,
@ -7038,7 +7185,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "TaxOffice",
CaptionName = "App.Listform.ListformField.TaxOffice",
Width = 0,
ListOrderNo = 5,
ListOrderNo = 10,
Visible = true,
IsActive = true,
@ -7056,7 +7203,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "Address1",
CaptionName = "App.Listform.ListformField.Address1",
Width = 0,
ListOrderNo = 6,
ListOrderNo = 11,
Visible = true,
IsActive = true,
@ -7074,7 +7221,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "Address2",
CaptionName = "App.Listform.ListformField.Address2",
Width = 0,
ListOrderNo = 7,
ListOrderNo = 12,
Visible = true,
IsActive = true,
@ -7091,7 +7238,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "Country",
CaptionName = "App.Listform.ListformField.Country",
Width = 0,
ListOrderNo = 8,
ListOrderNo = 13,
Visible = true,
IsActive = true,
@ -7117,7 +7264,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "City",
CaptionName = "App.Listform.ListformField.City",
Width = 0,
ListOrderNo = 9,
ListOrderNo = 14,
Visible = true,
IsActive = true,
@ -7146,7 +7293,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "District",
CaptionName = "App.Listform.ListformField.District",
Width = 0,
ListOrderNo = 10,
ListOrderNo = 15,
Visible = true,
IsActive = true,
@ -7174,7 +7321,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "PhoneNumber",
CaptionName = "Abp.Identity.User.UserInformation.PhoneNumber",
Width = 0,
ListOrderNo = 11,
ListOrderNo = 16,
Visible = true,
IsActive = true,
@ -7193,7 +7340,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "MobileNumber",
CaptionName = "App.Listform.ListformField.MobileNumber",
Width = 0,
ListOrderNo = 12,
ListOrderNo = 17,
Visible = true,
IsActive = true,
@ -7212,7 +7359,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "FaxNumber",
CaptionName = "App.Listform.ListformField.FaxNumber",
Width = 0,
ListOrderNo = 13,
ListOrderNo = 18,
Visible = true,
IsActive = true,
@ -7230,7 +7377,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "Website",
CaptionName = "App.Listform.ListformField.Website",
Width = 0,
ListOrderNo = 14,
ListOrderNo = 19,
Visible = true,
IsActive = true,
@ -7249,7 +7396,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
CaptionName = "App.Listform.ListformField.Subtotal",
Alignment = "right",
Width = 0,
ListOrderNo = 15,
ListOrderNo = 20,
Visible = true,
IsActive = true,
@ -7269,7 +7416,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
CaptionName = "App.Listform.ListformField.Commission",
Alignment = "right",
Width = 0,
ListOrderNo = 16,
ListOrderNo = 21,
Visible = true,
IsActive = true,
@ -7289,7 +7436,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
CaptionName = "App.Listform.ListformField.Total",
Alignment = "right",
Width = 0,
ListOrderNo = 17,
ListOrderNo = 22,
Visible = true,
IsActive = true,
@ -7308,7 +7455,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "PaymentMethodId",
CaptionName = "App.Listform.ListformField.PaymentMethodId",
Width = 0,
ListOrderNo = 18,
ListOrderNo = 23,
Visible = true,
IsActive = true,
@ -7327,7 +7474,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "Installment",
CaptionName = "App.Listform.ListformField.Installment",
Width = 0,
ListOrderNo = 19,
ListOrderNo = 24,
Visible = true,
IsActive = true,
@ -7346,7 +7493,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "IsActive",
CaptionName = "App.Listform.ListformField.IsActive",
Width = 0,
ListOrderNo = 20,
ListOrderNo = 25,
Visible = true,
IsActive = true,
@ -7363,7 +7510,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "PostalCode",
CaptionName = "App.Listform.ListformField.PostalCode",
Width = 0,
ListOrderNo = 21,
ListOrderNo = 26,
Visible = true,
IsActive = true,
@ -7381,7 +7528,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "Email",
CaptionName = "App.Listform.ListformField.Email",
Width = 0,
ListOrderNo = 22,
ListOrderNo = 27,
Visible = true,
IsActive = true,
@ -7394,6 +7541,133 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
]);
#endregion
}
else
{
var existingListForm = await _listFormRepository.FirstOrDefaultAsync(a => a.ListFormCode == listFormName);
if (existingListForm != null)
{
if (!await _listFormFieldRepository.AnyAsync(a =>
a.ListFormCode == listFormName &&
a.FieldName == "BillingCycle" &&
a.CultureName == LanguageCodes.En))
{
await _listFormFieldRepository.InsertAsync(new()
{
ListFormCode = existingListForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "BillingCycle",
CaptionName = "App.Listform.ListformField.BillingCycle",
Width = 0,
ListOrderNo = 20,
Visible = true,
IsActive = true,
AllowSearch = true,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(existingListForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson,
});
}
if (!await _listFormFieldRepository.AnyAsync(a =>
a.ListFormCode == listFormName &&
a.FieldName == "Period" &&
a.CultureName == LanguageCodes.En))
{
await _listFormFieldRepository.InsertAsync(new()
{
ListFormCode = existingListForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Int32,
FieldName = "Period",
CaptionName = "App.Listform.ListformField.Period",
Width = 0,
ListOrderNo = 21,
Visible = true,
IsActive = true,
AllowSearch = true,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(existingListForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson,
});
}
if (!await _listFormFieldRepository.AnyAsync(a =>
a.ListFormCode == listFormName &&
a.FieldName == "LicenseStartTime" &&
a.CultureName == LanguageCodes.En))
{
await _listFormFieldRepository.InsertAsync(new()
{
ListFormCode = existingListForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.DateTime,
FieldName = "LicenseStartTime",
CaptionName = "App.Listform.ListformField.LicenseStartTime",
Width = 0,
ListOrderNo = 22,
Visible = true,
IsActive = true,
AllowSearch = true,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(existingListForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson,
});
}
if (!await _listFormFieldRepository.AnyAsync(a =>
a.ListFormCode == listFormName &&
a.FieldName == "LicenseStopTime" &&
a.CultureName == LanguageCodes.En))
{
await _listFormFieldRepository.InsertAsync(new()
{
ListFormCode = existingListForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.DateTime,
FieldName = "LicenseStopTime",
CaptionName = "App.Listform.ListformField.LicenseStopTime",
Width = 0,
ListOrderNo = 23,
Visible = true,
IsActive = true,
AllowSearch = true,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(existingListForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson,
});
}
if (!await _listFormFieldRepository.AnyAsync(a =>
a.ListFormCode == listFormName &&
a.FieldName == "IsExisting" &&
a.CultureName == LanguageCodes.En))
{
await _listFormFieldRepository.InsertAsync(new()
{
ListFormCode = existingListForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Boolean,
FieldName = "IsExisting",
CaptionName = "App.Listform.ListformField.IsExisting",
Width = 0,
ListOrderNo = 24,
Visible = true,
IsActive = true,
AllowSearch = true,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(existingListForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson,
});
}
}
}
#endregion
#region Sales Order Item

View file

@ -9,6 +9,9 @@ public class Country : FullAuditedEntity<string>
public string GroupName { get; set; }
public string Currency { get; set; }
public int PhoneCode { get; set; }
public int? PhoneNumberMinLength { get; set; }
public int? PhoneNumberMaxLength { get; set; }
public string? PhoneNumberFormat { get; set; }
public string TaxLabel { get; set; }
public bool ZipRequired { get; set; }
public bool StateRequired { get; set; }
@ -23,6 +26,9 @@ public class Country : FullAuditedEntity<string>
string groupName,
string currency,
int phoneCode,
int? phoneNumberMinLength,
int? phoneNumberMaxLength,
string? phoneNumberFormat,
string taxLabel,
bool zipRequired = false,
bool stateRequired = false)
@ -32,6 +38,9 @@ public class Country : FullAuditedEntity<string>
GroupName = groupName;
Currency = currency;
PhoneCode = phoneCode;
PhoneNumberMinLength = phoneNumberMinLength;
PhoneNumberMaxLength = phoneNumberMaxLength;
PhoneNumberFormat = phoneNumberFormat;
TaxLabel = taxLabel;
ZipRequired = zipRequired;
StateRequired = stateRequired;

View file

@ -8,6 +8,7 @@ public class Order : FullAuditedEntity<Guid>
{
public string Name { get; set; }
public bool IsActive { get; set; }
public bool IsExisting { get; set; }
public string OrganizationName { get; set; }
public string Founder { get; set; }
@ -26,6 +27,11 @@ public class Order : FullAuditedEntity<Guid>
public string Website { get; set; }
public string MenuGroup { get; set; }
public string BillingCycle { get; set; }
public int Period { get; set; }
public DateTime? LicenseStartTime { get; set; }
public DateTime? LicenseStopTime { get; set; }
public List<OrderItem> Items { get; set; } = new();
public decimal Subtotal { get; set; }
public decimal Commission { get; set; }

View file

@ -747,6 +747,9 @@ public class PlatformDbContext :
b.Property(x => x.GroupName).HasMaxLength(128);
b.Property(x => x.TaxLabel).HasMaxLength(64);
b.Property(x => x.Currency).IsRequired().HasMaxLength(8);
b.Property(x => x.PhoneNumberMinLength).HasDefaultValue(0);
b.Property(x => x.PhoneNumberMaxLength).HasDefaultValue(0);
b.Property(x => x.PhoneNumberFormat).HasMaxLength(64);
b.HasIndex(x => new { x.Name }).IsUnique().HasFilter("[IsDeleted] = 0");
@ -1011,6 +1014,7 @@ public class PlatformDbContext :
b.ConfigureByConvention();
b.Property(o => o.Name).HasMaxLength(64);
b.Property(o => o.IsExisting).IsRequired().HasDefaultValue(false);
b.Property(o => o.OrganizationName).HasMaxLength(128);
b.Property(o => o.Founder).HasMaxLength(128);
b.Property(o => o.TaxOffice).HasMaxLength(128);
@ -1026,6 +1030,8 @@ public class PlatformDbContext :
b.Property(o => o.Email).HasMaxLength(128);
b.Property(o => o.Website).HasMaxLength(128);
b.Property(o => o.MenuGroup).HasMaxLength(64);
b.Property(o => o.BillingCycle).HasMaxLength(32);
b.Property(o => o.Period).IsRequired().HasDefaultValue(1);
b.Property(o => o.Subtotal).IsRequired().HasPrecision(18, 2);
b.Property(o => o.Commission).IsRequired().HasPrecision(18, 2);

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Sozsoft.Platform.Migrations
{
[DbContext(typeof(PlatformDbContext))]
[Migration("20260625183130_Initial")]
[Migration("20260706213526_Initial")]
partial class Initial
{
/// <inheritdoc />
@ -1542,6 +1542,20 @@ namespace Sozsoft.Platform.Migrations
b.Property<int>("PhoneCode")
.HasColumnType("int");
b.Property<string>("PhoneNumberFormat")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<int?>("PhoneNumberMaxLength")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.Property<int?>("PhoneNumberMinLength")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.Property<bool>("StateRequired")
.HasColumnType("bit");
@ -4071,6 +4085,10 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<string>("BillingCycle")
.HasMaxLength(32)
.HasColumnType("nvarchar(32)");
b.Property<string>("City")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
@ -4127,6 +4145,11 @@ namespace Sozsoft.Platform.Migrations
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<bool>("IsExisting")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
@ -4135,6 +4158,12 @@ namespace Sozsoft.Platform.Migrations
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<DateTime?>("LicenseStartTime")
.HasColumnType("datetime2");
b.Property<DateTime?>("LicenseStopTime")
.HasColumnType("datetime2");
b.Property<string>("MenuGroup")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
@ -4158,6 +4187,11 @@ namespace Sozsoft.Platform.Migrations
b.Property<Guid>("PaymentMethodId")
.HasColumnType("uniqueidentifier");
b.Property<int>("Period")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(1);
b.Property<string>("PhoneNumber")
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");

View file

@ -1573,6 +1573,7 @@ namespace Sozsoft.Platform.Migrations
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Name = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false),
IsExisting = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
OrganizationName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
Founder = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
VknTckn = table.Column<long>(type: "bigint", nullable: true),
@ -1589,6 +1590,10 @@ namespace Sozsoft.Platform.Migrations
Email = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
Website = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
MenuGroup = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
BillingCycle = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: true),
Period = table.Column<int>(type: "int", nullable: false, defaultValue: 1),
LicenseStartTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LicenseStopTime = table.Column<DateTime>(type: "datetime2", nullable: true),
Subtotal = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
Commission = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
Total = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
@ -2683,6 +2688,9 @@ namespace Sozsoft.Platform.Migrations
GroupName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
Currency = table.Column<string>(type: "nvarchar(8)", maxLength: 8, nullable: false),
PhoneCode = table.Column<int>(type: "int", nullable: false),
PhoneNumberMinLength = table.Column<int>(type: "int", nullable: true, defaultValue: 0),
PhoneNumberMaxLength = table.Column<int>(type: "int", nullable: true, defaultValue: 0),
PhoneNumberFormat = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
TaxLabel = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
ZipRequired = table.Column<bool>(type: "bit", nullable: false),
StateRequired = table.Column<bool>(type: "bit", nullable: false),

View file

@ -1539,6 +1539,20 @@ namespace Sozsoft.Platform.Migrations
b.Property<int>("PhoneCode")
.HasColumnType("int");
b.Property<string>("PhoneNumberFormat")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<int?>("PhoneNumberMaxLength")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.Property<int?>("PhoneNumberMinLength")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0);
b.Property<bool>("StateRequired")
.HasColumnType("bit");
@ -4068,6 +4082,10 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<string>("BillingCycle")
.HasMaxLength(32)
.HasColumnType("nvarchar(32)");
b.Property<string>("City")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
@ -4124,6 +4142,11 @@ namespace Sozsoft.Platform.Migrations
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<bool>("IsExisting")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
@ -4132,6 +4155,12 @@ namespace Sozsoft.Platform.Migrations
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<DateTime?>("LicenseStartTime")
.HasColumnType("datetime2");
b.Property<DateTime?>("LicenseStopTime")
.HasColumnType("datetime2");
b.Property<string>("MenuGroup")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
@ -4155,6 +4184,11 @@ namespace Sozsoft.Platform.Migrations
b.Property<Guid>("PaymentMethodId")
.HasColumnType("uniqueidentifier");
b.Property<int>("Period")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(1);
b.Property<string>("PhoneNumber")
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");

View file

@ -15,11 +15,123 @@ import {
FaUser,
FaUserPlus,
} from 'react-icons/fa'
import React, { useState } from 'react'
import React, { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { Button } from '@/components/ui'
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() || ''
interface TenantFormProps {
onSubmit: (tenant: CustomTenantDto) => void
@ -29,22 +141,203 @@ interface TenantFormProps {
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('')
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
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
onSubmit({ ...formData, isExisting: isExisting! } as CustomTenantDto)
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)
}
const handleInputChange = (field: keyof CustomTenantDto, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }))
}
const getTenantInfo = async () => {
if (!formData.name) return
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 tenant = await getTenantByNameDetail(formData.name)
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,
}))
const getTenantInfo = async () => {
const tenantName = formData.name?.trim()
if (!tenantName) {
setExistingTenantError('Lütfen önce organization code girin.')
return
}
setIsLoadingTenant(true)
setExistingTenantError('')
setFormError('')
try {
const tenant = await getTenantByNameDetail(tenantName)
setFormData((prev) => ({
...prev,
@ -68,8 +361,13 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
website: tenant.data.website,
menuGroup: tenant.data.menuGroup,
}))
setFoundTenantName(tenant.data.name)
} catch (error) {
setFoundTenantName('')
setExistingTenantError('Kurum bulunamadı. Lütfen organization code bilgisini kontrol edin.')
console.error('Kurum bilgisi alınırken hata:', error)
} finally {
setIsLoadingTenant(false)
}
}
@ -139,7 +437,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
autoFocus
placeholder="Enter your organization code"
value={formData.name || ''}
onChange={(e) => handleInputChange('name', e.target.value)}
onChange={(e) => resetExistingTenantDetails(e.target.value)}
onKeyDown={async (e) => {
if (e.key === 'Enter') {
e.preventDefault()
@ -151,23 +449,36 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
</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"
>
{translate('::Public.products.tenantForm.searchOrg')}
{isLoadingTenant
? 'Aranıyor...'
: translate('::Public.products.tenantForm.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">
Devam etmek için önce Find Organization ile kurum bilgilerini getirin.
</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">
<FaUser className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::LoginPanel.Profil')}
</span>
<span className="font-medium">{translate('::LoginPanel.Profil')}</span>
<span>{formData.founder}</span>
</div>
@ -189,18 +500,14 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
<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>
<span className="font-medium">{translate('::blog.categories.mobile')}:</span>
<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>
<span className="font-medium">{translate('::App.Address')}:</span>
<div>{formData.address1}</div>
</div>
</div>
@ -215,9 +522,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
<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>
<span className="font-medium">{translate('::Public.common.city')}:</span>
<span>{formData.city}</span>
</div>
@ -306,32 +611,76 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
<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')}
{translate('::Public.payment.customer.country')}
</label>
<div className="relative">
<FaPhone className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type="tel"
<Select<LocationOption<CountryDto>>
required
placeholder="+90 (___) ___-____"
value={formData.phoneNumber || ''}
onChange={(e) => handleInputChange('phoneNumber', 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"
isClearable
isLoading={isLoadingCountries}
options={countryOptions}
value={
countryOptions.find((option) => option.value === formData.country) || null
}
onChange={handleCountryChange}
placeholder="Ülke seçin"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{translate('::Abp.Account.EmailAddress')}
{translate('::Public.common.city')}
</label>
<div className="relative">
<FaEnvelope className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type="email"
<Select<LocationOption<CityDto>>
required
placeholder="sample@email.com"
value={formData.email || ''}
onChange={(e) => handleInputChange('email', e.target.value)}
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'}
/>
</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')}
</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'}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{translate('::Public.payment.customer.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
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>
@ -358,69 +707,87 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
<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')}
{translate('::Abp.Identity.User.UserInformation.PhoneNumber')}
</label>
<div className="relative">
<FaGlobe className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<FaPhone className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type="text"
placeholder="Türkiye"
value={formData.country || ''}
onChange={(e) => handleInputChange('country', 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"
type="tel"
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'
}`}
/>
</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('::Public.common.city')}
{translate('::Abp.Account.EmailAddress')}
</label>
<div className="relative">
<FaGlobe className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<FaEnvelope className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type="text"
type="email"
required
placeholder="İstanbul"
value={formData.city || ''}
onChange={(e) => handleInputChange('city', 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('::Public.payment.customer.district')}
</label>
<div className="relative">
<FaMapPin className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type="text"
required
placeholder="Çekmeköy"
value={formData.district || ''}
onChange={(e) => handleInputChange('district', 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('::Public.payment.customer.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
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"
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'
}`}
/>
</div>
{emailError && (
<p id="tenant-email-error" className="mt-1 text-sm text-red-600">
{emailError}
</p>
)}
</div>
</div>
@ -477,6 +844,8 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
</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"
@ -489,6 +858,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
</Button>
<Button
type="submit"
disabled={isExisting && !canSubmitExistingTenant}
icon={<FaArrowRight className="w-5 h-5" />}
variant="solid"
color="green-600"
@ -503,4 +873,3 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
</div>
)
}

View file

@ -60,6 +60,10 @@ export interface InstallmentOptionDto {
export interface OrderDto {
tenant: CustomTenantDto
items: BasketItem[]
billingCycle: BillingCycle
period: number
licenseStartTime?: string
licenseStopTime?: string
subtotal: number
commission: number
total: number

View file

@ -0,0 +1,45 @@
import apiService from './api.service'
export interface CountryDto {
id: string
name: string
phoneCode: number
phoneNumberMinLength: number | null
phoneNumberMaxLength: number | null
phoneNumberFormat: string | null
}
export interface CityDto {
id: string
country: string
name: string
plateCode?: string
}
export interface DistrictDto {
id: string
country: string
city: string
name: string
township?: string
}
export const getCountries = () =>
apiService.fetchData<CountryDto[]>({
method: 'GET',
url: '/api/app/public/country',
})
export const getCities = (country: string) =>
apiService.fetchData<CityDto[]>({
method: 'GET',
url: '/api/app/public/city',
params: { country },
})
export const getDistricts = (country: string, city: string) =>
apiService.fetchData<DistrictDto[]>({
method: 'GET',
url: '/api/app/public/district',
params: { country, city },
})

View file

@ -44,6 +44,10 @@ export class OrderService {
data: {
tenant: orderData.tenant,
items: orderData.items,
billingCycle: orderData.billingCycle,
period: orderData.period,
licenseStartTime: orderData.licenseStartTime,
licenseStopTime: orderData.licenseStopTime,
subtotal: orderData.subtotal,
commission: orderData.commission,
total: orderData.total,

View file

@ -62,13 +62,34 @@ const Payment: React.FC = () => {
setCartState(clearCart())
}
const calculateLicenseStopTime = (startTime: Date, billingCycle: string, period: number) => {
const stopTime = new Date(startTime)
if (billingCycle === 'monthly') {
stopTime.setMonth(stopTime.getMonth() + period)
} else {
stopTime.setFullYear(stopTime.getFullYear() + period)
}
return stopTime
}
const handlePaymentComplete = async (paymentData: Record<string, unknown>) => {
if (!tenant) return
try {
const billingCycle = cartState.globalBillingCycle || 'yearly'
const period = cartState.globalPeriod > 0 ? cartState.globalPeriod : 1
const licenseStartTime = new Date()
const licenseStopTime = calculateLicenseStopTime(licenseStartTime, billingCycle, period)
const orderData = {
tenant,
items: cartState.items,
billingCycle,
period,
licenseStartTime: licenseStartTime.toISOString(),
licenseStopTime: licenseStopTime.toISOString(),
subtotal: cartState.total,
commission: paymentData.commission,
total: paymentData.total,
@ -136,20 +157,20 @@ const Payment: React.FC = () => {
<div className="container mx-auto px-4 pt-4">
<PaymentForm
cartState={cartState}
tenant={tenant}
onBack={handleBackToTenant}
onComplete={handlePaymentComplete}
cartState={cartState}
/>
<Cart
cartState={cartState}
isOpen={isCartOpen}
clearCart={handleClearCart}
removeItem={handleRemoveItem}
updateQuantity={handleUpdateQuantity}
onClose={() => setIsCartOpen(false)}
onCheckout={handleCheckout}
cartState={cartState}
updateQuantity={handleUpdateQuantity}
removeItem={handleRemoveItem}
clearCart={handleClearCart}
/>
</div>
</div>