Sales Order güncellemeleri

This commit is contained in:
Sedat ÖZTÜRK 2026-07-07 16:41:07 +03:00
parent 5c9c99206c
commit 4fd22fe682
34 changed files with 1399 additions and 578 deletions

View file

@ -57,8 +57,8 @@ public static class LookupQueryValues
else
{
return $"SELECT * FROM (" +
$"SELECT NULL AS \"Key\", 'Host' AS \"Name\" " +
$"UNION ALL " +
// $"SELECT NULL AS \"Key\", 'Host' AS \"Name\" " +
// $"UNION ALL " +
$"SELECT " +
$"\"AbpTenants\".\"Id\" AS \"Key\", " +
$"\"AbpTenants\".\"Name\" AS \"Name\" " +
@ -178,11 +178,13 @@ public static class LookupQueryValues
public static string ProductValues =
$"SELECT " +
$"\"Id\" AS \"Key\", " +
$"\"Name\" AS \"Name\" " +
$"FROM \"{TableNameResolver.GetFullTableName(nameof(TableNameEnum.Product))}\" " +
$"WHERE \"IsDeleted\" = 'false' " +
$"ORDER BY \"Name\";";
$"\"{FullNameTable(TableNameEnum.Product)}\".\"Id\" AS \"Key\", " +
$"\"{FullNameTable(TableNameEnum.LanguageText)}\".\"Value\" AS \"Name\" " +
$"FROM \"{FullNameTable(TableNameEnum.Product)}\" " +
$"LEFT OUTER JOIN \"{FullNameTable(TableNameEnum.LanguageText)}\" " +
$"ON \"{FullNameTable(TableNameEnum.Product)}\".\"Name\" = \"{FullNameTable(TableNameEnum.LanguageText)}\".\"Key\" " +
$"WHERE \"{FullNameTable(TableNameEnum.Product)}\".\"IsDeleted\" = 'false' " +
$"ORDER BY \"{FullNameTable(TableNameEnum.LanguageText)}\".\"Value\";";
public static string MenuCodeValues =
$"SELECT " +

View file

@ -8,15 +8,17 @@ namespace Sozsoft.Platform.Public;
public class OrderDto : EntityDto<Guid>
{
public CustomTenantDto Tenant { get; set; }
public List<OrderItemDto> Items { get; set; }
public bool IsExisting { get; set; }
public string BillingCycle { get; set; }
public int Period { get; set; }
public DateTime? LicenseStartTime { get; set; }
public DateTime? LicenseStopTime { get; set; }
public DateTime? LicenseEndTime { get; set; }
public decimal Subtotal { get; set; }
public decimal VatTotal { get; set; }
public decimal Commission { get; set; }
public decimal Total { get; set; }

View file

@ -8,6 +8,8 @@ public class OrderItemDto
public ProductDto Product { get; set; }
public string BillingCycle { get; set; } // monthly | yearly
public int Quantity { get; set; }
public decimal VatRate { get; set; }
public decimal VatAmount { get; set; }
public decimal TotalPrice { get; set; }
}

View file

@ -13,6 +13,7 @@ public class ProductDto : EntityDto<Guid>
public int Order { get; set; }
public decimal? MonthlyPrice { get; set; }
public decimal? YearlyPrice { get; set; }
public decimal VatRate { get; set; }
public bool IsQuantityBased { get; set; }
public string ImageUrl { get; set; }
public string Unit =>

View file

@ -4,11 +4,8 @@ namespace Sozsoft.Platform.Tenants;
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 Name { get; set; }
public string OrganizationName { get; set; }
public string Founder { get; set; }
public long? VknTckn { get; set; }
@ -26,5 +23,6 @@ public class CustomTenantDto
public string Email { get; set; }
public string Website { get; set; }
public string MenuGroup { get; set; }
public string Reference { get; set; }
}

View file

@ -1,4 +1,5 @@
using Sozsoft.Platform.Identity.Dto;
using Sozsoft.Platform.Extensions;
using Sozsoft.Platform.OrganizationUnits;
using Sozsoft.Platform.Tenants;
using Volo.Abp.Identity;
@ -76,10 +77,44 @@ public partial class CreateUpdateOrganizationUnitDtoToOrganizationUnitMapper : M
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
public partial class TenantToCustomTenantDtoMapper : MapperBase<Tenant, CustomTenantDto>
{
[MapperIgnoreTarget(nameof(CustomTenantDto.IsExisting))]
[MapperIgnoreTarget(nameof(CustomTenantDto.OrganizationName))]
[MapperIgnoreTarget(nameof(CustomTenantDto.Founder))]
[MapperIgnoreTarget(nameof(CustomTenantDto.VknTckn))]
[MapperIgnoreTarget(nameof(CustomTenantDto.TaxOffice))]
[MapperIgnoreTarget(nameof(CustomTenantDto.Country))]
[MapperIgnoreTarget(nameof(CustomTenantDto.City))]
[MapperIgnoreTarget(nameof(CustomTenantDto.District))]
[MapperIgnoreTarget(nameof(CustomTenantDto.Township))]
[MapperIgnoreTarget(nameof(CustomTenantDto.Address1))]
[MapperIgnoreTarget(nameof(CustomTenantDto.Address2))]
[MapperIgnoreTarget(nameof(CustomTenantDto.PostalCode))]
[MapperIgnoreTarget(nameof(CustomTenantDto.MobileNumber))]
[MapperIgnoreTarget(nameof(CustomTenantDto.PhoneNumber))]
[MapperIgnoreTarget(nameof(CustomTenantDto.FaxNumber))]
[MapperIgnoreTarget(nameof(CustomTenantDto.Email))]
[MapperIgnoreTarget(nameof(CustomTenantDto.Website))]
[MapperIgnoreTarget(nameof(CustomTenantDto.MenuGroup))]
[MapperIgnoreTarget(nameof(CustomTenantDto.Reference))]
public override partial CustomTenantDto Map(Tenant source);
[MapperIgnoreTarget(nameof(CustomTenantDto.IsExisting))]
[MapperIgnoreTarget(nameof(CustomTenantDto.OrganizationName))]
[MapperIgnoreTarget(nameof(CustomTenantDto.Founder))]
[MapperIgnoreTarget(nameof(CustomTenantDto.VknTckn))]
[MapperIgnoreTarget(nameof(CustomTenantDto.TaxOffice))]
[MapperIgnoreTarget(nameof(CustomTenantDto.Country))]
[MapperIgnoreTarget(nameof(CustomTenantDto.City))]
[MapperIgnoreTarget(nameof(CustomTenantDto.District))]
[MapperIgnoreTarget(nameof(CustomTenantDto.Township))]
[MapperIgnoreTarget(nameof(CustomTenantDto.Address1))]
[MapperIgnoreTarget(nameof(CustomTenantDto.Address2))]
[MapperIgnoreTarget(nameof(CustomTenantDto.PostalCode))]
[MapperIgnoreTarget(nameof(CustomTenantDto.MobileNumber))]
[MapperIgnoreTarget(nameof(CustomTenantDto.PhoneNumber))]
[MapperIgnoreTarget(nameof(CustomTenantDto.FaxNumber))]
[MapperIgnoreTarget(nameof(CustomTenantDto.Email))]
[MapperIgnoreTarget(nameof(CustomTenantDto.Website))]
[MapperIgnoreTarget(nameof(CustomTenantDto.MenuGroup))]
[MapperIgnoreTarget(nameof(CustomTenantDto.Reference))]
public override partial void Map(Tenant source, CustomTenantDto destination);
public override void BeforeMap(Tenant source)
@ -88,6 +123,23 @@ public partial class TenantToCustomTenantDtoMapper : MapperBase<Tenant, CustomTe
public override void AfterMap(Tenant source, CustomTenantDto destination)
{
destination.OrganizationName = source.GetOrganizationName();
destination.Founder = source.GetFounder();
destination.VknTckn = source.GetVknTckn();
destination.TaxOffice = source.GetTaxOffice();
destination.Country = source.GetCountry();
destination.City = source.GetCity();
destination.District = source.GetDistrict();
destination.Township = source.GetTownship();
destination.Address1 = source.GetAddress1();
destination.Address2 = source.GetAddress2();
destination.PostalCode = source.GetPostalCode();
destination.MobileNumber = source.GetMobileNumber();
destination.PhoneNumber = source.GetPhoneNumber();
destination.FaxNumber = source.GetFaxNumber();
destination.Email = source.GetEmail();
destination.Website = source.GetWebsite();
destination.MenuGroup = source.GetMenuGroup();
}
}

View file

@ -8,8 +8,10 @@ using Sozsoft.Platform.Data.Seeds;
using Sozsoft.Sender.Mail;
using Volo.Abp.Settings;
using Sozsoft.Platform.Demos;
using Microsoft.Extensions.Logging;
using Volo.Abp.Domain.Entities;
using System.Linq;
using System.Net;
using Volo.Abp.Application.Dtos;
using System.Text.Json;
using Volo.Abp.Identity;
@ -463,15 +465,9 @@ 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()
var entity = new Order(GuidGenerator.Create())
{
Name = input.Tenant.Name,
IsActive = input.Tenant.IsActive,
IsExisting = input.Tenant.IsExisting,
TenantId = input.Tenant.Id,
OrganizationName = input.Tenant.OrganizationName,
Founder = input.Tenant.Founder,
VknTckn = input.Tenant.VknTckn,
@ -482,40 +478,77 @@ public class PublicAppService : PlatformAppService
Country = input.Tenant.Country,
City = input.Tenant.City,
PostalCode = input.Tenant.PostalCode,
MobileNumber = input.Tenant.MobileNumber,
PhoneNumber = input.Tenant.PhoneNumber,
FaxNumber = input.Tenant.FaxNumber,
MobileNumber = NormalizePhoneNumber(input.Tenant.MobileNumber, input.Tenant.Country),
PhoneNumber = NormalizePhoneNumber(input.Tenant.PhoneNumber, input.Tenant.Country),
FaxNumber = NormalizePhoneNumber(input.Tenant.FaxNumber, input.Tenant.Country),
Email = input.Tenant.Email,
Website = input.Tenant.Website,
MenuGroup = input.Tenant.MenuGroup,
BillingCycle = billingCycle,
Period = period,
BillingCycle = input.BillingCycle,
Period = input.Period,
LicenseStartTime = input.LicenseStartTime,
LicenseStopTime = input.LicenseStopTime,
LicenseEndTime = input.LicenseEndTime,
Subtotal = input.Subtotal,
VatTotal = input.VatTotal,
Commission = input.Commission,
Total = input.Total,
PaymentMethodId = input.PaymentMethodId,
Installment = input.Installment,
PaymentDataJson = JsonSerializer.Serialize(input.PaymentData),
IsExisting = input.IsExisting,
};
decimal subtotal = 0;
decimal vatTotal = 0;
foreach (var item in input.Items)
{
var productId = item.ProductId != Guid.Empty ? item.ProductId : item.Product.Id;
var product = await _productRepository.FindAsync(productId);
if (product == null && !item.Product.Name.IsNullOrWhiteSpace())
{
product = await _productRepository.FirstOrDefaultAsync(x => x.Name == item.Product.Name);
}
if (product == null)
{
throw new EntityNotFoundException(typeof(Product), productId);
}
var vatRate = product.VatRate;
var price = GetOrderItemPrice(product, item.BillingCycle);
var grossTotal = Math.Round(item.TotalPrice, 2, MidpointRounding.AwayFromZero);
var netTotal = vatRate > 0
? Math.Round(grossTotal / (1 + vatRate), 2, MidpointRounding.AwayFromZero)
: grossTotal;
var vatAmount = Math.Round(grossTotal - netTotal, 2, MidpointRounding.AwayFromZero);
subtotal += netTotal;
vatTotal += vatAmount;
entity.Items.Add(new OrderItem
{
OrderId = entity.Id,
Order = entity,
ProductId = item.Product.Id,
ProductName = item.Product.Name,
ProductId = product.Id,
ProductName = product.Name,
BillingCycle = item.BillingCycle,
Period = input.Period,
Quantity = item.Quantity,
TotalPrice = item.TotalPrice
Price = price,
VatRate = vatRate,
VatAmount = vatAmount,
TotalPrice = grossTotal
});
}
entity.Subtotal = Math.Round(subtotal, 2, MidpointRounding.AwayFromZero);
entity.VatTotal = Math.Round(vatTotal, 2, MidpointRounding.AwayFromZero);
entity.Total = Math.Round(entity.Subtotal + entity.VatTotal + entity.Commission, 2, MidpointRounding.AwayFromZero);
await _orderRepository.InsertAsync(entity, autoSave: true);
await SendOrderCreatedEmailsAsync(entity);
return new OrderDto
{
@ -525,6 +558,202 @@ public class PublicAppService : PlatformAppService
};
}
private static decimal GetOrderItemPrice(Product product, string billingCycle)
{
return string.Equals(billingCycle, "monthly", StringComparison.OrdinalIgnoreCase)
? product.MonthlyPrice ?? 0
: product.YearlyPrice ?? 0;
}
private async Task SendOrderCreatedEmailsAsync(Order order)
{
var recipients = new List<string>();
if (!order.Email.IsNullOrWhiteSpace())
{
recipients.Add(order.Email.Trim());
}
var notificationEmails = await _settingProvider.GetOrNullAsync("App.SiteManagement.General.NewTenantNotificationEmails");
if (!notificationEmails.IsNullOrWhiteSpace())
{
recipients.AddRange(notificationEmails
.Split(new[] { ',', ';', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)
.Select(email => email.Trim())
.Where(email => !email.IsNullOrWhiteSpace()));
}
recipients = recipients
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
if (recipients.Count == 0)
{
return;
}
var senderName = await _settingProvider.GetOrNullAsync(SeedConsts.AbpSettings.Mailing.Default.DefaultFromDisplayName);
var senderEmail = await _settingProvider.GetOrNullAsync(SeedConsts.AbpSettings.Mailing.Default.DefaultFromAddress);
KeyValuePair<string, string>? sender = null;
if (!senderEmail.IsNullOrWhiteSpace())
{
sender = new KeyValuePair<string, string>(senderName ?? string.Empty, senderEmail);
}
var body = BuildOrderEmailBody(order);
var subject = $"{PlatformConsts.AppName} : Yeni Sipariş #{order.Id}";
foreach (var recipient in recipients)
{
try
{
await _emailSender.SendEmailAsync(
recipient,
sender,
new { },
body,
subject,
null,
true);
}
catch (Exception ex)
{
Logger.LogError(ex, "Order email could not be sent to {Recipient}", recipient);
}
}
}
private static string BuildOrderEmailBody(Order order)
{
var itemRows = string.Join(string.Empty, order.Items.Select(item => $@"
<tr>
<td style=""padding:12px;border-bottom:1px solid #e5e7eb;font-weight:600;color:#111827;"">{Html(item.ProductName)}</td>
<td style=""padding:12px;border-bottom:1px solid #e5e7eb;text-align:right;color:#374151;"">{item.Quantity}</td>
<td style=""padding:12px;border-bottom:1px solid #e5e7eb;text-align:right;color:#374151;"">{BillingCycleLabel(item.BillingCycle)}</td>
<td style=""padding:12px;border-bottom:1px solid #e5e7eb;text-align:right;color:#374151;"">%{item.VatRate * 100:0.##} / {FormatPrice(item.VatAmount)}</td>
<td style=""padding:12px;border-bottom:1px solid #e5e7eb;text-align:right;font-weight:700;color:#111827;"">{FormatPrice(item.TotalPrice)}</td>
</tr>"));
return $@"
<!doctype html>
<html>
<body style=""margin:0;background:#f3f4f6;font-family:Arial,Helvetica,sans-serif;color:#111827;"">
<div style=""max-width:920px;margin:0 auto;padding:24px;"">
<div style=""background:#ffffff;border:1px solid #e5e7eb;border-radius:14px;overflow:hidden;"">
<div style=""display:flex;justify-content:space-between;gap:16px;padding:24px;background:#f9fafb;border-bottom:1px solid #e5e7eb;"">
<div>
<div style=""font-size:22px;font-weight:800;"">Sipariş Oluşturuldu</div>
<div style=""margin-top:6px;color:#4b5563;"">Sipariş No: <strong>#{order.Id}</strong></div>
</div>
<div style=""text-align:right;color:#4b5563;"">
<div style=""font-size:12px;text-transform:uppercase;letter-spacing:.08em;"">Invoice</div>
<div style=""font-weight:700;color:#111827;"">{FormatDate(DateTime.Now)}</div>
</div>
</div>
<div style=""padding:24px;"">
<div style=""display:grid;grid-template-columns:1fr 1fr;gap:20px;"">
<div style=""border:1px solid #e5e7eb;border-radius:12px;padding:18px;"">
<div style=""font-size:18px;font-weight:800;margin-bottom:14px;"">Customer Information</div>
<div style=""line-height:1.7;color:#374151;"">
<strong>{Html(order.OrganizationName)}</strong><br />
{Html(order.Email)}<br />
{Html(FormatPhoneNumber(order.PhoneNumber ?? order.MobileNumber))}<br />
{Html(order.Address1)}<br />
{Html(order.District)} / {Html(order.City)} / {Html(order.Country)}
</div>
</div>
<div style=""border:1px solid #e5e7eb;border-radius:12px;padding:18px;"">
<div style=""font-size:18px;font-weight:800;margin-bottom:14px;"">License Information</div>
<div style=""line-height:1.8;color:#374151;"">
<div><strong>License Start Time:</strong> {FormatDate(order.LicenseStartTime)}</div>
<div><strong>License End Time:</strong> {FormatDate(order.LicenseEndTime)}</div>
<div><strong>Billing Cycle:</strong> {BillingCycleLabel(order.BillingCycle)}</div>
<div><strong>Period:</strong> {order.Period}</div>
</div>
</div>
</div>
<table style=""width:100%;border-collapse:collapse;margin-top:24px;border:1px solid #e5e7eb;border-radius:12px;overflow:hidden;"">
<thead>
<tr style=""background:#f9fafb;color:#6b7280;font-size:12px;text-transform:uppercase;letter-spacing:.06em;"">
<th style=""padding:12px;text-align:left;"">Product</th>
<th style=""padding:12px;text-align:right;"">Qty</th>
<th style=""padding:12px;text-align:right;"">Cycle</th>
<th style=""padding:12px;text-align:right;"">VAT</th>
<th style=""padding:12px;text-align:right;"">Amount</th>
</tr>
</thead>
<tbody>{itemRows}</tbody>
</table>
<div style=""max-width:360px;margin:24px 0 0 auto;line-height:1.9;color:#374151;"">
<div style=""display:flex;justify-content:space-between;""><span>Subtotal</span><strong>{FormatPrice(order.Subtotal)}</strong></div>
<div style=""display:flex;justify-content:space-between;""><span>VAT</span><strong>{FormatPrice(order.VatTotal)}</strong></div>
<div style=""display:flex;justify-content:space-between;""><span>Commission</span><strong>{FormatPrice(order.Commission)}</strong></div>
<div style=""display:flex;justify-content:space-between;border-top:1px solid #e5e7eb;margin-top:8px;padding-top:10px;font-size:18px;color:#111827;""><span>Total</span><strong>{FormatPrice(order.Total)}</strong></div>
</div>
</div>
</div>
</div>
</body>
</html>";
}
private static string BillingCycleLabel(string billingCycle)
{
return billingCycle == "monthly" ? "Aylık" : "Yıllık";
}
private static string NormalizePhoneNumber(string value, string country)
{
if (value.IsNullOrWhiteSpace())
{
return value;
}
var digits = new string(value.Where(char.IsDigit).ToArray());
if (country == "Türkiye" || digits.StartsWith("90"))
{
if (digits.StartsWith("90") && digits.Length > 10)
{
digits = digits[^10..];
}
if (digits.StartsWith("0") && digits.Length > 10)
{
digits = digits[^10..];
}
}
return digits;
}
private static string FormatPhoneNumber(string value)
{
var digits = NormalizePhoneNumber(value, string.Empty);
if (digits?.Length != 10)
{
return value;
}
return $"({digits[..3]}) {digits[3..6]}-{digits[6..]}";
}
private static string FormatDate(DateTime? value)
{
return value.HasValue ? value.Value.ToString("dd.MM.yyyy") : "-";
}
private static string FormatPrice(decimal value)
{
return string.Format(new System.Globalization.CultureInfo("tr-TR"), "{0:C2}", value);
}
private static string Html(string value)
{
return WebUtility.HtmlEncode(value ?? string.Empty);
}
public async Task<AboutDto> GetAboutAsync()
{
var entity = await _aboutRepository.FirstOrDefaultAsync() ?? throw new EntityNotFoundException(typeof(About));

View file

@ -74,7 +74,7 @@ public class PlatformTenantAppService : TenantAppService, IPlatformTenantAppServ
}
var dto = ObjectMapper.Map<Tenant, CustomTenantDto>(tenant);
dto.IsActive = tenant.GetIsActive();
dto.Name = tenant.Name;
dto.OrganizationName = tenant.GetOrganizationName();
dto.Founder = tenant.GetFounder();
dto.VknTckn = tenant.GetVknTckn();

View file

@ -40,6 +40,22 @@
"selectOptions": {},
"order": 2
},
{
"code": "App.SiteManagement.General.NewTenantNotificationEmails",
"nameKey": "App.SiteManagement.General.NewTenantNotificationEmails",
"descriptionKey": "App.SiteManagement.General.NewTenantNotificationEmails.Description",
"defaultValue": "destek@sozsoft.com",
"isVisibleToClients": false,
"providers": "T|G|D",
"isInherited": false,
"isEncrypted": false,
"mainGroupKey": "App.SiteManagement",
"subGroupKey": "App.SiteManagement.General",
"requiredPermissionName": "App.SiteManagement.General",
"dataType": "Text",
"selectOptions": {},
"order": 3
},
{
"code": "App.SiteManagement.Theme.Style",
"nameKey": "App.SiteManagement.Theme.Style",

View file

@ -2334,6 +2334,18 @@
"en": "The emails sent for timed login notifications.",
"tr": "Süreli giriş bildirimleri için gönderilen mailler."
},
{
"resourceName": "Platform",
"key": "App.SiteManagement.General.NewTenantNotificationEmails",
"en": "New Tenant Notification Emails",
"tr": "Yeni Kiracı Bildirim Mailleri"
},
{
"resourceName": "Platform",
"key": "App.SiteManagement.General.NewTenantNotificationEmails.Description",
"en": "The emails sent to new tenants.",
"tr": "Yeni kiracılar için gönderilen mailler."
},
{
"resourceName": "Platform",
"key": "Abp.Localization",
@ -8061,14 +8073,14 @@
{
"resourceName": "Platform",
"key": "Public.payment.customer.code",
"tr": "Kurum Kodu",
"en": "Organization Code"
"tr": "Kurum",
"en": "Organization"
},
{
"resourceName": "Platform",
"key": "Public.payment.customer.company",
"tr": "Şirket",
"en": "Company"
"tr": "Kurum Adı",
"en": "Organization Name"
},
{
"resourceName": "Platform",
@ -8268,6 +8280,18 @@
"tr": "Adet",
"en": "Quantity"
},
{
"resourceName": "Platform",
"key": "Public.products.kdv",
"tr": "KDV",
"en": "VAT"
},
{
"resourceName": "Platform",
"key": "Public.products.kdvdahil",
"tr": "KDV Dahil",
"en": "VAT Included"
},
{
"resourceName": "Platform",
"key": "Public.products.savings.yearly",
@ -13758,6 +13782,12 @@
"en": "Billing Cycle",
"tr": "Fatura Döngüsü"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.Price",
"en": "Price",
"tr": "Fiyat"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.Period",
@ -13772,8 +13802,8 @@
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.LicenseStopTime",
"en": "License Stop Time",
"key": "App.Listform.ListformField.LicenseEndTime",
"en": "License End Time",
"tr": "Lisans Bitiş Tarihi"
},
{
@ -16180,7 +16210,7 @@
"resourceName": "Platform",
"key": "App.Listform.ListformField.IsExisting",
"en": "Existing Customer",
"tr": "Mevcut Musteri"
"tr": "Mevcut Müşteri"
},
{
"resourceName": "Platform",
@ -17178,6 +17208,24 @@
"en": "Total Price",
"tr": "Toplam Fiyat"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.VatAmount",
"en": "VAT Amount",
"tr": "KDV Tutarı"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.VatRate",
"en": "VAT Rate",
"tr": "KDV Oranı"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.VatTotal",
"en": "VAT Total",
"tr": "KDV Toplamı"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.TotalRevenue",

View file

@ -122,6 +122,14 @@ public static class ListFormSeeder_DefaultJsons
{
IsPivot = true
});
public static string DefaultColumnTotalSummaryJson(string showInColumn, string summaryType = "sum") => JsonSerializer.Serialize(new ColumnTotalSummaryDto
{
SummaryType = summaryType,
DisplayFormat = "{0}",
ValueFormat = "#,##0.00",
ShowInColumn = showInColumn
});
public static string DefaultGroupingJson(int groupIndex) => JsonSerializer.Serialize(new ColumnGroupingDto
{
AllowGrouping = true,

View file

@ -95,8 +95,8 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
{
new() { Order=1, ColCount=3, ColSpan=1, ItemType="group", Items =
[
new EditingFormItemDto { Order=1, DataField = "OrganizationName", ColSpan=1, EditorType2=EditorTypes.dxTextBox },
new EditingFormItemDto { Order=2, DataField = "Name", ColSpan=1, EditorType2=EditorTypes.dxTextBox },
new EditingFormItemDto { Order=1, DataField = "Name", ColSpan=1, EditorType2=EditorTypes.dxTextBox },
new EditingFormItemDto { Order=2, DataField = "OrganizationName", ColSpan=1, EditorType2=EditorTypes.dxTextBox },
new EditingFormItemDto { Order=3, DataField = "Founder", ColSpan=1, EditorType2=EditorTypes.dxTextBox },
new EditingFormItemDto { Order=4, DataField = "VknTckn", ColSpan=1, EditorType2=EditorTypes.dxNumberBox },
new EditingFormItemDto { Order=5, DataField = "TaxOffice", ColSpan=1, EditorType2=EditorTypes.dxTextBox },
@ -382,6 +382,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
Visible = true,
IsActive = true,
AllowSearch = true,
ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(TenantManagementPermissions.Tenants.Create, TenantManagementPermissions.Tenants.Default, TenantManagementPermissions.Tenants.Update, true, true, false),
PivotSettingsJson = DefaultPivotSettingsJson
@ -6442,13 +6443,15 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
new EditingFormItemDto { Order = 4, DataField = "Order", ColSpan = 1, EditorType2 = EditorTypes.dxNumberBox },
new EditingFormItemDto { Order = 5, DataField = "MonthlyPrice", ColSpan = 1, EditorType2 = EditorTypes.dxNumberBox, EditorOptions=EditorOptionValues.NumberStandartFormat() },
new EditingFormItemDto { Order = 6, DataField = "YearlyPrice", ColSpan = 1, EditorType2 = EditorTypes.dxNumberBox, EditorOptions=EditorOptionValues.NumberStandartFormat() },
new EditingFormItemDto { Order = 7, DataField = "IsQuantityBased", ColSpan = 1, EditorType2 = EditorTypes.dxCheckBox },
new EditingFormItemDto { Order = 8, DataField = "ImageUrl", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox },
new EditingFormItemDto { Order = 7, DataField = "VatRate", ColSpan = 1, EditorType2 = EditorTypes.dxNumberBox, EditorOptions=EditorOptionValues.NumberStandartFormat() },
new EditingFormItemDto { Order = 8, DataField = "IsQuantityBased", ColSpan = 1, EditorType2 = EditorTypes.dxCheckBox },
new EditingFormItemDto { Order = 9, DataField = "ImageUrl", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox },
]
}
}),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
new() { FieldName = "VatRate", FieldDbType = DbType.Decimal, Value = "0.20", CustomValueType = FieldCustomValueTypeEnum.Value },
new() { FieldName = "IsQuantityBased", FieldDbType = DbType.Boolean, Value = "true", CustomValueType = FieldCustomValueTypeEnum.Value }
})
});
@ -6610,6 +6613,26 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
EditorOptions=EditorOptionValues.NumberStandartFormat()
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Decimal,
FieldName = "VatRate",
CaptionName = "App.Listform.ListformField.VatRate",
Alignment = "right",
Width = 0,
ListOrderNo = 8,
Visible = true,
IsActive = true,
AllowSearch = true,
ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson,
EditorOptions=EditorOptionValues.NumberStandartFormat()
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
@ -6617,7 +6640,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "IsQuantityBased",
CaptionName = "App.Listform.ListformField.IsQuantityBased",
Width = 0,
ListOrderNo = 8,
ListOrderNo = 9,
Visible = true,
IsActive = true,
@ -6634,7 +6657,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "ImageUrl",
CaptionName = "App.Listform.ListformField.ImageUrl",
Width = 0,
ListOrderNo = 9,
ListOrderNo = 10,
Visible = true,
IsActive = true,
@ -6956,41 +6979,42 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Order)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
PagerOptionJson = DefaultPagerOptionJson,
EditingOptionJson = DefaultEditingOptionJson(listFormName, 1000, 700, true, true, true, true, false, true),
EditingOptionJson = DefaultEditingOptionJson(listFormName, 1200, 700, true, true, true, true, false, true),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>
{
new() { Order=1, ColCount=3, ColSpan=1, ItemType="group", Items =
new() { Order=1, ColCount=4, ColSpan=1, ItemType="group", Items =
[
new EditingFormItemDto { Order = 1, DataField = "OrganizationName", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox },
new EditingFormItemDto { Order = 2, DataField = "Founder", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox },
new EditingFormItemDto { Order = 3, DataField = "VknTckn", ColSpan = 1, EditorType2 = EditorTypes.dxNumberBox },
new EditingFormItemDto { Order = 4, DataField = "TaxOffice", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox },
new EditingFormItemDto { Order = 5, DataField = "Address1", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox },
new EditingFormItemDto { Order = 6, DataField = "Address2", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox },
new EditingFormItemDto { Order = 7, DataField = "Country", ColSpan = 1, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
new EditingFormItemDto { Order = 8, DataField = "City", ColSpan = 1, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
new EditingFormItemDto { Order = 9, DataField = "District", ColSpan = 1, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
new EditingFormItemDto { Order = 10, DataField = "PostalCode", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox },
new EditingFormItemDto { Order = 1, DataField = "TenantId", ColSpan = 1, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
new EditingFormItemDto { Order = 2, DataField = "IsExisting", ColSpan = 1, EditorType2 = EditorTypes.dxCheckBox },
new EditingFormItemDto { Order = 3, DataField = "OrganizationName", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox },
new EditingFormItemDto { Order = 4, DataField = "Founder", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox },
new EditingFormItemDto { Order = 5, DataField = "VknTckn", ColSpan = 1, EditorType2 = EditorTypes.dxNumberBox },
new EditingFormItemDto { Order = 6, DataField = "TaxOffice", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox },
new EditingFormItemDto { Order = 7, DataField = "Address1", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox },
new EditingFormItemDto { Order = 8, DataField = "Address2", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox },
new EditingFormItemDto { Order = 9, DataField = "Country", ColSpan = 1, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
new EditingFormItemDto { Order = 10, DataField = "City", ColSpan = 1, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
new EditingFormItemDto { Order = 11, DataField = "District", ColSpan = 1, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
new EditingFormItemDto { Order = 12, DataField = "PostalCode", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox },
new EditingFormItemDto { Order = 13, DataField = "PhoneNumber", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox, EditorOptions=EditorOptionValues.PhoneEditorOptions },
new EditingFormItemDto { Order = 14, DataField = "MobileNumber", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox, EditorOptions=EditorOptionValues.PhoneEditorOptions },
new EditingFormItemDto { Order = 15, DataField = "FaxNumber", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox, EditorOptions=EditorOptionValues.PhoneEditorOptions },
]
},
new() { Order=2, ColCount=3, ColSpan=1, ItemType="group", Items =
new() { Order=2, ColCount=4, ColSpan=1, ItemType="group", Items =
[
new EditingFormItemDto { Order = 11, DataField = "PhoneNumber", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox, EditorOptions=EditorOptionValues.PhoneEditorOptions },
new EditingFormItemDto { Order = 12, DataField = "MobileNumber", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox, EditorOptions=EditorOptionValues.PhoneEditorOptions },
new EditingFormItemDto { Order = 13, DataField = "FaxNumber", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox, EditorOptions=EditorOptionValues.PhoneEditorOptions },
new EditingFormItemDto { Order = 14, DataField = "Email", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox },
new EditingFormItemDto { Order = 15, DataField = "Website", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox },
new EditingFormItemDto { Order = 16, DataField = "Subtotal", ColSpan = 1, EditorType2 = EditorTypes.dxNumberBox, EditorOptions=EditorOptionValues.NumberStandartFormat() },
new EditingFormItemDto { Order = 17, DataField = "Commission", 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 = 20, DataField = "Installment", ColSpan = 1, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
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 },
new EditingFormItemDto { Order = 16, DataField = "PaymentMethodId", ColSpan = 1, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
new EditingFormItemDto { Order = 17, DataField = "Installment", ColSpan = 1, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
new EditingFormItemDto { Order = 18, DataField = "LicenseStartTime", ColSpan = 1, EditorType2 = EditorTypes.dxDateBox },
new EditingFormItemDto { Order = 19, DataField = "BillingCycle", ColSpan = 1, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
new EditingFormItemDto { Order = 20, DataField = "Period", ColSpan = 1, EditorType2 = EditorTypes.dxNumberBox },
new EditingFormItemDto { Order = 21, DataField = "LicenseEndTime", ColSpan = 1, EditorType2 = EditorTypes.dxDateBox },
new EditingFormItemDto { Order = 22, DataField = "Email", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox },
new EditingFormItemDto { Order = 23, DataField = "Website", ColSpan = 1, EditorType2 = EditorTypes.dxTextBox },
new EditingFormItemDto { Order = 24, DataField = "Subtotal", ColSpan = 1, EditorType2 = EditorTypes.dxNumberBox, EditorOptions=EditorOptionValues.NumberStandartFormat() },
new EditingFormItemDto { Order = 25, DataField = "VatTotal", ColSpan = 1, EditorType2 = EditorTypes.dxNumberBox, EditorOptions=EditorOptionValues.NumberStandartFormat() },
new EditingFormItemDto { Order = 26, DataField = "Commission", ColSpan = 1, EditorType2 = EditorTypes.dxNumberBox, EditorOptions=EditorOptionValues.NumberStandartFormat() },
new EditingFormItemDto { Order = 27, DataField = "Total", ColSpan = 1, EditorType2 = EditorTypes.dxNumberBox, EditorOptions=EditorOptionValues.NumberStandartFormat() },
]
}
}),
@ -7000,7 +7024,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
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 = "VatTotal", FieldDbType = DbType.Decimal, Value = "0", CustomValueType = FieldCustomValueTypeEnum.Value },
new() { FieldName = "Commission", FieldDbType = DbType.Decimal, Value = "0", CustomValueType = FieldCustomValueTypeEnum.Value }
}),
SubFormsJson = JsonSerializer.Serialize(new List<dynamic>() {
@ -7019,7 +7043,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
})
});
#region Purchase Order Fields
#region Sales Order Fields
await _listFormFieldRepository.InsertManyAsync([
new()
{
@ -7039,6 +7063,29 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PivotSettingsJson = DefaultPivotSettingsJson,
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Guid,
FieldName = "TenantId",
CaptionName = "App.Listform.ListformField.TenantId",
Width = 0,
ListOrderNo = 2,
Visible = true,
IsActive = true,
LookupJson = JsonSerializer.Serialize(new LookupDto {
DataSourceType = UiLookupDataSourceTypeEnum.Query,
DisplayExpr = "Name",
ValueExpr = "Key",
LookupQuery = LookupQueryValues.TenantValues(),
}),
AllowSearch = true,
ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson,
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
@ -7046,7 +7093,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "OrganizationName",
CaptionName = "App.Listform.ListformField.OrganizationName",
Width = 0,
ListOrderNo = 2,
ListOrderNo = 3,
Visible = true,
IsActive = true,
@ -7064,7 +7111,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "Founder",
CaptionName = "App.Listform.ListformField.Founder",
Width = 0,
ListOrderNo = 3,
ListOrderNo = 4,
Visible = true,
IsActive = true,
@ -7082,7 +7129,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "IsExisting",
CaptionName = "App.Listform.ListformField.IsExisting",
Width = 0,
ListOrderNo = 4,
ListOrderNo = 5,
Visible = true,
IsActive = true,
@ -7099,11 +7146,20 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "BillingCycle",
CaptionName = "App.Listform.ListformField.BillingCycle",
Width = 0,
ListOrderNo = 5,
ListOrderNo = 6,
Visible = true,
IsActive = true,
AllowSearch = true,
LookupJson = JsonSerializer.Serialize(new LookupDto {
DataSourceType = UiLookupDataSourceTypeEnum.StaticData,
DisplayExpr = "name",
ValueExpr = "key",
LookupQuery = JsonSerializer.Serialize(new LookupDataDto[] {
new () { Key="yearly", Name="Yearly" },
new () { Key="monthly", Name="Monthly" },
}),
}),
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson,
@ -7116,7 +7172,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "Period",
CaptionName = "App.Listform.ListformField.Period",
Width = 0,
ListOrderNo = 6,
ListOrderNo = 7,
Visible = true,
IsActive = true,
@ -7133,7 +7189,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "LicenseStartTime",
CaptionName = "App.Listform.ListformField.LicenseStartTime",
Width = 0,
ListOrderNo = 7,
ListOrderNo = 8,
Visible = true,
IsActive = true,
@ -7147,10 +7203,10 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.DateTime,
FieldName = "LicenseStopTime",
CaptionName = "App.Listform.ListformField.LicenseStopTime",
FieldName = "LicenseEndTime",
CaptionName = "App.Listform.ListformField.LicenseEndTime",
Width = 0,
ListOrderNo = 8,
ListOrderNo = 9,
Visible = true,
IsActive = true,
@ -7167,7 +7223,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "VknTckn",
CaptionName = "App.Listform.ListformField.VknTckn",
Width = 0,
ListOrderNo = 9,
ListOrderNo = 10,
Visible = true,
IsActive = true,
@ -7185,7 +7241,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "TaxOffice",
CaptionName = "App.Listform.ListformField.TaxOffice",
Width = 0,
ListOrderNo = 10,
ListOrderNo = 11,
Visible = true,
IsActive = true,
@ -7203,7 +7259,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "Address1",
CaptionName = "App.Listform.ListformField.Address1",
Width = 0,
ListOrderNo = 11,
ListOrderNo = 12,
Visible = true,
IsActive = true,
@ -7221,7 +7277,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "Address2",
CaptionName = "App.Listform.ListformField.Address2",
Width = 0,
ListOrderNo = 12,
ListOrderNo = 13,
Visible = true,
IsActive = true,
@ -7238,7 +7294,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "Country",
CaptionName = "App.Listform.ListformField.Country",
Width = 0,
ListOrderNo = 13,
ListOrderNo = 14,
Visible = true,
IsActive = true,
@ -7264,7 +7320,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "City",
CaptionName = "App.Listform.ListformField.City",
Width = 0,
ListOrderNo = 14,
ListOrderNo = 15,
Visible = true,
IsActive = true,
@ -7293,7 +7349,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "District",
CaptionName = "App.Listform.ListformField.District",
Width = 0,
ListOrderNo = 15,
ListOrderNo = 16,
Visible = true,
IsActive = true,
@ -7321,7 +7377,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "PhoneNumber",
CaptionName = "Abp.Identity.User.UserInformation.PhoneNumber",
Width = 0,
ListOrderNo = 16,
ListOrderNo = 17,
Visible = true,
IsActive = true,
@ -7340,7 +7396,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "MobileNumber",
CaptionName = "App.Listform.ListformField.MobileNumber",
Width = 0,
ListOrderNo = 17,
ListOrderNo = 18,
Visible = true,
IsActive = true,
@ -7359,7 +7415,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "FaxNumber",
CaptionName = "App.Listform.ListformField.FaxNumber",
Width = 0,
ListOrderNo = 18,
ListOrderNo = 19,
Visible = true,
IsActive = true,
@ -7377,7 +7433,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "Website",
CaptionName = "App.Listform.ListformField.Website",
Width = 0,
ListOrderNo = 19,
ListOrderNo = 20,
Visible = true,
IsActive = true,
@ -7396,7 +7452,27 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
CaptionName = "App.Listform.ListformField.Subtotal",
Alignment = "right",
Width = 0,
ListOrderNo = 20,
ListOrderNo = 21,
Visible = true,
IsActive = true,
AllowSearch = true,
ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson,
EditorOptions=EditorOptionValues.NumberStandartFormat()
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Decimal,
FieldName = "VatTotal",
CaptionName = "App.Listform.ListformField.VatTotal",
Alignment = "right",
Width = 0,
ListOrderNo = 22,
Visible = true,
IsActive = true,
@ -7416,7 +7492,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
CaptionName = "App.Listform.ListformField.Commission",
Alignment = "right",
Width = 0,
ListOrderNo = 21,
ListOrderNo = 23,
Visible = true,
IsActive = true,
@ -7436,7 +7512,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
CaptionName = "App.Listform.ListformField.Total",
Alignment = "right",
Width = 0,
ListOrderNo = 22,
ListOrderNo = 25,
Visible = true,
IsActive = true,
@ -7455,7 +7531,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "PaymentMethodId",
CaptionName = "App.Listform.ListformField.PaymentMethodId",
Width = 0,
ListOrderNo = 23,
ListOrderNo = 24,
Visible = true,
IsActive = true,
@ -7474,7 +7550,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "Installment",
CaptionName = "App.Listform.ListformField.Installment",
Width = 0,
ListOrderNo = 24,
ListOrderNo = 26,
Visible = true,
IsActive = true,
@ -7493,7 +7569,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "IsActive",
CaptionName = "App.Listform.ListformField.IsActive",
Width = 0,
ListOrderNo = 25,
ListOrderNo = 27,
Visible = true,
IsActive = true,
@ -7510,7 +7586,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "PostalCode",
CaptionName = "App.Listform.ListformField.PostalCode",
Width = 0,
ListOrderNo = 26,
ListOrderNo = 27,
Visible = true,
IsActive = true,
@ -7528,7 +7604,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
FieldName = "Email",
CaptionName = "App.Listform.ListformField.Email",
Width = 0,
ListOrderNo = 27,
ListOrderNo = 28,
Visible = true,
IsActive = true,
@ -7541,133 +7617,6 @@ 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
@ -7705,7 +7654,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
SelectionJson = DefaultSelectionSingleJson,
ColumnOptionJson = DefaultColumnOptionJson(),
PermissionJson = DefaultPermissionJson(listFormName),
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Uom)),
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.OrderItem)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
PagerOptionJson = DefaultPagerOptionJson,
@ -7716,7 +7665,11 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
new EditingFormItemDto { Order = 1, DataField = "ProductId", ColSpan = 1, EditorType2=EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
new EditingFormItemDto { Order = 2, DataField = "BillingCycle", ColSpan = 1, EditorType2=EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
new EditingFormItemDto { Order = 3, DataField = "Quantity", ColSpan = 1, EditorType2=EditorTypes.dxNumberBox },
new EditingFormItemDto { Order = 4, DataField = "TotalPrice", ColSpan = 1, EditorType2=EditorTypes.dxNumberBox },
new EditingFormItemDto { Order = 4, DataField = "Price", ColSpan = 1, EditorType2=EditorTypes.dxNumberBox },
new EditingFormItemDto { Order = 5, DataField = "Period", ColSpan = 1, EditorType2=EditorTypes.dxNumberBox },
new EditingFormItemDto { Order = 6, DataField = "VatRate", ColSpan = 1, EditorType2=EditorTypes.dxNumberBox },
new EditingFormItemDto { Order = 7, DataField = "VatAmount", ColSpan = 1, EditorType2=EditorTypes.dxNumberBox },
new EditingFormItemDto { Order = 8, DataField = "TotalPrice", ColSpan = 1, EditorType2=EditorTypes.dxNumberBox },
]}
}),
}, autoSave: true
@ -7818,8 +7771,8 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Decimal,
FieldName = "TotalPrice",
CaptionName = "App.Listform.ListformField.TotalPrice",
FieldName = "Price",
CaptionName = "App.Listform.ListformField.Price",
Alignment = "right",
Width = 0,
ListOrderNo = 5,
@ -7833,6 +7786,88 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PivotSettingsJson = DefaultPivotSettingsJson,
EditorOptions=EditorOptionValues.NumberStandartFormat()
},
new ListFormField
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Int32,
FieldName = "Period",
CaptionName = "App.Listform.ListformField.Period",
Alignment = "right",
Width = 0,
ListOrderNo = 6,
Visible = true,
IsActive = true,
AllowSearch = true,
ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson,
EditorOptions=EditorOptionValues.NumberStandartFormat()
},
new ListFormField
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Decimal,
FieldName = "VatRate",
CaptionName = "App.Listform.ListformField.VatRate",
Alignment = "right",
Width = 0,
ListOrderNo = 7,
Visible = true,
IsActive = true,
AllowSearch = true,
ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson,
EditorOptions=EditorOptionValues.NumberStandartFormat()
},
new ListFormField
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Decimal,
FieldName = "VatAmount",
CaptionName = "App.Listform.ListformField.VatAmount",
Alignment = "right",
Width = 0,
ListOrderNo = 8,
Visible = true,
IsActive = true,
AllowSearch = true,
ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson,
EditorOptions = EditorOptionValues.NumberStandartFormat(),
TotalSummaryJson = DefaultColumnTotalSummaryJson("VatAmount")
},
new ListFormField
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Decimal,
FieldName = "TotalPrice",
CaptionName = "App.Listform.ListformField.TotalPrice",
Alignment = "right",
Width = 0,
ListOrderNo = 9,
Visible = true,
IsActive = true,
AllowSearch = true,
ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson,
EditorOptions=EditorOptionValues.NumberStandartFormat(),
TotalSummaryJson = DefaultColumnTotalSummaryJson("TotalPrice")
},
], autoSave: true);
#endregion
}

View file

@ -6,10 +6,7 @@ namespace Sozsoft.Platform.Entities;
public class Order : FullAuditedEntity<Guid>
{
public string Name { get; set; }
public bool IsActive { get; set; }
public bool IsExisting { get; set; }
public Guid? TenantId { get; set; }
public string OrganizationName { get; set; }
public string Founder { get; set; }
public long? VknTckn { get; set; }
@ -27,24 +24,33 @@ public class Order : FullAuditedEntity<Guid>
public string Website { get; set; }
public string MenuGroup { get; set; }
public bool IsExisting { get; set; }
public bool IsActive { get; set; }
public string BillingCycle { get; set; }
public int Period { get; set; }
public DateTime? LicenseStartTime { get; set; }
public DateTime? LicenseStopTime { get; set; }
public DateTime? LicenseEndTime { get; set; }
public List<OrderItem> Items { get; set; } = new();
public decimal Subtotal { get; set; }
public decimal VatTotal { get; set; }
public decimal Commission { get; set; }
public decimal Total { get; set; }
public Guid PaymentMethodId { get; set; }
public int? Installment { get; set; }
public string PaymentDataJson { get; set; } // JSON olarak saklanacak
public List<OrderItem> Items { get; set; } = new();
public Order()
{
}
public Order(Guid id)
: base(id)
{
}
}
public class OrderItem : FullAuditedEntity<Guid>
@ -54,7 +60,11 @@ public class OrderItem : FullAuditedEntity<Guid>
public Guid ProductId { get; set; }
public string ProductName { get; set; }
public string BillingCycle { get; set; }
public int Period { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
public decimal VatRate { get; set; }
public decimal VatAmount { get; set; }
public decimal TotalPrice { get; set; }
public OrderItem()

View file

@ -11,6 +11,7 @@ public class Product : FullAuditedEntity<Guid>
public int Order { get; set; }
public decimal? MonthlyPrice { get; set; }
public decimal? YearlyPrice { get; set; }
public decimal VatRate { get; set; } = 0.20m;
public bool IsQuantityBased { get; set; } = false;
public string ImageUrl { get; set; }
}

View file

@ -980,6 +980,7 @@ public class PlatformDbContext :
b.Property(x => x.Category).IsRequired().HasMaxLength(64);
b.Property(x => x.MonthlyPrice).HasPrecision(18, 2);
b.Property(x => x.YearlyPrice).HasPrecision(18, 2);
b.Property(x => x.VatRate).IsRequired().HasPrecision(5, 3).HasDefaultValue(0.20m);
b.Property(x => x.ImageUrl).HasMaxLength(512);
b.HasIndex(x => new { x.Name }).IsUnique().HasFilter("[IsDeleted] = 0");
@ -1013,7 +1014,12 @@ public class PlatformDbContext :
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.Order)), Prefix.DbSchema);
b.ConfigureByConvention();
b.Property(o => o.Name).HasMaxLength(64);
var isActiveComputedColumnSql = _isPostgreSql
? "CASE WHEN \"LicenseEndTime\" IS NOT NULL AND CURRENT_DATE > \"LicenseEndTime\"::date THEN false ELSE true END"
: "CONVERT(bit, CASE WHEN [LicenseEndTime] IS NOT NULL AND CONVERT(date, GETDATE()) > CONVERT(date, [LicenseEndTime]) THEN 0 ELSE 1 END)";
b.Property(o => o.IsActive)
.HasComputedColumnSql(isActiveComputedColumnSql, stored: false);
b.Property(o => o.IsExisting).IsRequired().HasDefaultValue(false);
b.Property(o => o.OrganizationName).HasMaxLength(128);
b.Property(o => o.Founder).HasMaxLength(128);
@ -1034,6 +1040,7 @@ public class PlatformDbContext :
b.Property(o => o.Period).IsRequired().HasDefaultValue(1);
b.Property(o => o.Subtotal).IsRequired().HasPrecision(18, 2);
b.Property(o => o.VatTotal).IsRequired().HasPrecision(18, 2);
b.Property(o => o.Commission).IsRequired().HasPrecision(18, 2);
b.Property(o => o.Total).IsRequired().HasPrecision(18, 2);
@ -1052,6 +1059,10 @@ public class PlatformDbContext :
b.Property(i => i.ProductName).IsRequired().HasMaxLength(128);
b.Property(i => i.BillingCycle).IsRequired().HasMaxLength(32);
b.Property(i => i.Period).IsRequired().HasDefaultValue(1);
b.Property(i => i.Price).IsRequired().HasPrecision(18, 2);
b.Property(i => i.VatRate).IsRequired().HasPrecision(5, 3);
b.Property(i => i.VatAmount).IsRequired().HasPrecision(18, 2);
b.Property(i => i.TotalPrice).IsRequired().HasPrecision(18, 2);
});

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Sozsoft.Platform.Migrations
{
[DbContext(typeof(PlatformDbContext))]
[Migration("20260706213526_Initial")]
[Migration("20260707105700_Initial")]
partial class Initial
{
/// <inheritdoc />
@ -4137,7 +4137,9 @@ namespace Sozsoft.Platform.Migrations
.HasColumnType("int");
b.Property<bool>("IsActive")
.HasColumnType("bit");
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("bit")
.HasComputedColumnSql("CONVERT(bit, CASE WHEN [LicenseEndTime] IS NOT NULL AND CONVERT(date, GETDATE()) > CONVERT(date, [LicenseEndTime]) THEN 0 ELSE 1 END)", false);
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
@ -4158,10 +4160,10 @@ namespace Sozsoft.Platform.Migrations
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<DateTime?>("LicenseStartTime")
b.Property<DateTime?>("LicenseEndTime")
.HasColumnType("datetime2");
b.Property<DateTime?>("LicenseStopTime")
b.Property<DateTime?>("LicenseStartTime")
.HasColumnType("datetime2");
b.Property<string>("MenuGroup")
@ -4172,10 +4174,6 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<string>("Name")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("OrganizationName")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
@ -4208,10 +4206,17 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier");
b.Property<decimal>("Total")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<decimal>("VatTotal")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<long?>("VknTckn")
.HasColumnType("bigint");
@ -4267,6 +4272,15 @@ namespace Sozsoft.Platform.Migrations
b.Property<Guid>("OrderId")
.HasColumnType("uniqueidentifier");
b.Property<int>("Period")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(1);
b.Property<decimal>("Price")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<Guid>("ProductId")
.HasColumnType("uniqueidentifier");
@ -4282,6 +4296,14 @@ namespace Sozsoft.Platform.Migrations
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<decimal>("VatAmount")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<decimal>("VatRate")
.HasPrecision(5, 3)
.HasColumnType("decimal(5,3)");
b.HasKey("Id");
b.HasIndex("OrderId");
@ -4409,6 +4431,12 @@ namespace Sozsoft.Platform.Migrations
b.Property<int>("Order")
.HasColumnType("int");
b.Property<decimal>("VatRate")
.ValueGeneratedOnAdd()
.HasPrecision(5, 3)
.HasColumnType("decimal(5,3)")
.HasDefaultValue(0.20m);
b.Property<decimal?>("YearlyPrice")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");

View file

@ -1571,9 +1571,7 @@ namespace Sozsoft.Platform.Migrations
columns: table => new
{
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),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
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),
@ -1590,11 +1588,14 @@ 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),
IsExisting = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
IsActive = table.Column<bool>(type: "bit", nullable: false, computedColumnSql: "CONVERT(bit, CASE WHEN [LicenseEndTime] IS NOT NULL AND CONVERT(date, GETDATE()) > CONVERT(date, [LicenseEndTime]) THEN 0 ELSE 1 END)", stored: false),
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),
LicenseEndTime = table.Column<DateTime>(type: "datetime2", nullable: true),
Subtotal = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
VatTotal = 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),
PaymentMethodId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
@ -1645,6 +1646,7 @@ namespace Sozsoft.Platform.Migrations
Order = table.Column<int>(type: "int", nullable: false),
MonthlyPrice = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: true),
YearlyPrice = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: true),
VatRate = table.Column<decimal>(type: "decimal(5,3)", precision: 5, scale: 3, nullable: false, defaultValue: 0.20m),
IsQuantityBased = table.Column<bool>(type: "bit", nullable: false),
ImageUrl = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
@ -2890,7 +2892,11 @@ namespace Sozsoft.Platform.Migrations
ProductId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
ProductName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
BillingCycle = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
Period = table.Column<int>(type: "int", nullable: false, defaultValue: 1),
Quantity = table.Column<int>(type: "int", nullable: false),
Price = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
VatRate = table.Column<decimal>(type: "decimal(5,3)", precision: 5, scale: 3, nullable: false),
VatAmount = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
TotalPrice = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),

View file

@ -4134,7 +4134,9 @@ namespace Sozsoft.Platform.Migrations
.HasColumnType("int");
b.Property<bool>("IsActive")
.HasColumnType("bit");
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("bit")
.HasComputedColumnSql("CONVERT(bit, CASE WHEN [LicenseEndTime] IS NOT NULL AND CONVERT(date, GETDATE()) > CONVERT(date, [LicenseEndTime]) THEN 0 ELSE 1 END)", false);
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
@ -4155,10 +4157,10 @@ namespace Sozsoft.Platform.Migrations
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<DateTime?>("LicenseStartTime")
b.Property<DateTime?>("LicenseEndTime")
.HasColumnType("datetime2");
b.Property<DateTime?>("LicenseStopTime")
b.Property<DateTime?>("LicenseStartTime")
.HasColumnType("datetime2");
b.Property<string>("MenuGroup")
@ -4169,10 +4171,6 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<string>("Name")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("OrganizationName")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
@ -4205,10 +4203,17 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier");
b.Property<decimal>("Total")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<decimal>("VatTotal")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<long?>("VknTckn")
.HasColumnType("bigint");
@ -4264,6 +4269,15 @@ namespace Sozsoft.Platform.Migrations
b.Property<Guid>("OrderId")
.HasColumnType("uniqueidentifier");
b.Property<int>("Period")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(1);
b.Property<decimal>("Price")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<Guid>("ProductId")
.HasColumnType("uniqueidentifier");
@ -4279,6 +4293,14 @@ namespace Sozsoft.Platform.Migrations
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<decimal>("VatAmount")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<decimal>("VatRate")
.HasPrecision(5, 3)
.HasColumnType("decimal(5,3)");
b.HasKey("Id");
b.HasIndex("OrderId");
@ -4406,6 +4428,12 @@ namespace Sozsoft.Platform.Migrations
b.Property<int>("Order")
.HasColumnType("int");
b.Property<decimal>("VatRate")
.ValueGeneratedOnAdd()
.HasPrecision(5, 3)
.HasColumnType("decimal(5,3)")
.HasDefaultValue(0.20m);
b.Property<decimal?>("YearlyPrice")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");

View file

@ -29,6 +29,7 @@ public class DynamicFormReport : XtraReport
public const string ReportName = nameof(DynamicFormReport);
private const string QueryName = "DynamicForm";
private const int HorizontalMargin = 50;
private const int LandscapeColumnThreshold = 4;
private const float RowHeight = 24;
private const float ColumnGap = 10;
private static readonly System.Drawing.Color FieldBorderColor = System.Drawing.Color.FromArgb(226, 232, 240);
@ -118,17 +119,23 @@ public class DynamicFormReport : XtraReport
var row = GetFirstRow(await dynamicDataRepository.QueryAsync(selectQuery, connectionString));
ConfigurePage();
var formGroups = GetFormGroups(listForm, reportFields).ToList();
var listSubForms = GetListSubForms(listForm, row).ToList();
var subReports = await CreateSubReportsAsync(listSubForms);
var requiresLandscape = HasWideFormLayout(formGroups) ||
subReports.Any(x => x.Report.RequiresLandscapeLayout);
ConfigurePage(requiresLandscape);
ConfigureDataSource(selectQuery, connectionString, dataSourceType);
await BuildLayoutAsync(listForm, reportFields, row, localizer, DynamicReportImageHelper.GetImageBaseUrl(configuration), currentUser.UserName, isSubReport);
BuildLayout(listForm, formGroups, row, subReports, localizer, DynamicReportImageHelper.GetImageBaseUrl(configuration), currentUser.UserName, isSubReport);
RequestParameters = false;
}
private void ConfigurePage()
private void ConfigurePage(bool landscape)
{
Margins = CreateReportMargins(HorizontalMargin, HorizontalMargin, 35, 35);
PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4;
Landscape = false;
Landscape = landscape;
Font = DynamicReportFontHelper.CreateDataFont();
}
@ -153,23 +160,22 @@ public class DynamicFormReport : XtraReport
DataMember = QueryName;
}
private async Task BuildLayoutAsync(
private void BuildLayout(
ListForm listForm,
IReadOnlyList<FormReportField> fields,
IReadOnlyList<FormGroup> formGroups,
IDictionary<string, object> row,
IReadOnlyList<SubReportSource> subReports,
IStringLocalizer<PlatformResource> localizer,
string imageBaseUrl,
string currentUserName,
bool isSubReport)
{
Bands.Clear();
var formGroups = GetFormGroups(listForm, fields).ToList();
var listSubForms = GetListSubForms(listForm, row).ToList();
var topMargin = new TopMarginBand { HeightF = Margins.Top };
var bottomMargin = new BottomMarginBand { HeightF = 32 };
var reportHeader = new ReportHeaderBand { HeightF = 38 };
var detail = new DetailBand { HeightF = CalculateDetailHeight(formGroups, listSubForms.Count) };
var detail = new DetailBand { HeightF = CalculateDetailHeight(formGroups, subReports.Count) };
Bands.AddRange([topMargin, reportHeader, detail, bottomMargin]);
@ -242,12 +248,12 @@ public class DynamicFormReport : XtraReport
y += 8;
}
foreach (var subForm in listSubForms)
foreach (var subReport in subReports)
{
detail.Controls.Add(new XRSubreport
{
BoundsF = new System.Drawing.RectangleF(0, y, pageWidth, 24),
ReportSource = await DynamicGridReport.CreateAsync(CreateSubReportParameters(subForm.Form, subForm.Filter))
ReportSource = subReport.Report
});
y += 28;
}
@ -255,6 +261,24 @@ public class DynamicFormReport : XtraReport
detail.HeightF = Math.Max(y, RowHeight);
}
private static bool HasWideFormLayout(IEnumerable<FormGroup> formGroups)
{
return formGroups.Any(x => GetSafeColumnCount(x.ColumnCount) >= LandscapeColumnThreshold);
}
private static async Task<List<SubReportSource>> CreateSubReportsAsync(
IReadOnlyList<(SubFormDto Form, string Filter)> subForms)
{
var reports = new List<SubReportSource>();
foreach (var subForm in subForms)
{
reports.Add(new SubReportSource(
await DynamicGridReport.CreateAsync(CreateSubReportParameters(subForm.Form, subForm.Filter))));
}
return reports;
}
private static XRLabel CreateCaptionLabel(
FormReportField field,
IStringLocalizer<PlatformResource> localizer,
@ -728,6 +752,7 @@ public class DynamicFormReport : XtraReport
private sealed record FieldWithEditor(ListFormField Field, string EditorType, int ColumnSpan);
private sealed record FormGroup(string Caption, int ColumnCount, IReadOnlyList<FormReportField> Fields);
private sealed record FormLayoutItem(FormReportField Field, int ColumnIndex, int ColumnSpan);
private sealed record SubReportSource(DynamicGridReport Report);
private sealed record FormReportField(
ListFormField Field,
string BindingName,

View file

@ -33,6 +33,8 @@ public class DynamicGridReport : XtraReport
private static readonly System.Drawing.Color TableBorderColor = System.Drawing.Color.FromArgb(226, 232, 240);
private static IServiceScopeFactory _scopeFactory;
public bool RequiresLandscapeLayout { get; private set; }
public static void SetScopeFactory(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
@ -177,7 +179,7 @@ public class DynamicGridReport : XtraReport
localizer);
reportColumns = await ApplyImageCountsAsync(reportColumns, dynamicDataRepository, selectQuery, connectionString, imageBaseUrl);
ConfigurePage(reportColumns, localizer, stateWidths, isSubReport);
ConfigurePage(reportColumns, localizer, stateWidths);
ConfigureDataSource(
selectQuery,
connectionString,
@ -213,8 +215,7 @@ public class DynamicGridReport : XtraReport
private void ConfigurePage(
IReadOnlyCollection<ReportColumn> visibleFields,
IStringLocalizer<PlatformResource> localizer,
IReadOnlyDictionary<string, float> stateWidths,
bool isSubReport)
IReadOnlyDictionary<string, float> stateWidths)
{
Margins = CreateReportMargins(HorizontalMargin, HorizontalMargin, 35, 35);
PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4;
@ -222,8 +223,9 @@ public class DynamicGridReport : XtraReport
var requestedContentWidth = visibleFields.Sum(x => x.IsImage
? GetImageColumnWidth(x)
: DynamicReportGridStateHelper.GetPreferredColumnWidth(x.Field, localizer, stateWidths));
Landscape = !isSubReport &&
(visibleFields.Count >= LandscapeColumnThreshold || requestedContentWidth > portraitContentWidth);
RequiresLandscapeLayout = visibleFields.Count >= LandscapeColumnThreshold ||
requestedContentWidth > portraitContentWidth;
Landscape = RequiresLandscapeLayout;
Font = DynamicReportFontHelper.CreateDataFont();
}

View file

@ -1,6 +1,16 @@
{
"commit": "e7670c6",
"commit": "708bfd0",
"releases": [
{
"version": "1.1.06",
"buildDate": "2026-07-06",
"commit": "bab639ce6343b451b81d6688e4f7af666f0233da",
"changeLog": [
"- Çekirdek kod için optimizasyon",
"- List Reportlara Gruplu ve Alttoplam eklendi.",
"- Paketlerdeki deprecated uyarları kaldırıldı"
]
},
{
"version": "1.1.05",
"buildDate": "2026-06-27",

View file

@ -6,6 +6,8 @@ import { Button } from '@/components/ui'
interface CartState {
items: BasketItem[]
subtotal: number
vatTotal: number
total: number
globalBillingCycle: BillingCycle
globalPeriod: number
@ -109,7 +111,9 @@ export const Cart: React.FC<CartProps> = ({
<div className="flex items-center justify-between">
<div className="text-sm text-gray-600 dark:text-gray-300">
{item.product.isQuantityBased && item.quantity > 1 && (
<span className="font-medium text-gray-900 dark:text-gray-100">{item.quantity}x - </span>
<span className="font-medium text-gray-900 dark:text-gray-100">
{item.quantity}x -{' '}
</span>
)}
{item.billingCycle === 'monthly'
? translate('::Public.products.billingcycle.monthly')
@ -135,7 +139,9 @@ export const Cart: React.FC<CartProps> = ({
variant="default"
size="xs"
/>
<span className="w-8 text-center text-sm dark:text-gray-100">{item.quantity}</span>
<span className="w-2 text-center text-sm dark:text-gray-100">
{item.quantity}
</span>
<Button
onClick={() => updateQuantity(item.product.id, item.quantity + 1)}
icon={<FaPlus className="w-3 h-3" />}
@ -144,10 +150,10 @@ export const Cart: React.FC<CartProps> = ({
/>
</div>
)}
</div>
<div className="mt-2 font-semibold text-blue-600">
{formatPrice(item.totalPrice)}
<div className="font-semibold text-blue-600">
{formatPrice(item.totalPrice)}
</div>
</div>
</div>
))}
@ -157,21 +163,26 @@ export const Cart: React.FC<CartProps> = ({
{cartState.items.length > 0 && (
<div className="border-t border-gray-200 p-6 dark:border-gray-700">
<div className="flex justify-between items-center mb-4">
<span className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{translate('::App.Listform.ListformField.Total')}
</span>
<span className="text-xl font-bold text-blue-600">
{formatPrice(cartState.total)}
</span>
<div className="space-y-2 mb-4">
<div className="flex justify-between text-sm text-gray-600 dark:text-gray-300">
<span>{translate('::App.Listform.ListformField.Subtotal')}</span>
<span>{formatPrice(cartState.subtotal)}</span>
</div>
<div className="flex justify-between text-sm text-gray-600 dark:text-gray-300">
<span>{translate('::Public.products.kdv')}</span>
<span>{formatPrice(cartState.vatTotal)}</span>
</div>
<div className="flex justify-between items-center pt-2 border-t border-gray-200 dark:border-gray-700">
<span className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{translate('::App.Listform.ListformField.Total')}
</span>
<span className="text-xl font-bold text-blue-600">
{formatPrice(cartState.total)}
</span>
</div>
</div>
<Button
onClick={onCheckout}
block
variant="solid"
size="md"
>
<Button onClick={onCheckout} block variant="solid" size="md">
{translate('::Public.cart.checkout')}
</Button>
</div>

View file

@ -1,46 +1,214 @@
import React from 'react'
import { FaCheckCircle, FaArrowLeft } from 'react-icons/fa'
import { FaArrowLeft, FaBuilding, FaCalendarAlt, FaCheckCircle, FaEnvelope, FaPhone } from 'react-icons/fa'
import { useNavigate } from 'react-router-dom'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { Button } from '@/components/ui'
import { BasketItem, BillingCycle } from '@/proxy/order/models'
import { CustomTenantDto } from '@/proxy/config/models'
export interface OrderSuccessData {
orderId: string
tenant: CustomTenantDto
items: BasketItem[]
billingCycle: BillingCycle
period: number
licenseStartTime?: string
licenseEndTime?: string
subtotal: number
vatTotal: number
commission: number
total: number
installment?: number
}
interface OrderSuccessProps {
orderId: string
order?: OrderSuccessData | null
onBackToShop: () => void
}
export const OrderSuccess: React.FC<OrderSuccessProps> = ({ orderId }) => {
export const OrderSuccess: React.FC<OrderSuccessProps> = ({ orderId, order }) => {
const navigate = useNavigate()
const { translate } = useLocalization()
const formatPrice = (price: number) =>
new Intl.NumberFormat('tr-TR', {
style: 'currency',
currency: 'TRY',
minimumFractionDigits: 2,
}).format(price)
const formatDate = (value?: string) => {
if (!value) return '-'
return new Intl.DateTimeFormat('tr-TR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
}).format(new Date(value))
}
const billingCycleLabel =
order?.billingCycle === 'monthly'
? 'Aylık'
: order?.billingCycle === 'yearly'
? 'Yıllık'
: '-'
const getItemPrice = (item: BasketItem) =>
item.billingCycle === 'monthly'
? item.product.monthlyPrice ?? 0
: item.product.yearlyPrice ?? 0
const vatRateLabel = (() => {
if (!order) return ''
const rates = Array.from(
new Set(order.items.map((item) => Math.round((item.vatRate ?? 0) * 100))),
).filter((rate) => rate > 0)
return rates.length > 0 ? ` (${rates.map((rate) => `%${rate}`).join(', ')})` : ''
})()
return (
<div className="max-w-2xl mx-auto text-center">
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-8 dark:border-gray-700 dark:bg-gray-900 dark:shadow-gray-950/40">
<div className="mb-6">
<FaCheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" />
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100">
{translate('::Public.order.success.title')}
</h2>
<p className="text-gray-600 dark:text-gray-300">
{translate('::Public.order.success.number')}{' '}
<span className="font-semibold text-blue-600">#{orderId}</span>
</p>
<div className="max-w-6xl mx-auto pb-7">
<div className="bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden dark:border-gray-700 dark:bg-gray-900 dark:shadow-gray-950/40">
<div className="flex flex-col gap-4 border-b border-gray-200 bg-gray-50 px-6 py-5 dark:border-gray-700 dark:bg-gray-800/70 md:flex-row md:items-center md:justify-between">
<div className="flex items-center gap-3">
<FaCheckCircle className="h-10 w-10 text-green-500" />
<div>
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100">
{translate('::Public.order.success.title')}
</h2>
<p className="text-sm text-gray-600 dark:text-gray-300">
{translate('::Public.order.success.number')}{' '}
<span className="font-semibold text-blue-600">#{orderId}</span>
</p>
</div>
</div>
<div className="text-left md:text-right">
<div className="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
Invoice
</div>
<div className="text-sm font-semibold text-gray-900 dark:text-gray-100">
{formatDate(new Date().toISOString())}
</div>
</div>
</div>
<div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-6 dark:border-green-900/60 dark:bg-green-950/30">
<h3 className="font-semibold text-green-800 mb-2 dark:text-green-300">
{translate('::Public.order.success.nextSteps')}
</h3>
<ul className="text-sm text-green-700 space-y-1 text-left dark:text-green-200">
<li> {translate('::Public.order.success.step1')}</li>
<li> {translate('::Public.order.success.step2')}</li>
<li> {translate('::Public.order.success.step3')}</li>
</ul>
</div>
{order && (
<div className="p-6">
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
<section className="rounded-lg border border-gray-200 p-5 dark:border-gray-700">
<h3 className="mb-4 text-lg font-semibold text-gray-900 dark:text-gray-100">
Customer Information
</h3>
<div className="space-y-3 text-sm text-gray-700 dark:text-gray-300">
<div className="flex items-center gap-3">
<FaBuilding className="h-4 w-4 text-gray-500" />
<span className="font-medium">{order.tenant.organizationName}</span>
</div>
<div className="flex items-center gap-3">
<FaEnvelope className="h-4 w-4 text-gray-500" />
<span>{order.tenant.email}</span>
</div>
<div className="flex items-center gap-3">
<FaPhone className="h-4 w-4 text-gray-500" />
<span>{order.tenant.phoneNumber || order.tenant.mobileNumber}</span>
</div>
<div className="pt-2 text-gray-600 dark:text-gray-400">
<div>{order.tenant.address1}</div>
<div>
{order.tenant.district} / {order.tenant.city} / {order.tenant.country}
</div>
</div>
</div>
</section>
<div className="flex flex-col sm:flex-row gap-4 items-center justify-center">
<section className="rounded-lg border border-gray-200 p-5 dark:border-gray-700">
<h3 className="mb-4 flex items-center gap-2 text-lg font-semibold text-gray-900 dark:text-gray-100">
<FaCalendarAlt className="h-5 w-5 text-green-600" />
License Information
</h3>
<div className="grid grid-cols-1 gap-3 text-sm text-gray-700 dark:text-gray-300 sm:grid-cols-2">
<div className="rounded-lg bg-gray-50 p-3 dark:bg-gray-800/70">
<div className="text-xs text-gray-500 dark:text-gray-400">License Start Time</div>
<div className="font-semibold">{formatDate(order.licenseStartTime)}</div>
</div>
<div className="rounded-lg bg-gray-50 p-3 dark:bg-gray-800/70">
<div className="text-xs text-gray-500 dark:text-gray-400">License End Time</div>
<div className="font-semibold">{formatDate(order.licenseEndTime)}</div>
</div>
<div className="rounded-lg bg-gray-50 p-3 dark:bg-gray-800/70">
<div className="text-xs text-gray-500 dark:text-gray-400">Billing Cycle</div>
<div className="font-semibold">{billingCycleLabel}</div>
</div>
<div className="rounded-lg bg-gray-50 p-3 dark:bg-gray-800/70">
<div className="text-xs text-gray-500 dark:text-gray-400">Period</div>
<div className="font-semibold">{order.period}</div>
</div>
</div>
</section>
</div>
<section className="mt-6 overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
<div className="grid grid-cols-12 bg-gray-50 px-4 py-3 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:bg-gray-800/70 dark:text-gray-400">
<div className="col-span-3">Product</div>
<div className="col-span-2 text-right">Price</div>
<div className="col-span-1 text-right">Period</div>
<div className="col-span-1 text-right">Qty</div>
<div className="col-span-1 text-right">Cycle</div>
<div className="col-span-2 text-right">{translate('::Public.products.kdv')}</div>
<div className="col-span-2 text-right">Amount</div>
</div>
{order.items.map((item, index) => (
<div
key={`${item.product.id}-${index}`}
className="grid grid-cols-12 border-t border-gray-200 px-4 py-4 text-sm text-gray-700 dark:border-gray-700 dark:text-gray-300"
>
<div className="col-span-3 font-medium text-gray-900 dark:text-gray-100">
{translate('::' + item.product.name)}
</div>
<div className="col-span-2 text-right">{formatPrice(getItemPrice(item))}</div>
<div className="col-span-1 text-right">{order.period}</div>
<div className="col-span-1 text-right">{item.quantity}</div>
<div className="col-span-1 text-right">
{item.billingCycle === 'monthly' ? 'Aylık' : 'Yıllık'}
</div>
<div className="col-span-2 text-right">
%{((item.vatRate ?? 0) * 100).toFixed(0)} / {formatPrice(item.vatAmount ?? 0)}
</div>
<div className="col-span-2 text-right font-semibold">
{formatPrice(item.totalPrice)}
</div>
</div>
))}
</section>
<div className="mt-6 flex justify-end">
<div className="w-full max-w-sm space-y-2 text-sm text-gray-700 dark:text-gray-300">
<div className="flex justify-between">
<span>{translate('::Public.payment.summary.subtotal')}</span>
<span>{formatPrice(order.subtotal)}</span>
</div>
<div className="flex justify-between">
<span>{translate('::Public.products.kdv')}{vatRateLabel}</span>
<span>{formatPrice(order.vatTotal)}</span>
</div>
<div className="flex justify-between">
<span>{translate('::Public.payment.installments.commission')}</span>
<span>{formatPrice(order.commission)}</span>
</div>
<div className="flex justify-between border-t border-gray-200 pt-3 text-lg font-bold text-gray-900 dark:border-gray-700 dark:text-gray-100">
<span>{translate('::App.Listform.ListformField.Total')}</span>
<span className="text-blue-600">{formatPrice(order.total)}</span>
</div>
</div>
</div>
</div>
)}
<div className="border-t border-gray-200 px-6 py-5 dark:border-gray-700">
<Button
onClick={() => navigate(ROUTES_ENUM.public.home)}
icon={<FaArrowLeft className="w-4 h-4" />}

View file

@ -1,19 +1,5 @@
import React, { useEffect, useState } from 'react'
import {
FaCreditCard,
FaLock,
FaArrowLeft,
FaUser,
FaBuilding,
FaMailBulk,
FaPhone,
FaMapMarkerAlt,
FaMap,
FaGlobe,
FaMoneyBillWave,
FaDollarSign,
FaUserPlus,
} from 'react-icons/fa'
import { FaCreditCard, FaLock, FaArrowLeft, FaCalendarAlt, FaClock } from 'react-icons/fa'
import {
BillingCycle,
BasketItem,
@ -27,6 +13,8 @@ import { Button } from '@/components/ui'
interface CartState {
items: BasketItem[]
subtotal: number
vatTotal: number
total: number
globalBillingCycle: BillingCycle
globalPeriod: number
@ -39,12 +27,7 @@ interface PaymentFormProps {
cartState: CartState
}
export const PaymentForm: React.FC<PaymentFormProps> = ({
tenant,
onBack,
onComplete,
cartState,
}) => {
export const PaymentForm: React.FC<PaymentFormProps> = ({ onBack, onComplete, cartState }) => {
const defaultPaymentMethod = 'Kredi Kartı'
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string>(defaultPaymentMethod)
const [selectedInstallment, setSelectedInstallment] = useState<InstallmentOptionDto>()
@ -54,6 +37,9 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({
cvv: '',
cardName: '',
})
const [licenseStartTime, setLicenseStartTime] = useState(() =>
new Date().toISOString().slice(0, 10),
)
const { translate } = useLocalization()
const [paymentMethods, setPaymentMethods] = useState<PaymentMethodDto[]>([])
@ -92,6 +78,9 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({
}
const finalTotal = cartState.total + commission
const period = cartState.globalPeriod > 0 ? cartState.globalPeriod : 1
const billingCycle = cartState.globalBillingCycle || 'yearly'
const licenseEndTime = calculateLicenseEndTime(new Date(licenseStartTime), billingCycle, period)
const formatPrice = (price: number) =>
new Intl.NumberFormat('tr-TR', {
@ -100,10 +89,20 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({
minimumFractionDigits: 2,
}).format(price)
const vatRateLabel = (() => {
const rates = Array.from(
new Set(cartState.items.map((item) => Math.round((item.vatRate ?? 0) * 100))),
).filter((rate) => rate > 0)
return rates.length > 0 ? ` (${rates.map((rate) => `%${rate}`).join(', ')})` : ''
})()
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
onComplete({
...paymentData,
licenseStartTime: new Date(licenseStartTime).toISOString(),
licenseEndTime: licenseEndTime.toISOString(),
paymentMethodId: selectedMethod?.id,
installment:
selectedPaymentMethod === defaultPaymentMethod
@ -118,136 +117,75 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({
setPaymentData((prev) => ({ ...prev, [field]: value }))
}
function calculateLicenseEndTime(
startTime: Date,
selectedBillingCycle: string,
selectedPeriod: number,
) {
const stopTime = new Date(startTime)
if (selectedBillingCycle === 'monthly') {
stopTime.setMonth(stopTime.getMonth() + selectedPeriod)
} else {
stopTime.setFullYear(stopTime.getFullYear() + selectedPeriod)
}
return stopTime
}
const formatDate = (date: Date) =>
new Intl.DateTimeFormat('tr-TR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
}).format(date)
const billingCycleLabel = billingCycle === 'monthly' ? 'Aylık' : 'Yıllık'
return (
<div className="mx-auto px-4 lg:px-8 mt-5">
{loading ? (
<div className="text-center py-12 text-gray-500">
{translate('::App.Loading')}
</div>
<div className="text-center py-12 text-gray-500">{translate('::App.Loading')}</div>
) : (
<form onSubmit={handleSubmit} className="flex flex-col lg:flex-row gap-6">
{/* 3 Sütun: Ödeme Yöntemi | Taksit Seçenekleri | Sipariş Özeti */}
<div className="w-full lg:w-1/3 flex flex-col gap-6">
<div className="bg-white rounded-xl shadow border p-6 dark:border-gray-700 dark:bg-gray-900 dark:shadow-gray-950/40">
{tenant && (
<>
<h2 className="text-lg font-semibold mb-4 flex items-center">
<FaUser className="w-5 h-5 text-green-600 mr-2" />{' '}
{translate('::Public.payment.customerInfo')}
</h2>
<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
<div className="grid grid-cols-1 gap-y-3 text-sm text-gray-700 dark:text-gray-300">
<div className="flex items-center gap-2">
<FaUser className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Public.payment.customer.code')}
</span>
<span>{tenant.name}</span>
</div>
<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>{tenant.founder}</span>
</div>
<div className="flex items-center gap-2">
<FaBuilding className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Public.payment.customer.company')}
</span>
<span>{tenant.organizationName}</span>
</div>
<div className="flex items-center gap-2">
<FaMailBulk className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Abp.Account.EmailAddress')}
</span>
<span>{tenant.email}</span>
</div>
<div className="flex items-center gap-2">
<FaPhone className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Abp.Identity.User.UserInformation.PhoneNumber')}
</span>
<span>{tenant.phoneNumber}</span>
</div>
<div className="flex items-start gap-2">
<FaMapMarkerAlt className="w-4 h-4 text-gray-500 mt-0.5" />
<div>
<span className="font-medium">
{translate('::App.Address')}:
</span>
<div>{tenant.address1}</div>
</div>
</div>
<div className="flex items-center gap-2">
<FaGlobe className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Public.payment.customer.country')}
</span>
<span>{tenant.country}</span>
</div>
<div className="flex items-center gap-2">
<FaGlobe className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Public.common.city')}
</span>
<span>{tenant.city}</span>
</div>
<div className="flex items-center gap-2">
<FaMap className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Public.payment.customer.district')}
</span>
<span>{tenant.district}</span>
</div>
<div className="flex items-center gap-2">
<FaMapMarkerAlt className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Public.payment.customer.postalCode')}:
</span>
<span>{tenant.postalCode}</span>
</div>
<div className="flex items-center gap-2">
<FaMoneyBillWave className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Public.contact.taxOffice')}:
</span>
<span>{tenant.taxOffice}</span>
</div>
<div className="flex items-center gap-2">
<FaDollarSign className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Public.contact.taxNumber')}:
</span>
<span>{tenant.vknTckn}</span>
</div>
{tenant.reference && (
<div className="flex items-center gap-2">
<FaUserPlus className="w-4 h-4 text-gray-500" />
<span className="font-medium">
{translate('::Public.payment.customer.reference')}:
</span>
<span>{tenant.reference}</span>
</div>
)}
</div>
<h2 className="text-lg font-semibold mb-4 flex items-center text-gray-900 dark:text-gray-100">
<FaCalendarAlt className="w-5 h-5 text-green-600 mr-2" />
Lisans Bilgileri
</h2>
<div className="space-y-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
License Start Time
</label>
<input
type="date"
required
value={licenseStartTime}
onChange={(e) => setLicenseStartTime(e.target.value)}
className="w-full px-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 className="grid grid-cols-1 gap-3 text-sm text-gray-700 dark:text-gray-300">
<div className="flex items-center justify-between rounded-lg bg-gray-50 px-4 py-3 dark:bg-gray-800/70">
<span className="font-medium">Period</span>
<span>{period}</span>
</div>
</>
)}
<div className="flex items-center justify-between rounded-lg bg-gray-50 px-4 py-3 dark:bg-gray-800/70">
<span className="font-medium">Billing Cycle</span>
<span>{billingCycleLabel}</span>
</div>
<div className="flex items-center justify-between rounded-lg bg-blue-50 px-4 py-3 text-blue-800 dark:bg-blue-950/40 dark:text-blue-200">
<span className="font-medium flex items-center gap-2">
<FaClock className="h-4 w-4" />
License End Time
</span>
<span>{formatDate(licenseEndTime)}</span>
</div>
</div>
</div>
</div>
</div>
@ -288,7 +226,9 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({
{method.logo}
</span>
<div className="min-w-0 flex-1">
<div className="font-semibold text-gray-900 dark:text-gray-100">{method.name}</div>
<div className="font-semibold text-gray-900 dark:text-gray-100">
{method.name}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
{method.name === defaultPaymentMethod
? translate('::Public.payment.method.installmentsAvailable')
@ -412,21 +352,20 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({
<div className="space-y-4 mb-4">
{cartState.items.map((item, index) => (
<div key={`${item.product.id}-${index}`} className="flex justify-between text-sm">
<div>
<div className="font-medium">{translate('::' + item.product.name)}</div>
<div className="text-gray-500">
{item.quantity}x -{' '}
{item.billingCycle === 'monthly'
? 'Aylık'
: item.billingCycle === 'yearly'
? 'Yıllık'
: 'Aylık'}{' '}
{cartState.globalPeriod > 1 &&
`(${cartState.globalPeriod} ${item.billingCycle === 'monthly' ? 'Ay' : 'Yıl'})`}
<div key={`${item.product.id}-${index}`} className="text-sm">
<div className="flex items-center justify-between gap-4">
<div className="flex min-w-0 items-center gap-3">
{item.product.imageUrl && (
<img
src={item.product.imageUrl}
alt={translate('::' + item.product.name)}
className="h-10 w-10 shrink-0 rounded-md object-cover"
/>
)}
<div className="min-w-0 font-medium">{translate('::' + item.product.name)}</div>
</div>
<div className="font-medium text-right">{formatPrice(item.totalPrice)}</div>
</div>
<div className="font-medium">{formatPrice(item.totalPrice)}</div>
</div>
))}
</div>
@ -434,7 +373,11 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({
<div className="space-y-1 text-sm border-t border-gray-200 dark:border-gray-700 pt-4 text-gray-600 dark:text-gray-300">
<div className="flex justify-between">
<span>{translate('::Public.payment.summary.subtotal')}</span>
<span>{formatPrice(cartState.total)}</span>
<span>{formatPrice(cartState.subtotal)}</span>
</div>
<div className="flex justify-between">
<span>{translate('::Public.products.kdv')}{vatRateLabel}</span>
<span>{formatPrice(cartState.vatTotal)}</span>
</div>
<div className="flex justify-between">
<span>{translate('::Public.payment.installments.commission')}</span>
@ -486,4 +429,3 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({
</div>
)
}

View file

@ -40,10 +40,20 @@ export const ProductCard: React.FC<ProductCardProps> = ({
return globalBillingCycle === 'monthly' ? product.monthlyPrice! : product.yearlyPrice!
}
const getVatRate = () => product.vatRate ?? 0.2
const getTotalPrice = () => {
return getCurrentPrice() * quantity * globalPeriod
}
const getVatAmount = () => {
return getTotalPrice() * getVatRate()
}
const getTotalPriceWithVat = () => {
return getTotalPrice() + getVatAmount()
}
const handleAddToCart = () => {
// Non-quantity products should not be added if already in cart
if (!product.isQuantityBased && isInCart) {
@ -153,12 +163,20 @@ export const ProductCard: React.FC<ProductCardProps> = ({
{formatPrice(getCurrentPrice())}
<span className="text-sm font-normal text-gray-500 ml-1 dark:text-gray-400">{getUnitText()}</span>
</div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{translate('::Public.products.kdv')} (%{(getVatRate() * 100).toFixed(0)}): {formatPrice(getVatAmount())}
</div>
{globalPeriod > 1 && (
<div className="text-lg font-semibold text-blue-600 mt-1">
{translate('::App.Listform.ListformField.Total')} {formatPrice(getTotalPrice())}
{translate('::App.Listform.ListformField.Total')} {formatPrice(getTotalPriceWithVat())}
<span className="text-sm font-normal text-gray-500 ml-1 dark:text-gray-400">{getPeriodText()}</span>
</div>
)}
{globalPeriod <= 1 && (
<div className="text-lg font-semibold text-blue-600 mt-1">
{translate('::Public.products.kdvdahil')} {formatPrice(getTotalPriceWithVat())}
</div>
)}
</div>
{(() => {

View file

@ -14,6 +14,7 @@ import {
FaSearch,
FaUser,
FaUserPlus,
FaCheckCircle,
} from 'react-icons/fa'
import React, { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
@ -36,6 +37,8 @@ interface LocationOption<T> {
}
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
const TENANT_DATA_STORAGE_KEY = 'tenantData'
const TENANT_FORM_DRAFT_STORAGE_KEY = 'tenantFormDraft'
const DEFAULT_PHONE_FORMAT = '000 000 000 000 000'
const TURKEY_PHONE_FORMAT = '(000) 000-0000'
const REQUIRED_NEW_CUSTOMER_FIELDS: Array<keyof CustomTenantDto> = [
@ -123,6 +126,9 @@ const formatPhoneNumber = (value: string, country?: CountryDto) => {
return `+${country.phoneCode}${nationalNumber ? ` ${nationalNumber}` : ''}`
}
const normalizePhoneForStorage = (value?: string, country?: CountryDto) =>
value ? getNationalPhoneDigits(value, country) : value
const isBlankValue = (value: unknown) =>
value === undefined || value === null || String(value).trim() === ''
@ -133,6 +139,11 @@ const hasRequiredFields = (
const normalizeCode = (value?: string) => value?.trim().toLowerCase() || ''
type TenantFormDraft = Partial<CustomTenantDto> & {
isExisting?: boolean
foundTenantName?: string
}
interface TenantFormProps {
onSubmit: (tenant: CustomTenantDto) => void
onBack: () => void
@ -156,6 +167,22 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
const navigate = useNavigate()
const { translate } = useLocalization()
useEffect(() => {
const savedDraft = localStorage.getItem(TENANT_FORM_DRAFT_STORAGE_KEY)
const savedTenant = localStorage.getItem(TENANT_DATA_STORAGE_KEY)
const savedData = savedDraft || savedTenant
if (!savedData) return
try {
const parsed = JSON.parse(savedData) as TenantFormDraft
setIsExisting(parsed.isExisting ?? true)
setFoundTenantName(parsed.foundTenantName || parsed.name || '')
setFormData(parsed)
} catch (error) {
console.error('Müşteri form taslağı okunamadı:', error)
}
}, [])
useEffect(() => {
const loadCountries = async () => {
setIsLoadingCountries(true)
@ -197,6 +224,48 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
void loadCountries()
}, [])
useEffect(() => {
if (Object.keys(formData).length === 0) return
localStorage.setItem(
TENANT_FORM_DRAFT_STORAGE_KEY,
JSON.stringify({
...formData,
isExisting,
foundTenantName,
}),
)
}, [formData, foundTenantName, isExisting])
useEffect(() => {
const loadLocationOptions = async () => {
if (!formData.country) return
setIsLoadingCities(true)
try {
const cityResponse = await getCities(formData.country)
setCities(cityResponse.data)
} catch (error) {
console.error('Şehirler alınırken hata:', error)
} finally {
setIsLoadingCities(false)
}
if (!formData.city) return
setIsLoadingDistricts(true)
try {
const districtResponse = await getDistricts(formData.country, formData.city)
setDistricts(districtResponse.data)
} catch (error) {
console.error('İlçeler alınırken hata:', error)
} finally {
setIsLoadingDistricts(false)
}
}
void loadLocationOptions()
}, [formData.country, formData.city])
const selectedCountry = countries.find((country) => country.name === formData.country)
const phoneMask = selectedCountry
? `+${selectedCountry.phoneCode} ${getPhoneFormat(selectedCountry)}`
@ -233,7 +302,13 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
return
}
onSubmit({ ...formData, isExisting: true } as CustomTenantDto)
onSubmit({
...formData,
phoneNumber: normalizePhoneForStorage(formData.phoneNumber, selectedCountry),
mobileNumber: normalizePhoneForStorage(formData.mobileNumber, selectedCountry),
faxNumber: normalizePhoneForStorage(formData.faxNumber, selectedCountry),
isExisting: true,
} as CustomTenantDto)
return
}
@ -254,13 +329,46 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
}
if (hasValidationError) return
onSubmit({ ...formData, isExisting: false } as CustomTenantDto)
onSubmit({
...formData,
phoneNumber: normalizePhoneForStorage(formData.phoneNumber, selectedCountry),
mobileNumber: normalizePhoneForStorage(formData.mobileNumber, selectedCountry),
faxNumber: normalizePhoneForStorage(formData.faxNumber, selectedCountry),
isExisting: false,
} as CustomTenantDto)
}
const handleInputChange = (field: keyof CustomTenantDto, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }))
}
const getDefaultNewCustomerData = (): Partial<CustomTenantDto> => {
const defaultCountry = countries.find((country) => country.name === 'Türkiye')
return defaultCountry
? {
country: defaultCountry.name,
city: '',
district: '',
phoneNumber: `+${defaultCountry.phoneCode}`,
}
: {}
}
const handleCustomerTypeChange = (nextIsExisting: boolean) => {
if (nextIsExisting === isExisting) return
setIsExisting(nextIsExisting)
setFoundTenantName('')
setExistingTenantError('')
setFormError('')
setEmailError('')
setPhoneError('')
setDistricts([])
setFormData(nextIsExisting ? {} : getDefaultNewCustomerData())
localStorage.removeItem(TENANT_DATA_STORAGE_KEY)
localStorage.removeItem(TENANT_FORM_DRAFT_STORAGE_KEY)
}
const resetExistingTenantDetails = (name: string) => {
setFoundTenantName('')
setExistingTenantError('')
@ -343,25 +451,25 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
...prev,
id: tenant.data.id,
name: tenant.data.name,
isActive: tenant.data.isActive,
organizationName: tenant.data.organizationName,
founder: tenant.data.founder,
vknTckn: tenant.data.vknTckn,
taxOffice: tenant.data.taxOffice,
address1: tenant.data.address1,
address2: tenant.data.address2,
district: tenant.data.district,
country: tenant.data.country,
city: tenant.data.city,
district: tenant.data.district,
postalCode: tenant.data.postalCode,
phoneNumber: tenant.data.phoneNumber,
mobileNumber: tenant.data.mobileNumber,
faxNumber: tenant.data.faxNumber,
address1: tenant.data.address1,
address2: tenant.data.address2,
email: tenant.data.email,
website: tenant.data.website,
menuGroup: tenant.data.menuGroup,
}))
setFoundTenantName(tenant.data.name)
if (tenant.data.name) setFoundTenantName(tenant.data.name)
} catch (error) {
setFoundTenantName('')
setExistingTenantError('Kurum bulunamadı. Lütfen organization code bilgisini kontrol edin.')
@ -384,35 +492,41 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
<div className="space-y-4">
<div className="grid grid-cols-1 gap-4">
<Button
onClick={() => setIsExisting(true)}
onClick={() => handleCustomerTypeChange(true)}
variant="plain"
className={`block h-auto w-full p-4 border-2 rounded-xl transition-all text-left select-none ${
className={`relative block h-auto w-full p-4 border-2 rounded-xl transition-all text-left select-none shadow-sm ${
isExisting === true
? 'border-blue-500 bg-blue-50 shadow-md scale-[1.02] dark:bg-blue-950/30'
: 'border-gray-200 hover:border-blue-300 dark:border-gray-700 dark:hover:border-blue-500'
? 'border-blue-600 bg-blue-100 shadow-lg ring-4 ring-blue-200 scale-[1.02] dark:border-sky-400 dark:bg-sky-950/60 dark:ring-sky-500/30'
: 'border-gray-200 bg-white hover:border-blue-400 hover:bg-blue-50/60 dark:border-gray-700 dark:bg-gray-800/70 dark:hover:border-sky-500 dark:hover:bg-sky-950/30'
}`}
>
{isExisting === true && (
<FaCheckCircle className="absolute right-4 top-4 h-5 w-5 text-blue-600 dark:text-sky-300" />
)}
<div className="font-semibold text-gray-900 dark:text-gray-100 mb-2">
{translate('::Public.products.tenantForm.existing.title')}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
<div className="text-sm text-gray-700 dark:text-gray-300">
{translate('::Public.products.tenantForm.existing.desc')}
</div>
</Button>
<Button
onClick={() => setIsExisting(false)}
onClick={() => handleCustomerTypeChange(false)}
variant="plain"
className={`block h-auto w-full p-4 border-2 rounded-xl transition-all text-left select-none ${
className={`relative block h-auto w-full p-4 border-2 rounded-xl transition-all text-left select-none shadow-sm ${
isExisting === false
? 'border-blue-500 bg-blue-50 shadow-md scale-[1.02] dark:bg-blue-950/30'
: 'border-gray-200 hover:border-blue-300 dark:border-gray-700 dark:hover:border-blue-500'
? 'border-blue-600 bg-blue-100 shadow-lg ring-4 ring-blue-200 scale-[1.02] dark:border-sky-400 dark:bg-sky-950/60 dark:ring-sky-500/30'
: 'border-gray-200 bg-white hover:border-blue-400 hover:bg-blue-50/60 dark:border-gray-700 dark:bg-gray-800/70 dark:hover:border-sky-500 dark:hover:bg-sky-950/30'
}`}
>
{isExisting === false && (
<FaCheckCircle className="absolute right-4 top-4 h-5 w-5 text-blue-600 dark:text-sky-300" />
)}
<div className="font-semibold text-gray-900 dark:text-gray-100 mb-2">
{translate('::Public.products.tenantForm.new.title')}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
<div className="text-sm text-gray-700 dark:text-gray-300">
{translate('::Public.products.tenantForm.new.desc')}
</div>
</Button>
@ -476,12 +590,6 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
{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>{formData.founder}</span>
</div>
<div className="flex items-center gap-2">
<FaBuilding className="w-4 h-4 text-gray-500" />
<span className="font-medium">
@ -490,6 +598,12 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
<span>{formData.organizationName}</span>
</div>
<div className="flex items-center gap-2">
<FaUser className="w-4 h-4 text-gray-500" />
<span className="font-medium">{translate('::LoginPanel.Profil')}</span>
<span>{formData.founder}</span>
</div>
<div className="flex items-center gap-2">
<FaEnvelope className="w-4 h-4 text-gray-500" />
<span className="font-medium">

View file

@ -21,28 +21,24 @@ export interface TenantDto extends ExtensibleEntityDto<string> {
}
export interface CustomTenantDto extends TenantDto {
isExisting: boolean
reference?: string
id?: string
name: string
isActive: boolean
organizationName: string
founder?: string
vknTckn: number
taxOffice: string
address1: string
address2: string
district: string
country: string
city: string
district: string
address1: string
address2: string
postalCode: string
phoneNumber: string
mobileNumber: string
phoneNumber: string
faxNumber: string
email: string
website: string
menuGroup?: string
reference?: string
}
export interface MultiTenancyInfoDto {

View file

@ -9,6 +9,7 @@ export interface Product {
category: string
monthlyPrice?: number
yearlyPrice?: number
vatRate: number
unit: BillingCycle
isQuantityBased: boolean
imageUrl?: string
@ -22,6 +23,7 @@ export interface ProductDto {
category: string
monthlyPrice?: number
yearlyPrice?: number
vatRate: number
unit: string
isQuantityBased: boolean
imageUrl: string
@ -31,6 +33,8 @@ export interface BasketItem {
product: ProductDto
quantity: number
billingCycle: BillingCycle
vatRate: number
vatAmount: number
totalPrice: number
}
@ -60,14 +64,21 @@ export interface InstallmentOptionDto {
export interface OrderDto {
tenant: CustomTenantDto
items: BasketItem[]
isExisting: boolean
billingCycle: BillingCycle
period: number
licenseStartTime?: string
licenseStopTime?: string
licenseEndTime?: string
subtotal: number
vatTotal: number
commission: number
total: number
paymentMethodId: string
installment: number
paymentData: Record<string, unknown>
}

View file

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

View file

@ -2,6 +2,8 @@ import { BillingCycle, BasketItem, ProductDto } from "@/proxy/order/models";
export interface CartState {
items: BasketItem[];
subtotal: number;
vatTotal: number;
total: number;
globalBillingCycle: BillingCycle;
globalPeriod: number;
@ -9,6 +11,8 @@ export interface CartState {
export const initialCartState: CartState = {
items: [],
subtotal: 0,
vatTotal: 0,
total: 0,
globalBillingCycle: 'yearly',
globalPeriod: 1
@ -24,7 +28,43 @@ export const calculateItemPrice = (
period: number = 1
): number => {
const price = billingCycle === 'monthly' ? product.monthlyPrice! : product.yearlyPrice!;
return price * quantity * period;
const vatRate = product.vatRate ?? 0.2;
return roundMoney(price * quantity * period * (1 + vatRate));
};
const roundMoney = (value: number): number => Math.round(value * 100) / 100;
const calculateItemVatAmount = (
totalPrice: number,
vatRate: number = 0.2,
): number => (vatRate > 0 ? roundMoney(totalPrice - totalPrice / (1 + vatRate)) : 0);
const withItemVat = (
product: ProductDto,
quantity: number,
billingCycle: BillingCycle,
period: number,
): BasketItem => {
const vatRate = product.vatRate ?? 0.2;
const totalPrice = calculateItemPrice(product, quantity, billingCycle, period);
return {
product,
quantity,
billingCycle,
vatRate,
vatAmount: calculateItemVatAmount(totalPrice, vatRate),
totalPrice,
};
};
const calculateCartTotals = (items: BasketItem[]) => {
const total = roundMoney(items.reduce((sum, item) => sum + item.totalPrice, 0));
const vatTotal = roundMoney(items.reduce((sum, item) => sum + (item.vatAmount ?? 0), 0));
return {
subtotal: roundMoney(total - vatTotal),
vatTotal,
total,
};
};
export const addItemToCart = (
@ -47,28 +87,28 @@ export const addItemToCart = (
if (existingItemIndex > -1) {
newItems = [...cartState.items];
newItems[existingItemIndex].quantity += quantity;
newItems[existingItemIndex].totalPrice = calculateItemPrice(
newItems[existingItemIndex] = withItemVat(
product,
newItems[existingItemIndex].quantity,
billingCycle,
cartState.globalPeriod
cartState.globalPeriod,
);
} else {
const actualQuantity = !product.isQuantityBased ? 1 : quantity;
const totalPrice = calculateItemPrice(product, actualQuantity, billingCycle, cartState.globalPeriod);
newItems = [...cartState.items, { product, quantity: actualQuantity, billingCycle, totalPrice }];
newItems = [
...cartState.items,
withItemVat(product, actualQuantity, billingCycle, cartState.globalPeriod),
];
}
const total = newItems.reduce((sum, item) => sum + item.totalPrice, 0);
const updatedState = { ...cartState, items: newItems, total };
const updatedState = { ...cartState, items: newItems, ...calculateCartTotals(newItems) };
saveCartToLocalStorage(updatedState);
return updatedState;
};
export const removeItemFromCart = (cartState: CartState, id: string): CartState => {
const newItems = cartState.items.filter(item => item.product.id !== id);
const total = newItems.reduce((sum, item) => sum + item.totalPrice, 0);
const updatedState = { ...cartState, items: newItems, total };
const updatedState = { ...cartState, items: newItems, ...calculateCartTotals(newItems) };
saveCartToLocalStorage(updatedState);
return updatedState;
};
@ -79,17 +119,12 @@ export const updateCartItemQuantity = (cartState: CartState, id: string, quantit
if (quantity <= 0) return null;
if (!item.product.isQuantityBased) return item;
return {
...item,
quantity,
totalPrice: calculateItemPrice(item.product, quantity, item.billingCycle, cartState.globalPeriod)
};
return withItemVat(item.product, quantity, item.billingCycle, cartState.globalPeriod);
}
return item;
}).filter(Boolean) as BasketItem[];
const total = newItems.reduce((sum, item) => sum + item.totalPrice, 0);
const updatedState = { ...cartState, items: newItems, total };
const updatedState = { ...cartState, items: newItems, ...calculateCartTotals(newItems) };
saveCartToLocalStorage(updatedState);
return updatedState;
};
@ -100,24 +135,19 @@ export const clearCart = (): CartState => {
};
export const setCartGlobalBillingCycle = (cartState: CartState, cycle: BillingCycle): CartState => {
const newItems = cartState.items.map(item => ({
...item,
billingCycle: cycle,
totalPrice: calculateItemPrice(item.product, item.quantity, cycle, cartState.globalPeriod)
}));
const total = newItems.reduce((sum, item) => sum + item.totalPrice, 0);
const updatedState = { ...cartState, globalBillingCycle: cycle, items: newItems, total };
const newItems = cartState.items.map(item =>
withItemVat(item.product, item.quantity, cycle, cartState.globalPeriod)
);
const updatedState = { ...cartState, globalBillingCycle: cycle, items: newItems, ...calculateCartTotals(newItems) };
saveCartToLocalStorage(updatedState);
return updatedState;
};
export const setCartGlobalPeriod = (cartState: CartState, period: number): CartState => {
const newItems = cartState.items.map(item => ({
...item,
totalPrice: calculateItemPrice(item.product, item.quantity, item.billingCycle, period)
}));
const total = newItems.reduce((sum, item) => sum + item.totalPrice, 0);
const updatedState = { ...cartState, globalPeriod: period, items: newItems, total };
const newItems = cartState.items.map(item =>
withItemVat(item.product, item.quantity, item.billingCycle, period)
);
const updatedState = { ...cartState, globalPeriod: period, items: newItems, ...calculateCartTotals(newItems) };
saveCartToLocalStorage(updatedState);
return updatedState;
};
@ -142,5 +172,17 @@ const saveCartToLocalStorage = (cartState: CartState) => {
// Utility: Load from localStorage
export const loadCartFromLocalStorage = (): CartState => {
const saved = localStorage.getItem(CART_STORAGE_KEY);
return saved ? JSON.parse(saved) : initialCartState;
if (!saved) return initialCartState;
const parsed = JSON.parse(saved) as CartState;
const period = parsed.globalPeriod || 1;
const items = (parsed.items || []).map(item =>
withItemVat(item.product, item.quantity, item.billingCycle, period)
);
return {
...initialCartState,
...parsed,
items,
...calculateCartTotals(items),
};
};

View file

@ -6,7 +6,7 @@ import { ROUTES_ENUM } from '@/routes/route.constant'
import {
CartState,
clearCart,
initialCartState,
loadCartFromLocalStorage,
removeItemFromCart,
updateCartItemQuantity,
} from '@/utils/cartUtils'
@ -15,14 +15,15 @@ import React, { useState, useEffect } from 'react'
import { Helmet } from 'react-helmet'
import { useNavigate } from 'react-router-dom'
const TENANT_DATA_STORAGE_KEY = 'tenantData'
const Checkout: React.FC = () => {
const navigate = useNavigate()
const [isCartOpen, setIsCartOpen] = useState(false)
const { translate } = useLocalization()
const [cartState, setCartState] = useState<CartState>(() => {
const savedCart = localStorage.getItem('cartState')
return savedCart ? JSON.parse(savedCart) : initialCartState
return loadCartFromLocalStorage()
})
useEffect(() => {
@ -30,7 +31,7 @@ const Checkout: React.FC = () => {
}, [cartState])
const handleTenantSubmit = (tenantData: CustomTenantDto) => {
localStorage.setItem('tenantData', JSON.stringify(tenantData))
localStorage.setItem(TENANT_DATA_STORAGE_KEY, JSON.stringify(tenantData))
navigate(ROUTES_ENUM.public.payment)
}

View file

@ -10,6 +10,7 @@ import {
CartState,
clearCart,
initialCartState,
loadCartFromLocalStorage,
removeItemFromCart,
updateCartItemQuantity,
} from '@/utils/cartUtils'
@ -18,6 +19,9 @@ import React, { useState, useEffect } from 'react'
import { Helmet } from 'react-helmet'
import { useNavigate } from 'react-router-dom'
const TENANT_DATA_STORAGE_KEY = 'tenantData'
const TENANT_FORM_DRAFT_STORAGE_KEY = 'tenantFormDraft'
const Payment: React.FC = () => {
const navigate = useNavigate()
const [tenant, setTenant] = useState<CustomTenantDto | null>(null)
@ -26,8 +30,7 @@ const Payment: React.FC = () => {
const { translate } = useLocalization()
useEffect(() => {
const tenantData = localStorage.getItem('tenantData') // ✅ güncellendi
const savedCartState = localStorage.getItem('cartState')
const tenantData = localStorage.getItem(TENANT_DATA_STORAGE_KEY) // ✅ güncellendi
if (tenantData) {
setTenant(JSON.parse(tenantData))
@ -35,9 +38,7 @@ const Payment: React.FC = () => {
navigate(ROUTES_ENUM.public.checkout)
}
if (savedCartState) {
setCartState(JSON.parse(savedCartState))
}
setCartState(loadCartFromLocalStorage())
}, [navigate])
// Save cart state to localStorage whenever it changes
@ -62,35 +63,25 @@ 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 licenseStartTime = new Date(paymentData.licenseStartTime as string)
const licenseEndTime = new Date(paymentData.licenseEndTime as string)
const orderData = {
tenant,
items: cartState.items,
isExisting: paymentData.isExisting,
billingCycle,
period,
licenseStartTime: licenseStartTime.toISOString(),
licenseStopTime: licenseStopTime.toISOString(),
subtotal: cartState.total,
licenseEndTime: licenseEndTime.toISOString(),
subtotal: cartState.subtotal,
vatTotal: cartState.vatTotal,
commission: paymentData.commission,
total: paymentData.total,
paymentMethodId: paymentData.paymentMethodId,
@ -109,8 +100,16 @@ const Payment: React.FC = () => {
if (result.success && result.orderId) {
localStorage.setItem('orderId', result.orderId) // ✅ güncellendi
localStorage.setItem(
'orderSuccessData',
JSON.stringify({
orderId: result.orderId,
...orderData,
}),
)
setCartState(clearCart())
localStorage.removeItem('tenantData') // ✅ temizlik
localStorage.removeItem(TENANT_DATA_STORAGE_KEY) // ✅ temizlik
localStorage.removeItem(TENANT_FORM_DRAFT_STORAGE_KEY)
localStorage.removeItem('cartState') // ✅ temizlik
navigate(ROUTES_ENUM.public.success)
} else {

View file

@ -7,7 +7,7 @@ import { ROUTES_ENUM } from '@/routes/route.constant'
import {
CartState,
clearCart,
initialCartState,
loadCartFromLocalStorage,
removeItemFromCart,
setCartGlobalBillingCycle,
setCartGlobalPeriod,
@ -24,9 +24,7 @@ const Products: React.FC = () => {
const { translate } = useLocalization()
const [cartState, setCartState] = useState<CartState>(() => {
// Load cart state from localStorage if available
const savedCart = localStorage.getItem('cartState')
return savedCart ? JSON.parse(savedCart) : initialCartState
return loadCartFromLocalStorage()
})
const navigate = useNavigate()

View file

@ -1,4 +1,4 @@
import { OrderSuccess } from '@/components/orders/OrderSuccess'
import { OrderSuccess, OrderSuccessData } from '@/components/orders/OrderSuccess'
import { Loading } from '@/components/shared'
import { APP_NAME } from '@/constants/app.constant'
import { ROUTES_ENUM } from '@/routes/route.constant'
@ -10,15 +10,21 @@ import { useNavigate } from 'react-router-dom'
const Success: React.FC = () => {
const navigate = useNavigate()
const [orderId, setOrderId] = useState<string>('')
const [order, setOrder] = useState<OrderSuccessData | null>(null)
const { translate } = useLocalization()
useEffect(() => {
// Get order ID from local storage
const orderIdData = localStorage.getItem('orderId')
const orderSuccessData = localStorage.getItem('orderSuccessData')
if (orderIdData) {
setOrderId(orderIdData)
if (orderSuccessData) {
setOrder(JSON.parse(orderSuccessData))
}
// Clear order ID from local storage
localStorage.removeItem('orderId')
localStorage.removeItem('orderSuccessData')
} else {
navigate(ROUTES_ENUM.public.home)
}
@ -56,7 +62,7 @@ const Success: React.FC = () => {
</div>
<div className="container mx-auto px-4 pt-12">
<OrderSuccess orderId={orderId} onBackToShop={handleBackToShop} />
<OrderSuccess orderId={orderId} order={order} onBackToShop={handleBackToShop} />
</div>
</div>
)