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 GroupName { get; set; }
public string Currency { get; set; } public string Currency { get; set; }
public int PhoneCode { 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 string TaxLabel { get; set; }
public bool ZipRequired { get; set; } public bool ZipRequired { get; set; }
public bool StateRequired { 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 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 Subtotal { get; set; }
public decimal Commission { get; set; } public decimal Commission { get; set; }
public decimal Total { get; set; } public decimal Total { get; set; }

View file

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

View file

@ -76,8 +76,10 @@ public partial class CreateUpdateOrganizationUnitDtoToOrganizationUnitMapper : M
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] [Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
public partial class TenantToCustomTenantDtoMapper : MapperBase<Tenant, CustomTenantDto> public partial class TenantToCustomTenantDtoMapper : MapperBase<Tenant, CustomTenantDto>
{ {
[MapperIgnoreTarget(nameof(CustomTenantDto.IsExisting))]
public override partial CustomTenantDto Map(Tenant source); public override partial CustomTenantDto Map(Tenant source);
[MapperIgnoreTarget(nameof(CustomTenantDto.IsExisting))]
public override partial void Map(Tenant source, CustomTenantDto destination); public override partial void Map(Tenant source, CustomTenantDto destination);
public override void BeforeMap(Tenant source) 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<Home, Guid> _homeRepository;
private readonly IRepository<Contact, Guid> _contactRepository; private readonly IRepository<Contact, Guid> _contactRepository;
private readonly IRepository<Country, string> _countryRepository; private readonly IRepository<Country, string> _countryRepository;
private readonly IRepository<City, Guid> _cityRepository;
private readonly IRepository<District, Guid> _districtRepository;
private readonly IIdentityUserRepository _identityUserRepository; private readonly IIdentityUserRepository _identityUserRepository;
private readonly IRepository<LanguageKey, Guid> _languageKeyRepository; private readonly IRepository<LanguageKey, Guid> _languageKeyRepository;
private readonly IRepository<LanguageText, Guid> _languageTextRepository; private readonly IRepository<LanguageText, Guid> _languageTextRepository;
@ -54,6 +56,8 @@ public class PublicAppService : PlatformAppService
IRepository<Home, Guid> homeRepository, IRepository<Home, Guid> homeRepository,
IRepository<Contact, Guid> contactRepository, IRepository<Contact, Guid> contactRepository,
IRepository<Country, string> countryRepository, IRepository<Country, string> countryRepository,
IRepository<City, Guid> cityRepository,
IRepository<District, Guid> districtRepository,
IIdentityUserRepository identityUserRepository, IIdentityUserRepository identityUserRepository,
IRepository<LanguageKey, Guid> languageKeyRepository, IRepository<LanguageKey, Guid> languageKeyRepository,
IRepository<LanguageText, Guid> languageTextRepository, IRepository<LanguageText, Guid> languageTextRepository,
@ -78,6 +82,8 @@ public class PublicAppService : PlatformAppService
_languageTextRepository = languageTextRepository; _languageTextRepository = languageTextRepository;
_languageTextAppService = languageTextAppService; _languageTextAppService = languageTextAppService;
_countryRepository = countryRepository; _countryRepository = countryRepository;
_cityRepository = cityRepository;
_districtRepository = districtRepository;
} }
public async Task<List<ServiceDto>> GetServicesListAsync() public async Task<List<ServiceDto>> GetServicesListAsync()
@ -457,10 +463,14 @@ public class PublicAppService : PlatformAppService
public async Task<OrderDto> CreateOrderAsync(OrderDto input) 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() var entity = new Order()
{ {
Name = input.Tenant.Name, Name = input.Tenant.Name,
IsActive = input.Tenant.IsActive, IsActive = input.Tenant.IsActive,
IsExisting = input.Tenant.IsExisting,
OrganizationName = input.Tenant.OrganizationName, OrganizationName = input.Tenant.OrganizationName,
Founder = input.Tenant.Founder, Founder = input.Tenant.Founder,
@ -479,6 +489,10 @@ public class PublicAppService : PlatformAppService
Website = input.Tenant.Website, Website = input.Tenant.Website,
MenuGroup = input.Tenant.MenuGroup, MenuGroup = input.Tenant.MenuGroup,
BillingCycle = billingCycle,
Period = period,
LicenseStartTime = input.LicenseStartTime,
LicenseStopTime = input.LicenseStopTime,
Subtotal = input.Subtotal, Subtotal = input.Subtotal,
Commission = input.Commission, Commission = input.Commission,
Total = input.Total, Total = input.Total,
@ -526,9 +540,75 @@ public class PublicAppService : PlatformAppService
public async Task<List<CountryDto>> GetCountryAsync() 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) 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, groupName,
item.Currency, item.Currency,
item.PhoneCode, item.PhoneCode,
item.PhoneNumberMinLength,
item.PhoneNumberMaxLength,
item.PhoneNumberFormat,
item.TaxLabel item.TaxLabel
)); ));

View file

@ -13758,6 +13758,24 @@
"en": "Billing Cycle", "en": "Billing Cycle",
"tr": "Fatura Döngüsü" "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", "resourceName": "Platform",
"key": "App.Listform.ListformField.Abbreviation", "key": "App.Listform.ListformField.Abbreviation",
@ -16068,6 +16086,24 @@
"en": "Phone Code", "en": "Phone Code",
"tr": "Telefon Kodu" "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", "resourceName": "Platform",
"key": "App.Listform.ListformField.Place", "key": "App.Listform.ListformField.Place",
@ -16140,6 +16176,12 @@
"en": "Postal Code", "en": "Postal Code",
"tr": "Posta Kodu" "tr": "Posta Kodu"
}, },
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.IsExisting",
"en": "Existing Customer",
"tr": "Mevcut Musteri"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.Listform.ListformField.Priority", "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 = 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 = 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 = 5, DataField = "PhoneCode", ColSpan = 1, EditorType2 = EditorTypes.dxNumberBox },
new EditingFormItemDto { Order = 6, DataField = "TaxLabel", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox }, new EditingFormItemDto { Order = 6, DataField = "PhoneNumberMinLength", ColSpan = 1, EditorType2 = EditorTypes.dxNumberBox },
new EditingFormItemDto { Order = 7, DataField = "ZipRequired", ColSpan = 1, EditorType2 = EditorTypes.dxCheckBox }, new EditingFormItemDto { Order = 7, DataField = "PhoneNumberMaxLength", ColSpan = 1, EditorType2 = EditorTypes.dxNumberBox },
new EditingFormItemDto { Order = 8, DataField = "StateRequired", ColSpan = 1, EditorType2 = EditorTypes.dxCheckBox } 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), PermissionJson = DefaultFieldPermissionJson(listForm.Name),
}, },
new() 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, ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
@ -1873,7 +1927,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "TaxLabel", FieldName = "TaxLabel",
CaptionName = "App.Listform.ListformField.TaxLabel", CaptionName = "App.Listform.ListformField.TaxLabel",
Width = 0, Width = 0,
ListOrderNo = 7, ListOrderNo = 10,
Visible = true, Visible = true,
IsActive = true, IsActive = true,
@ -1889,7 +1943,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "ZipRequired", FieldName = "ZipRequired",
CaptionName = "App.Listform.ListformField.ZipRequired", CaptionName = "App.Listform.ListformField.ZipRequired",
Width = 0, Width = 0,
ListOrderNo = 8, ListOrderNo = 11,
Visible = true, Visible = true,
IsActive = true, IsActive = true,
@ -1904,7 +1958,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "StateRequired", FieldName = "StateRequired",
CaptionName = "App.Listform.ListformField.StateRequired", CaptionName = "App.Listform.ListformField.StateRequired",
Width = 0, Width = 0,
ListOrderNo = 9, ListOrderNo = 12,
Visible = true, Visible = true,
IsActive = 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 = 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 = 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 = 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(), InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] { FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
new() { FieldName = "Country", FieldDbType = DbType.String, Value = "Türkiye", CustomValueType = FieldCustomValueTypeEnum.Value }, 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 = "IsActive", FieldDbType = DbType.Boolean, Value = "true", CustomValueType = FieldCustomValueTypeEnum.Value },
new() { FieldName = "Commission", FieldDbType = DbType.Decimal, Value = "0", 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, PivotSettingsJson = DefaultPivotSettingsJson,
}, },
new() 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, ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
@ -7020,7 +7167,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "VknTckn", FieldName = "VknTckn",
CaptionName = "App.Listform.ListformField.VknTckn", CaptionName = "App.Listform.ListformField.VknTckn",
Width = 0, Width = 0,
ListOrderNo = 4, ListOrderNo = 9,
Visible = true, Visible = true,
IsActive = true, IsActive = true,
@ -7038,7 +7185,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "TaxOffice", FieldName = "TaxOffice",
CaptionName = "App.Listform.ListformField.TaxOffice", CaptionName = "App.Listform.ListformField.TaxOffice",
Width = 0, Width = 0,
ListOrderNo = 5, ListOrderNo = 10,
Visible = true, Visible = true,
IsActive = true, IsActive = true,
@ -7056,7 +7203,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "Address1", FieldName = "Address1",
CaptionName = "App.Listform.ListformField.Address1", CaptionName = "App.Listform.ListformField.Address1",
Width = 0, Width = 0,
ListOrderNo = 6, ListOrderNo = 11,
Visible = true, Visible = true,
IsActive = true, IsActive = true,
@ -7074,7 +7221,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "Address2", FieldName = "Address2",
CaptionName = "App.Listform.ListformField.Address2", CaptionName = "App.Listform.ListformField.Address2",
Width = 0, Width = 0,
ListOrderNo = 7, ListOrderNo = 12,
Visible = true, Visible = true,
IsActive = true, IsActive = true,
@ -7091,7 +7238,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "Country", FieldName = "Country",
CaptionName = "App.Listform.ListformField.Country", CaptionName = "App.Listform.ListformField.Country",
Width = 0, Width = 0,
ListOrderNo = 8, ListOrderNo = 13,
Visible = true, Visible = true,
IsActive = true, IsActive = true,
@ -7117,7 +7264,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "City", FieldName = "City",
CaptionName = "App.Listform.ListformField.City", CaptionName = "App.Listform.ListformField.City",
Width = 0, Width = 0,
ListOrderNo = 9, ListOrderNo = 14,
Visible = true, Visible = true,
IsActive = true, IsActive = true,
@ -7146,7 +7293,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "District", FieldName = "District",
CaptionName = "App.Listform.ListformField.District", CaptionName = "App.Listform.ListformField.District",
Width = 0, Width = 0,
ListOrderNo = 10, ListOrderNo = 15,
Visible = true, Visible = true,
IsActive = true, IsActive = true,
@ -7174,7 +7321,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "PhoneNumber", FieldName = "PhoneNumber",
CaptionName = "Abp.Identity.User.UserInformation.PhoneNumber", CaptionName = "Abp.Identity.User.UserInformation.PhoneNumber",
Width = 0, Width = 0,
ListOrderNo = 11, ListOrderNo = 16,
Visible = true, Visible = true,
IsActive = true, IsActive = true,
@ -7193,7 +7340,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "MobileNumber", FieldName = "MobileNumber",
CaptionName = "App.Listform.ListformField.MobileNumber", CaptionName = "App.Listform.ListformField.MobileNumber",
Width = 0, Width = 0,
ListOrderNo = 12, ListOrderNo = 17,
Visible = true, Visible = true,
IsActive = true, IsActive = true,
@ -7212,7 +7359,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "FaxNumber", FieldName = "FaxNumber",
CaptionName = "App.Listform.ListformField.FaxNumber", CaptionName = "App.Listform.ListformField.FaxNumber",
Width = 0, Width = 0,
ListOrderNo = 13, ListOrderNo = 18,
Visible = true, Visible = true,
IsActive = true, IsActive = true,
@ -7230,7 +7377,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "Website", FieldName = "Website",
CaptionName = "App.Listform.ListformField.Website", CaptionName = "App.Listform.ListformField.Website",
Width = 0, Width = 0,
ListOrderNo = 14, ListOrderNo = 19,
Visible = true, Visible = true,
IsActive = true, IsActive = true,
@ -7249,7 +7396,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
CaptionName = "App.Listform.ListformField.Subtotal", CaptionName = "App.Listform.ListformField.Subtotal",
Alignment = "right", Alignment = "right",
Width = 0, Width = 0,
ListOrderNo = 15, ListOrderNo = 20,
Visible = true, Visible = true,
IsActive = true, IsActive = true,
@ -7269,7 +7416,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
CaptionName = "App.Listform.ListformField.Commission", CaptionName = "App.Listform.ListformField.Commission",
Alignment = "right", Alignment = "right",
Width = 0, Width = 0,
ListOrderNo = 16, ListOrderNo = 21,
Visible = true, Visible = true,
IsActive = true, IsActive = true,
@ -7289,7 +7436,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
CaptionName = "App.Listform.ListformField.Total", CaptionName = "App.Listform.ListformField.Total",
Alignment = "right", Alignment = "right",
Width = 0, Width = 0,
ListOrderNo = 17, ListOrderNo = 22,
Visible = true, Visible = true,
IsActive = true, IsActive = true,
@ -7308,7 +7455,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "PaymentMethodId", FieldName = "PaymentMethodId",
CaptionName = "App.Listform.ListformField.PaymentMethodId", CaptionName = "App.Listform.ListformField.PaymentMethodId",
Width = 0, Width = 0,
ListOrderNo = 18, ListOrderNo = 23,
Visible = true, Visible = true,
IsActive = true, IsActive = true,
@ -7327,7 +7474,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "Installment", FieldName = "Installment",
CaptionName = "App.Listform.ListformField.Installment", CaptionName = "App.Listform.ListformField.Installment",
Width = 0, Width = 0,
ListOrderNo = 19, ListOrderNo = 24,
Visible = true, Visible = true,
IsActive = true, IsActive = true,
@ -7346,7 +7493,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "IsActive", FieldName = "IsActive",
CaptionName = "App.Listform.ListformField.IsActive", CaptionName = "App.Listform.ListformField.IsActive",
Width = 0, Width = 0,
ListOrderNo = 20, ListOrderNo = 25,
Visible = true, Visible = true,
IsActive = true, IsActive = true,
@ -7363,7 +7510,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "PostalCode", FieldName = "PostalCode",
CaptionName = "App.Listform.ListformField.PostalCode", CaptionName = "App.Listform.ListformField.PostalCode",
Width = 0, Width = 0,
ListOrderNo = 21, ListOrderNo = 26,
Visible = true, Visible = true,
IsActive = true, IsActive = true,
@ -7381,7 +7528,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "Email", FieldName = "Email",
CaptionName = "App.Listform.ListformField.Email", CaptionName = "App.Listform.ListformField.Email",
Width = 0, Width = 0,
ListOrderNo = 22, ListOrderNo = 27,
Visible = true, Visible = true,
IsActive = true, IsActive = true,
@ -7394,6 +7541,133 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
]); ]);
#endregion #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 #endregion
#region Sales Order Item #region Sales Order Item

View file

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

View file

@ -8,6 +8,7 @@ public class Order : FullAuditedEntity<Guid>
{ {
public string Name { get; set; } public string Name { get; set; }
public bool IsActive { get; set; } public bool IsActive { get; set; }
public bool IsExisting { get; set; }
public string OrganizationName { get; set; } public string OrganizationName { get; set; }
public string Founder { get; set; } public string Founder { get; set; }
@ -26,6 +27,11 @@ public class Order : FullAuditedEntity<Guid>
public string Website { get; set; } public string Website { get; set; }
public string MenuGroup { 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 List<OrderItem> Items { get; set; } = new();
public decimal Subtotal { get; set; } public decimal Subtotal { get; set; }
public decimal Commission { 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.GroupName).HasMaxLength(128);
b.Property(x => x.TaxLabel).HasMaxLength(64); b.Property(x => x.TaxLabel).HasMaxLength(64);
b.Property(x => x.Currency).IsRequired().HasMaxLength(8); 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"); b.HasIndex(x => new { x.Name }).IsUnique().HasFilter("[IsDeleted] = 0");
@ -1011,6 +1014,7 @@ public class PlatformDbContext :
b.ConfigureByConvention(); b.ConfigureByConvention();
b.Property(o => o.Name).HasMaxLength(64); 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.OrganizationName).HasMaxLength(128);
b.Property(o => o.Founder).HasMaxLength(128); b.Property(o => o.Founder).HasMaxLength(128);
b.Property(o => o.TaxOffice).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.Email).HasMaxLength(128);
b.Property(o => o.Website).HasMaxLength(128); b.Property(o => o.Website).HasMaxLength(128);
b.Property(o => o.MenuGroup).HasMaxLength(64); 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.Subtotal).IsRequired().HasPrecision(18, 2);
b.Property(o => o.Commission).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 namespace Sozsoft.Platform.Migrations
{ {
[DbContext(typeof(PlatformDbContext))] [DbContext(typeof(PlatformDbContext))]
[Migration("20260625183130_Initial")] [Migration("20260706213526_Initial")]
partial class Initial partial class Initial
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -1542,6 +1542,20 @@ namespace Sozsoft.Platform.Migrations
b.Property<int>("PhoneCode") b.Property<int>("PhoneCode")
.HasColumnType("int"); .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") b.Property<bool>("StateRequired")
.HasColumnType("bit"); .HasColumnType("bit");
@ -4071,6 +4085,10 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(512) .HasMaxLength(512)
.HasColumnType("nvarchar(512)"); .HasColumnType("nvarchar(512)");
b.Property<string>("BillingCycle")
.HasMaxLength(32)
.HasColumnType("nvarchar(32)");
b.Property<string>("City") b.Property<string>("City")
.HasMaxLength(128) .HasMaxLength(128)
.HasColumnType("nvarchar(128)"); .HasColumnType("nvarchar(128)");
@ -4127,6 +4145,11 @@ namespace Sozsoft.Platform.Migrations
.HasDefaultValue(false) .HasDefaultValue(false)
.HasColumnName("IsDeleted"); .HasColumnName("IsDeleted");
b.Property<bool>("IsExisting")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<DateTime?>("LastModificationTime") b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2") .HasColumnType("datetime2")
.HasColumnName("LastModificationTime"); .HasColumnName("LastModificationTime");
@ -4135,6 +4158,12 @@ namespace Sozsoft.Platform.Migrations
.HasColumnType("uniqueidentifier") .HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId"); .HasColumnName("LastModifierId");
b.Property<DateTime?>("LicenseStartTime")
.HasColumnType("datetime2");
b.Property<DateTime?>("LicenseStopTime")
.HasColumnType("datetime2");
b.Property<string>("MenuGroup") b.Property<string>("MenuGroup")
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("nvarchar(64)"); .HasColumnType("nvarchar(64)");
@ -4158,6 +4187,11 @@ namespace Sozsoft.Platform.Migrations
b.Property<Guid>("PaymentMethodId") b.Property<Guid>("PaymentMethodId")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");
b.Property<int>("Period")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(1);
b.Property<string>("PhoneNumber") b.Property<string>("PhoneNumber")
.HasMaxLength(20) .HasMaxLength(20)
.HasColumnType("nvarchar(20)"); .HasColumnType("nvarchar(20)");

View file

@ -1573,6 +1573,7 @@ namespace Sozsoft.Platform.Migrations
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false), Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Name = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true), Name = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false), 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), OrganizationName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
Founder = 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), 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), Email = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
Website = 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), 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), 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), 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), 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), GroupName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: true),
Currency = table.Column<string>(type: "nvarchar(8)", maxLength: 8, nullable: false), Currency = table.Column<string>(type: "nvarchar(8)", maxLength: 8, nullable: false),
PhoneCode = table.Column<int>(type: "int", 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), TaxLabel = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
ZipRequired = table.Column<bool>(type: "bit", nullable: false), ZipRequired = table.Column<bool>(type: "bit", nullable: false),
StateRequired = 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") b.Property<int>("PhoneCode")
.HasColumnType("int"); .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") b.Property<bool>("StateRequired")
.HasColumnType("bit"); .HasColumnType("bit");
@ -4068,6 +4082,10 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(512) .HasMaxLength(512)
.HasColumnType("nvarchar(512)"); .HasColumnType("nvarchar(512)");
b.Property<string>("BillingCycle")
.HasMaxLength(32)
.HasColumnType("nvarchar(32)");
b.Property<string>("City") b.Property<string>("City")
.HasMaxLength(128) .HasMaxLength(128)
.HasColumnType("nvarchar(128)"); .HasColumnType("nvarchar(128)");
@ -4124,6 +4142,11 @@ namespace Sozsoft.Platform.Migrations
.HasDefaultValue(false) .HasDefaultValue(false)
.HasColumnName("IsDeleted"); .HasColumnName("IsDeleted");
b.Property<bool>("IsExisting")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<DateTime?>("LastModificationTime") b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2") .HasColumnType("datetime2")
.HasColumnName("LastModificationTime"); .HasColumnName("LastModificationTime");
@ -4132,6 +4155,12 @@ namespace Sozsoft.Platform.Migrations
.HasColumnType("uniqueidentifier") .HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId"); .HasColumnName("LastModifierId");
b.Property<DateTime?>("LicenseStartTime")
.HasColumnType("datetime2");
b.Property<DateTime?>("LicenseStopTime")
.HasColumnType("datetime2");
b.Property<string>("MenuGroup") b.Property<string>("MenuGroup")
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("nvarchar(64)"); .HasColumnType("nvarchar(64)");
@ -4155,6 +4184,11 @@ namespace Sozsoft.Platform.Migrations
b.Property<Guid>("PaymentMethodId") b.Property<Guid>("PaymentMethodId")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");
b.Property<int>("Period")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(1);
b.Property<string>("PhoneNumber") b.Property<string>("PhoneNumber")
.HasMaxLength(20) .HasMaxLength(20)
.HasColumnType("nvarchar(20)"); .HasColumnType("nvarchar(20)");

View file

@ -15,11 +15,123 @@ import {
FaUser, FaUser,
FaUserPlus, FaUserPlus,
} from 'react-icons/fa' } from 'react-icons/fa'
import React, { useState } from 'react' import React, { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { ROUTES_ENUM } from '@/routes/route.constant' 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 { interface TenantFormProps {
onSubmit: (tenant: CustomTenantDto) => void onSubmit: (tenant: CustomTenantDto) => void
@ -29,22 +141,203 @@ interface TenantFormProps {
export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => { export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
const [isExisting, setIsExisting] = useState<boolean>(true) const [isExisting, setIsExisting] = useState<boolean>(true)
const [formData, setFormData] = useState<Partial<CustomTenantDto>>({}) 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 navigate = useNavigate()
const { translate } = useLocalization() 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) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault() 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) => { const handleInputChange = (field: keyof CustomTenantDto, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value })) setFormData((prev) => ({ ...prev, [field]: value }))
} }
const getTenantInfo = async () => { const resetExistingTenantDetails = (name: string) => {
if (!formData.name) return 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 { 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) => ({ setFormData((prev) => ({
...prev, ...prev,
@ -68,8 +361,13 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
website: tenant.data.website, website: tenant.data.website,
menuGroup: tenant.data.menuGroup, menuGroup: tenant.data.menuGroup,
})) }))
setFoundTenantName(tenant.data.name)
} catch (error) { } catch (error) {
setFoundTenantName('')
setExistingTenantError('Kurum bulunamadı. Lütfen organization code bilgisini kontrol edin.')
console.error('Kurum bilgisi alınırken hata:', error) console.error('Kurum bilgisi alınırken hata:', error)
} finally {
setIsLoadingTenant(false)
} }
} }
@ -139,7 +437,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
autoFocus autoFocus
placeholder="Enter your organization code" placeholder="Enter your organization code"
value={formData.name || ''} value={formData.name || ''}
onChange={(e) => handleInputChange('name', e.target.value)} onChange={(e) => resetExistingTenantDetails(e.target.value)}
onKeyDown={async (e) => { onKeyDown={async (e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.preventDefault() e.preventDefault()
@ -151,23 +449,36 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
</div> </div>
<Button <Button
type="button" type="button"
disabled={isLoadingTenant || !formData.name?.trim()}
onClick={getTenantInfo} onClick={getTenantInfo}
icon={<FaSearch className="w-4 h-4" />} icon={<FaSearch className="w-4 h-4" />}
variant="solid" variant="solid"
size="sm" size="sm"
className="!h-[40px] min-w-[180px] sm:mr-2 sm:rounded-r-lg sm:rounded-l-none [&>span]:gap-2" 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> </Button>
</div> </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 && ( {formData.organizationName && (
<div className="grid grid-cols-1 gap-y-3 text-sm text-gray-700 dark:text-gray-300 p-3"> <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"> <div className="flex items-center gap-2">
<FaUser className="w-4 h-4 text-gray-500" /> <FaUser className="w-4 h-4 text-gray-500" />
<span className="font-medium"> <span className="font-medium">{translate('::LoginPanel.Profil')}</span>
{translate('::LoginPanel.Profil')}
</span>
<span>{formData.founder}</span> <span>{formData.founder}</span>
</div> </div>
@ -189,18 +500,14 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaPhone className="w-4 h-4 text-gray-500" /> <FaPhone className="w-4 h-4 text-gray-500" />
<span className="font-medium"> <span className="font-medium">{translate('::blog.categories.mobile')}:</span>
{translate('::blog.categories.mobile')}:
</span>
<span>{formData.mobileNumber}</span> <span>{formData.mobileNumber}</span>
</div> </div>
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<FaMapPin className="w-4 h-4 text-gray-500 mt-0.5" /> <FaMapPin className="w-4 h-4 text-gray-500 mt-0.5" />
<div> <div>
<span className="font-medium"> <span className="font-medium">{translate('::App.Address')}:</span>
{translate('::App.Address')}:
</span>
<div>{formData.address1}</div> <div>{formData.address1}</div>
</div> </div>
</div> </div>
@ -215,9 +522,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaGlobe className="w-4 h-4 text-gray-500" /> <FaGlobe className="w-4 h-4 text-gray-500" />
<span className="font-medium"> <span className="font-medium">{translate('::Public.common.city')}:</span>
{translate('::Public.common.city')}:
</span>
<span>{formData.city}</span> <span>{formData.city}</span>
</div> </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 className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <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> </label>
<div className="relative"> <div className="relative">
<FaPhone className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" /> <Select<LocationOption<CountryDto>>
<input
type="tel"
required required
placeholder="+90 (___) ___-____" isClearable
value={formData.phoneNumber || ''} isLoading={isLoadingCountries}
onChange={(e) => handleInputChange('phoneNumber', e.target.value)} options={countryOptions}
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" value={
countryOptions.find((option) => option.value === formData.country) || null
}
onChange={handleCountryChange}
placeholder="Ülke seçin"
/> />
</div> </div>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <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> </label>
<div className="relative"> <div className="relative">
<FaEnvelope className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" /> <Select<LocationOption<CityDto>>
<input
type="email"
required required
placeholder="sample@email.com" isClearable
value={formData.email || ''} isDisabled={!formData.country}
onChange={(e) => handleInputChange('email', e.target.value)} 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" 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>
@ -358,69 +707,87 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <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> </label>
<div className="relative"> <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 <input
type="text" type="tel"
placeholder="Türkiye" required
value={formData.country || ''} inputMode="numeric"
onChange={(e) => handleInputChange('country', e.target.value)} placeholder={phoneMask.replace(/0/g, '_')}
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" 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> </div>
{phoneError && (
<p id="tenant-phone-error" className="mt-1 text-sm text-red-600">
{phoneError}
</p>
)}
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <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> </label>
<div className="relative"> <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 <input
type="text" type="email"
required required
placeholder="İstanbul" placeholder="sample@email.com"
value={formData.city || ''} value={formData.email || ''}
onChange={(e) => handleInputChange('city', e.target.value)} onChange={(e) => {
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" handleInputChange('email', e.target.value)
/> if (emailError) {
</div> setEmailError(
</div> EMAIL_PATTERN.test(e.target.value)
</div> ? ''
: 'Lütfen geçerli bir e-posta adresi girin.',
<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')} onBlur={() =>
</label> setEmailError(
<div className="relative"> EMAIL_PATTERN.test(formData.email || '')
<FaMapPin className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" /> ? ''
<input : 'Lütfen geçerli bir e-posta adresi girin.',
type="text" )
required }
placeholder="Çekmeköy" aria-invalid={Boolean(emailError)}
value={formData.district || ''} aria-describedby={emailError ? 'tenant-email-error' : undefined}
onChange={(e) => handleInputChange('district', e.target.value)} 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 ${
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" emailError
/> ? 'border-red-500 dark:border-red-500'
</div> : 'border-gray-300 dark:border-gray-700'
</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> </div>
{emailError && (
<p id="tenant-email-error" className="mt-1 text-sm text-red-600">
{emailError}
</p>
)}
</div> </div>
</div> </div>
@ -477,6 +844,8 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
</div> </div>
)} )}
{formError && <p className="mt-2 text-sm text-red-600">{formError}</p>}
<div className="flex justify-between items-center mt-6"> <div className="flex justify-between items-center mt-6">
<Button <Button
type="button" type="button"
@ -489,6 +858,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
</Button> </Button>
<Button <Button
type="submit" type="submit"
disabled={isExisting && !canSubmitExistingTenant}
icon={<FaArrowRight className="w-5 h-5" />} icon={<FaArrowRight className="w-5 h-5" />}
variant="solid" variant="solid"
color="green-600" color="green-600"
@ -503,4 +873,3 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
</div> </div>
) )
} }

View file

@ -60,6 +60,10 @@ export interface InstallmentOptionDto {
export interface OrderDto { export interface OrderDto {
tenant: CustomTenantDto tenant: CustomTenantDto
items: BasketItem[] items: BasketItem[]
billingCycle: BillingCycle
period: number
licenseStartTime?: string
licenseStopTime?: string
subtotal: number subtotal: number
commission: number commission: number
total: 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: { data: {
tenant: orderData.tenant, tenant: orderData.tenant,
items: orderData.items, items: orderData.items,
billingCycle: orderData.billingCycle,
period: orderData.period,
licenseStartTime: orderData.licenseStartTime,
licenseStopTime: orderData.licenseStopTime,
subtotal: orderData.subtotal, subtotal: orderData.subtotal,
commission: orderData.commission, commission: orderData.commission,
total: orderData.total, total: orderData.total,

View file

@ -62,13 +62,34 @@ const Payment: React.FC = () => {
setCartState(clearCart()) 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>) => { const handlePaymentComplete = async (paymentData: Record<string, unknown>) => {
if (!tenant) return if (!tenant) return
try { 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 = { const orderData = {
tenant, tenant,
items: cartState.items, items: cartState.items,
billingCycle,
period,
licenseStartTime: licenseStartTime.toISOString(),
licenseStopTime: licenseStopTime.toISOString(),
subtotal: cartState.total, subtotal: cartState.total,
commission: paymentData.commission, commission: paymentData.commission,
total: paymentData.total, total: paymentData.total,
@ -136,20 +157,20 @@ const Payment: React.FC = () => {
<div className="container mx-auto px-4 pt-4"> <div className="container mx-auto px-4 pt-4">
<PaymentForm <PaymentForm
cartState={cartState}
tenant={tenant} tenant={tenant}
onBack={handleBackToTenant} onBack={handleBackToTenant}
onComplete={handlePaymentComplete} onComplete={handlePaymentComplete}
cartState={cartState}
/> />
<Cart <Cart
cartState={cartState}
isOpen={isCartOpen} isOpen={isCartOpen}
clearCart={handleClearCart}
removeItem={handleRemoveItem}
updateQuantity={handleUpdateQuantity}
onClose={() => setIsCartOpen(false)} onClose={() => setIsCartOpen(false)}
onCheckout={handleCheckout} onCheckout={handleCheckout}
cartState={cartState}
updateQuantity={handleUpdateQuantity}
removeItem={handleRemoveItem}
clearCart={handleClearCart}
/> />
</div> </div>
</div> </div>