Dil Desteği ve Seederlar

This commit is contained in:
Sedat Öztürk 2025-08-17 22:07:04 +03:00
parent dd7458093c
commit 284d0d777c
16 changed files with 1252 additions and 334 deletions

View file

@ -12186,8 +12186,8 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
ItemType = "group", ItemType = "group",
Items = Items =
[ [
new EditingFormItemDto { Order = 1, DataField = "Name", ColSpan = 2, IsRequired = true, EditorType2 = EditorTypes.dxTextBox }, new EditingFormItemDto { Order = 1, DataField = "Name", ColSpan = 2, IsRequired = true, EditorType2 = EditorTypes.dxSelectBox },
new EditingFormItemDto { Order = 2, DataField = "Description", ColSpan = 2, EditorType2 = EditorTypes.dxTextBox }, new EditingFormItemDto { Order = 2, DataField = "Description", ColSpan = 2, EditorType2 = EditorTypes.dxSelectBox },
new EditingFormItemDto { Order = 3, DataField = "Category", ColSpan = 2, EditorType2 = EditorTypes.dxSelectBox }, new EditingFormItemDto { Order = 3, DataField = "Category", ColSpan = 2, EditorType2 = EditorTypes.dxSelectBox },
new EditingFormItemDto { Order = 4, DataField = "MonthlyPrice", ColSpan = 2, EditorType2 = EditorTypes.dxNumberBox }, new EditingFormItemDto { Order = 4, DataField = "MonthlyPrice", ColSpan = 2, EditorType2 = EditorTypes.dxNumberBox },
new EditingFormItemDto { Order = 5, DataField = "YearlyPrice", ColSpan = 2, EditorType2 = EditorTypes.dxNumberBox }, new EditingFormItemDto { Order = 5, DataField = "YearlyPrice", ColSpan = 2, EditorType2 = EditorTypes.dxNumberBox },
@ -12262,7 +12262,14 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
E = true, E = true,
I = true, I = true,
Deny = false Deny = false
}) }),
LookupJson = JsonSerializer.Serialize(new LookupDto
{
DataSourceType = UiLookupDataSourceTypeEnum.Query,
DisplayExpr = "Name",
ValueExpr = "Key",
LookupQuery = lookupQueryLanguageKeyValues
}),
}, },
new() new()
{ {
@ -12284,7 +12291,14 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
E = true, E = true,
I = true, I = true,
Deny = false Deny = false
}) }),
LookupJson = JsonSerializer.Serialize(new LookupDto
{
DataSourceType = UiLookupDataSourceTypeEnum.Query,
DisplayExpr = "Name",
ValueExpr = "Key",
LookupQuery = lookupQueryLanguageKeyValues
}),
}, },
new() new()
{ {
@ -12304,9 +12318,9 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
DisplayExpr = "name", DisplayExpr = "name",
ValueExpr = "key", ValueExpr = "key",
LookupQuery = JsonSerializer.Serialize(new LookupDataDto[] { LookupQuery = JsonSerializer.Serialize(new LookupDataDto[] {
new () { Key="Üyelik",Name="Üyelik" }, new () { Key="Public.products.categories.Üyelik",Name="Üyelik" },
new () { Key="Lisans",Name="Lisans" }, new () { Key="Public.products.categories.Lisans",Name="Lisans" },
new () { Key="Ek Hizmetler",Name="Ek Hizmetler" }, new () { Key="Public.products.categories.Ek Hizmetler",Name="Ek Hizmetler" },
}), }),
}), }),
ValidationRuleJson = JsonSerializer.Serialize(new[] ValidationRuleJson = JsonSerializer.Serialize(new[]

View file

@ -7189,6 +7189,600 @@
"tr": "Demo Talep Edin", "tr": "Demo Talep Edin",
"en": "Request a Demo" "en": "Request a Demo"
}, },
{
"resourceName": "Platform",
"key": "Public.products.billingcycle",
"tr": "Faturalama Döngüsü",
"en": "Billing Cycle"
},
{
"resourceName": "Platform",
"key": "Public.products.period",
"tr": "Periyot",
"en": "Period"
},
{
"resourceName": "Platform",
"key": "Public.products.billingcycle.yearly",
"tr": "Yıllık",
"en": "Yearly"
},
{
"resourceName": "Platform",
"key": "Public.products.billingcycle.monthly",
"tr": "Aylık",
"en": "Monthly"
},
{
"resourceName": "Platform",
"key": "Public.products.billingcycle.year",
"tr": "Yıl",
"en": "Year"
},
{
"resourceName": "Platform",
"key": "Public.products.billingcycle.month",
"tr": "Ay",
"en": "Month"
},
{
"resourceName": "Platform",
"key": "Public.products.categories",
"tr": "Kategoriler",
"en": "Categories"
},
{
"resourceName": "Platform",
"key": "Public.products.categories.all",
"tr": "Tüm Ürünler",
"en": "All Products"
},
{
"resourceName": "Platform",
"key": "Public.products.categories.Üyelik",
"tr": "Üyelik",
"en": "Membership"
},
{
"resourceName": "Platform",
"key": "Public.products.categories.Lisans",
"tr": "Lisans",
"en": "License"
},
{
"resourceName": "Platform",
"key": "Public.products.categories.Ek Hizmetler",
"tr": "Ek Hizmetler",
"en": "Additional Services"
},
{
"resourceName": "Platform",
"key": "Public.order.success.title",
"tr": "Siparişiniz Başarıyla Alındı!",
"en": "Your order has been received successfully!"
},
{
"resourceName": "Platform",
"key": "Public.order.success.number",
"tr": "Sipariş numaranız:",
"en": "Your order number:"
},
{
"resourceName": "Platform",
"key": "Public.order.success.nextSteps",
"tr": "Sonraki Adımlar",
"en": "Next Steps"
},
{
"resourceName": "Platform",
"key": "Public.order.success.step1",
"tr": "Siparişiniz 24 saat içinde işleme alınacaktır",
"en": "Your order will be processed within 24 hours"
},
{
"resourceName": "Platform",
"key": "Public.order.success.step2",
"tr": "Lisans bilgileri e-posta adresinize gönderilecektir",
"en": "License details will be sent to your email address"
},
{
"resourceName": "Platform",
"key": "Public.order.success.step3",
"tr": "Kurulum desteği için ekibimizle iletişime geçebilirsiniz",
"en": "You can contact our team for installation support"
},
{
"resourceName": "Platform",
"key": "Public.order.success.backHome",
"tr": "Ana Sayfa'ya Dön",
"en": "Back to Home"
},
{
"resourceName": "Platform",
"key": "Public.payment.loading",
"tr": "Yükleniyor...",
"en": "Loading..."
},
{
"resourceName": "Platform",
"key": "Public.payment.customerInfo",
"tr": "Müşteri Bilgileri",
"en": "Customer Information"
},
{
"resourceName": "Platform",
"key": "Public.payment.customer.code",
"tr": "Kurum Kodu:",
"en": "Institution Code:"
},
{
"resourceName": "Platform",
"key": "Public.payment.customer.founder",
"tr": "Kurucu:",
"en": "Founder:"
},
{
"resourceName": "Platform",
"key": "Public.payment.customer.company",
"tr": "Şirket:",
"en": "Company:"
},
{
"resourceName": "Platform",
"key": "Public.payment.customer.email",
"tr": "E-posta:",
"en": "Email:"
},
{
"resourceName": "Platform",
"key": "Public.payment.customer.phone",
"tr": "Telefon:",
"en": "Phone:"
},
{
"resourceName": "Platform",
"key": "Public.payment.customer.address",
"tr": "Adres:",
"en": "Address:"
},
{
"resourceName": "Platform",
"key": "Public.payment.customer.country",
"tr": "Ülke:",
"en": "Country:"
},
{
"resourceName": "Platform",
"key": "Public.payment.customer.city",
"tr": "Şehir:",
"en": "City:"
},
{
"resourceName": "Platform",
"key": "Public.payment.customer.district",
"tr": "İlçe:",
"en": "District:"
},
{
"resourceName": "Platform",
"key": "Public.payment.customer.postalCode",
"tr": "Posta Kodu:",
"en": "Postal Code:"
},
{
"resourceName": "Platform",
"key": "Public.payment.customer.taxOffice",
"tr": "Vergi Dairesi:",
"en": "Tax Office:"
},
{
"resourceName": "Platform",
"key": "Public.payment.customer.taxNumber",
"tr": "Vergi No:",
"en": "Tax Number:"
},
{
"resourceName": "Platform",
"key": "Public.payment.customer.reference",
"tr": "Referans:",
"en": "Reference:"
},
{
"resourceName": "Platform",
"key": "Public.payment.method.title",
"tr": "Ödeme Yöntemi",
"en": "Payment Method"
},
{
"resourceName": "Platform",
"key": "Public.payment.method.installmentsAvailable",
"tr": "Taksit seçenekleri mevcut",
"en": "Installment options available"
},
{
"resourceName": "Platform",
"key": "Public.payment.method.noCommission",
"tr": "Komisyon yok",
"en": "No commission"
},
{
"resourceName": "Platform",
"key": "Public.payment.installments.title",
"tr": "Taksit Seçenekleri",
"en": "Installment Options"
},
{
"resourceName": "Platform",
"key": "Public.payment.installments.commission",
"tr": "Komisyon:",
"en": "Commission:"
},
{
"resourceName": "Platform",
"key": "Public.payment.installments.monthly",
"tr": "x aylık",
"en": "x monthly"
},
{
"resourceName": "Platform",
"key": "Public.payment.installments.single",
"tr": "Tek ödeme",
"en": "Single Payment"
},
{
"resourceName": "Platform",
"key": "Public.payment.card.title",
"tr": "Kart Bilgileri",
"en": "Card Information"
},
{
"resourceName": "Platform",
"key": "Public.payment.card.name",
"tr": "Kart Üzerindeki İsim",
"en": "Name on Card"
},
{
"resourceName": "Platform",
"key": "Public.payment.card.number",
"tr": "Kart Numarası",
"en": "Card Number"
},
{
"resourceName": "Platform",
"key": "Public.payment.summary.title",
"tr": "Sipariş Özeti",
"en": "Order Summary"
},
{
"resourceName": "Platform",
"key": "Public.payment.summary.subtotal",
"tr": "Ara Toplam:",
"en": "Subtotal:"
},
{
"resourceName": "Platform",
"key": "Public.payment.summary.commission",
"tr": "Komisyon:",
"en": "Commission:"
},
{
"resourceName": "Platform",
"key": "Public.payment.summary.monthlyInstallment",
"tr": "Aylık Taksit:",
"en": "Monthly Installment:"
},
{
"resourceName": "Platform",
"key": "Public.payment.summary.total",
"tr": "Toplam:",
"en": "Total:"
},
{
"resourceName": "Platform",
"key": "Public.payment.buttons.back",
"tr": "Geri",
"en": "Back"
},
{
"resourceName": "Platform",
"key": "Public.payment.buttons.completeOrder",
"tr": "Siparişi Tamamla",
"en": "Complete Order"
},
{
"resourceName": "Platform",
"key": "Public.payment.buttons.pay",
"tr": "Ödeme Yap",
"en": "Pay"
},
{
"resourceName": "Platform",
"key": "Public.products.tenantForm.customerInfo",
"tr": "Müşteri Bilgileri",
"en": "Customer Information"
},
{
"resourceName": "Platform",
"key": "Public.products.tenantForm.existing.title",
"tr": "Mevcut Müşteri",
"en": "Existing Customer"
},
{
"resourceName": "Platform",
"key": "Public.products.tenantForm.existing.desc",
"tr": "Kurum kodunuz ile giriş yapın",
"en": "Log in with your organization code"
},
{
"resourceName": "Platform",
"key": "Public.products.tenantForm.new.title",
"tr": "Yeni Müşteri",
"en": "New Customer"
},
{
"resourceName": "Platform",
"key": "Public.products.tenantForm.new.desc",
"tr": "Kayıt olun ve hemen başlayın",
"en": "Register and get started immediately"
},
{
"resourceName": "Platform",
"key": "Public.products.tenantForm.orgCode",
"tr": "Kurum Kodu *",
"en": "Organization Code *"
},
{
"resourceName": "Platform",
"key": "Public.products.tenantForm.orgName",
"tr": "Şirket Adı *",
"en": "Company Name *"
},
{
"resourceName": "Platform",
"key": "Public.products.tenantForm.searchOrg",
"tr": "Kurumu Bul",
"en": "Find Organization"
},
{
"resourceName": "Platform",
"key": "Public.products.tenantForm.founder",
"tr": "Kurucu:",
"en": "Founder:"
},
{
"resourceName": "Platform",
"key": "Public.products.tenantForm.company",
"tr": "Şirket:",
"en": "Company:"
},
{
"resourceName": "Platform",
"key": "Public.products.tenantForm.email",
"tr": "E-posta:",
"en": "Email:"
},
{
"resourceName": "Platform",
"key": "Public.products.tenantForm.phone",
"tr": "Telefon *",
"en": "Phone *"
},
{
"resourceName": "Platform",
"key": "Public.products.tenantForm.mobile",
"tr": "Mobile:",
"en": "Mobile:"
},
{
"resourceName": "Platform",
"key": "Public.products.tenantForm.address",
"tr": "Adres *",
"en": "Address *"
},
{
"resourceName": "Platform",
"key": "Public.products.tenantForm.country",
"tr": "Ülke",
"en": "Country"
},
{
"resourceName": "Platform",
"key": "Public.products.tenantForm.city",
"tr": "Şehir *",
"en": "City *"
},
{
"resourceName": "Platform",
"key": "Public.products.tenantForm.district",
"tr": "İlçe *",
"en": "District *"
},
{
"resourceName": "Platform",
"key": "Public.products.tenantForm.postalCode",
"tr": "Posta Kodu *",
"en": "Postal Code *"
},
{
"resourceName": "Platform",
"key": "Public.products.tenantForm.taxOffice",
"tr": "Vergi Dairesi *",
"en": "Tax Office *"
},
{
"resourceName": "Platform",
"key": "Public.products.tenantForm.taxNumber",
"tr": "Vergi No *",
"en": "Tax Number *"
},
{
"resourceName": "Platform",
"key": "Public.products.tenantForm.reference",
"tr": "Referans",
"en": "Reference"
},
{
"resourceName": "Platform",
"key": "Public.products.tenantForm.buttons.back",
"tr": "Geri",
"en": "Back"
},
{
"resourceName": "Platform",
"key": "Public.products.tenantForm.buttons.continue",
"tr": "Devam Et",
"en": "Continue"
},
{
"resourceName": "Platform",
"key": "Public.products.search.title",
"tr": "Arama",
"en": "Search"
},
{
"resourceName": "Platform",
"key": "Public.products.search.placeholder",
"tr": "Ürün ara...",
"en": "Search products..."
},
{
"resourceName": "Platform",
"key": "Public.products.empty.title",
"tr": "Ürün bulunamadı",
"en": "No products found"
},
{
"resourceName": "Platform",
"key": "Public.products.empty.description",
"tr": "Arama kriterlerinizi değiştirmeyi deneyin.",
"en": "Try changing your search criteria."
},
{
"resourceName": "Platform",
"key": "Public.products.loading",
"tr": "Yükleniyor...",
"en": "Loading..."
},
{
"resourceName": "Platform",
"key": "Public.cart.title",
"tr": "Sepetim",
"en": "My Cart"
},
{
"resourceName": "Platform",
"key": "Public.cart.clearConfirm",
"tr": "Sepetteki tüm ürünleri silmek istediğinizden emin misiniz?",
"en": "Are you sure you want to remove all items from the cart?"
},
{
"resourceName": "Platform",
"key": "Public.cart.clear",
"tr": "Sepeti Temizle",
"en": "Clear Cart"
},
{
"resourceName": "Platform",
"key": "Public.cart.empty.title",
"tr": "Sepetiniz boş",
"en": "Your cart is empty"
},
{
"resourceName": "Platform",
"key": "Public.cart.empty.subtitle",
"tr": "Ürün eklemek için katalogdan seçim yapın",
"en": "Add products from the catalog"
},
{
"resourceName": "Platform",
"key": "Public.cart.monthly",
"tr": "Aylık",
"en": "Monthly"
},
{
"resourceName": "Platform",
"key": "Public.cart.yearly",
"tr": "Yıllık",
"en": "Yearly"
},
{
"resourceName": "Platform",
"key": "Public.cart.month",
"tr": "Ay",
"en": "Month"
},
{
"resourceName": "Platform",
"key": "Public.cart.year",
"tr": "Yıl",
"en": "Year"
},
{
"resourceName": "Platform",
"key": "Public.cart.total",
"tr": "Toplam:",
"en": "Total:"
},
{
"resourceName": "Platform",
"key": "Public.cart.checkout",
"tr": "Ödemeye Geç",
"en": "Proceed to Checkout"
},
{
"resourceName": "Platform",
"key": "Public.products.quantity",
"tr": "Adet:",
"en": "Quantity:"
},
{
"resourceName": "Platform",
"key": "Public.products.savings.yearly",
"tr": "Yıllık ile %{{percent}} tasarruf",
"en": "Save {{percent}}% with yearly"
},
{
"resourceName": "Platform",
"key": "Public.products.price.perMonthSuffix",
"tr": "/ Ay",
"en": "/ Month"
},
{
"resourceName": "Platform",
"key": "Public.products.price.perYearSuffix",
"tr": "/ Yıl",
"en": "/ Year"
},
{
"resourceName": "Platform",
"key": "Public.products.period.month",
"tr": "Ay",
"en": "Month"
},
{
"resourceName": "Platform",
"key": "Public.products.period.year",
"tr": "Yıl",
"en": "Year"
},
{
"resourceName": "Platform",
"key": "Public.products.total",
"tr": "Toplam:",
"en": "Total:"
},
{
"resourceName": "Platform",
"key": "Public.products.addToCart",
"tr": "Sepete Ekle",
"en": "Add to Cart"
},
{
"resourceName": "Platform",
"key": "Public.products.inCart",
"tr": "Sepette Mevcut",
"en": "Already in Cart"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "Public.products.cta.description", "key": "Public.products.cta.description",
@ -9408,7 +10002,177 @@
"key": "App.DeveloperKit.ComponentEditor.Loading", "key": "App.DeveloperKit.ComponentEditor.Loading",
"en": "Loading component...", "en": "Loading component...",
"tr": "Bileşen yükleniyor..." "tr": "Bileşen yükleniyor..."
} },
{
"resourceName": "Platform",
"key": "Public.products.remoteBranchTraining",
"tr": "Uzaktan Şube Eğitimi (5 Saat)",
"en": "Remote Branch Training (5 Hours)"
},
{
"resourceName": "Platform",
"key": "Public.products.remoteBranchTraining.desc",
"tr": "Sistem kullanımı sırasında kullanıcı hataları ortaya çıkabilir, Sözsoft kullanıcı hatalarının giderilmesi yönünde destek hizmeti sunmaktadır.",
"en": "During system usage, user errors may occur. Sözsoft provides support services to resolve user errors."
},
{
"resourceName": "Platform",
"key": "Public.products.branchHosting",
"tr": "Şube Barındırma Hizmeti",
"en": "Branch Hosting Service"
},
{
"resourceName": "Platform",
"key": "Public.products.branchHosting.desc",
"tr": "Şube veya şubelerinize ait tüm bilgilerinizin veya resimlerinizin barındırıldığı alan kiralama hizmetidir.",
"en": "A hosting service where all your branch-related information or images are stored."
},
{
"resourceName": "Platform",
"key": "Public.products.sms100k",
"tr": "100,000 SMS SOFTWARE",
"en": "100,000 SMS SOFTWARE"
},
{
"resourceName": "Platform",
"key": "Public.products.sms100k.desc",
"tr": "http://www.smsyukle.com adresinden yüklenebilir. İhtiyacınız olan SMS adedini belirleyerek, SMS satın almak için lütfen SMS yükle linkine tıklayın veya bize ulaşın.",
"en": "Available at http://www.smsyukle.com. Select the number of SMS you need and click the SMS load link or contact us to purchase."
},
{
"resourceName": "Platform",
"key": "Public.products.teacherLicense",
"tr": "Öğretmen Lisansı",
"en": "Teacher License"
},
{
"resourceName": "Platform",
"key": "Public.products.teacherLicense.desc",
"tr": "Şube/şubelerinizde sistemin kaç öğretmen tarafından kullandırılacağı kararına bağlı olarak belirlenir. Öğretmen lisansları aylık/yıllık kullanım olarak sunulmaktadır.",
"en": "Determined by how many teachers in your branch/branches will use the system. Teacher licenses are offered monthly/annually."
},
{
"resourceName": "Platform",
"key": "Public.products.extraHourlyService",
"tr": "Saatlik Ek Hizmet",
"en": "Extra Hourly Service"
},
{
"resourceName": "Platform",
"key": "Public.products.extraHourlyService.desc",
"tr": "Şubelerin standart hizmetlerin dışında istedikleri özel çalışmaları için belirlenen saatlik hizmet bedelidir.",
"en": "Hourly service fee for special work requested outside the standard services of branches."
},
{
"resourceName": "Platform",
"key": "Public.products.sms50k",
"tr": "50,000 SMS SOFTWARE",
"en": "50,000 SMS SOFTWARE"
},
{
"resourceName": "Platform",
"key": "Public.products.sms50k.desc",
"tr": "http://www.smsyukle.com adresinden yüklenebilir. İhtiyacınız olan SMS adedini belirleyerek, SMS satın almak için lütfen SMS yükle linkine tıklayın veya bize ulaşın.",
"en": "Available at http://www.smsyukle.com. Select the number of SMS you need and click the SMS load link or contact us to purchase."
},
{
"resourceName": "Platform",
"key": "Public.products.sms25k",
"tr": "25,000 SMS SOFTWARE",
"en": "25,000 SMS SOFTWARE"
},
{
"resourceName": "Platform",
"key": "Public.products.sms25k.desc",
"tr": "http://www.smsyukle.com adresinden yüklenebilir. İhtiyacınız olan SMS adedini belirleyerek, SMS satın almak için lütfen SMS yükle linkine tıklayın veya bize ulaşın.",
"en": "Available at http://www.smsyukle.com. Select the number of SMS you need and click the SMS load link or contact us to purchase."
},
{
"resourceName": "Platform",
"key": "Public.products.sms10k",
"tr": "10,000 SMS SOFTWARE",
"en": "10,000 SMS SOFTWARE"
},
{
"resourceName": "Platform",
"key": "Public.products.sms10k.desc",
"tr": "http://www.smsyukle.com adresinden yüklenebilir. İhtiyacınız olan SMS adedini belirleyerek, SMS satın almak için lütfen SMS yükle linkine tıklayın veya bize ulaşın.",
"en": "Available at http://www.smsyukle.com. Select the number of SMS you need and click the SMS load link or contact us to purchase."
},
{
"resourceName": "Platform",
"key": "Public.products.sms5k",
"tr": "5,000 SMS SOFTWARE",
"en": "5,000 SMS SOFTWARE"
},
{
"resourceName": "Platform",
"key": "Public.products.sms5k.desc",
"tr": "http://www.smsyukle.com adresinden yüklenebilir. İhtiyacınız olan SMS adedini belirleyerek, SMS satın almak için lütfen SMS yükle linkine tıklayın veya bize ulaşın.",
"en": "Available at http://www.smsyukle.com. Select the number of SMS you need and click the SMS load link or contact us to purchase."
},
{
"resourceName": "Platform",
"key": "Public.products.smsBlocking",
"tr": "SMS Engelleme Hizmeti",
"en": "SMS Blocking Service"
},
{
"resourceName": "Platform",
"key": "Public.products.smsBlocking.desc",
"tr": "Şube/şubelerinizden gönderilen SMS'lerin kursiyerler tarafından engelleme hizmetidir.",
"en": "A service that allows recipients to block SMS messages sent from your branch/branches."
},
{
"resourceName": "Platform",
"key": "Public.products.backupService",
"tr": "Veri Yedekleme Hizmeti",
"en": "Data Backup Service"
},
{
"resourceName": "Platform",
"key": "Public.products.backupService.desc",
"tr": "Bilgileriniz bizler için de önemlidir. Bu doğrultuda aldığımız yüksek güvenlik önlemlerinin yanısıra BACK-UP/ yedekleme hizmeti sunmaktayız.",
"en": "Your data is also important to us. In addition to our high security measures, we provide a BACK-UP/backup service."
},
{
"resourceName": "Platform",
"key": "Public.products.userLicense",
"tr": "Kullanıcı Lisansı",
"en": "User License"
},
{
"resourceName": "Platform",
"key": "Public.products.userLicense.desc",
"tr": "Şube/şubelerinizde sistemin kaç personel tarafından kullandırılacağı kararına bağlı olarak belirlenir. Kullanıcı lisansları aylık/yıllık kullanım olarak sunulmaktadır.",
"en": "Determined by how many personnel in your branch/branches will use the system. User licenses are offered monthly/annually."
},
{
"resourceName": "Platform",
"key": "Public.products.remoteSupportContract",
"tr": "Şube Uzaktan Destek Sözleşmesi",
"en": "Branch Remote Support Contract"
},
{
"resourceName": "Platform",
"key": "Public.products.remoteSupportContract.desc",
"tr": "Kullanıcıların/Şubelerin talebi doğrultusunda imzalanan servis sözleşmesi ile Sözsoft kapsamı ve detayı sözleşmelerimizde yer alan iş birliği hizmeti sağlamaktadır.",
"en": "With a service contract signed upon user/branch request, Sözsoft provides cooperation services as detailed in our agreements."
},
{
"resourceName": "Platform",
"key": "Public.products.mobileReporting",
"tr": "Mobil Raporlama Arayüzü",
"en": "Mobile Reporting Interface"
},
{
"resourceName": "Platform",
"key": "Public.products.mobileReporting.desc",
"tr": "kursyazilimi.com mobil arayüzü ile şubeler bazında akıllı telefon teknolojisi ile yönetim imkanı sağlamaktadır.",
"en": "With kursyazilimi.coms mobile interface, management is possible per branch using smartphone technology."
}
], ],
"Settings": [ "Settings": [
{ {
@ -26730,9 +27494,9 @@
"Products": [ "Products": [
{ {
"id": "5f4d6c1f-b1e0-4f91-854c-1d59c25e7193", "id": "5f4d6c1f-b1e0-4f91-854c-1d59c25e7193",
"name": "Şube Barındırma Hizmeti", "name": "Public.products.branchHosting",
"description": "Şube veya şubelerinize ait tüm bilgilerinizin veya resimlerinizin barındırıldığı alan kiralama hizmetidir.", "description": "Public.products.branchHosting.desc",
"category": "Üyelik", "category": "Public.products.categories.Üyelik",
"order": 1, "order": 1,
"monthlyPrice": 850, "monthlyPrice": 850,
"yearlyPrice": 3400, "yearlyPrice": 3400,
@ -26741,9 +27505,9 @@
}, },
{ {
"id": "a85d0f04-7d40-47cb-bcf6-d95fbe31ec93", "id": "a85d0f04-7d40-47cb-bcf6-d95fbe31ec93",
"name": "Veri Yedekleme Hizmeti", "name": "Public.products.backupService",
"description": "Bilgileriniz bizler için de önemlidir. Bu doğrultuda aldığımız yüksek güvenlik önlemlerinin yanısıra BACK-UP/ yedekleme hizmeti sunmaktayız.", "description": "Public.products.backupService.desc",
"category": "Üyelik", "category": "Public.products.categories.Üyelik",
"order": 2, "order": 2,
"monthlyPrice": 2100, "monthlyPrice": 2100,
"yearlyPrice": 2100, "yearlyPrice": 2100,
@ -26752,9 +27516,9 @@
}, },
{ {
"id": "03cfae0b-4e3a-4b4b-917f-f798f18e0f15", "id": "03cfae0b-4e3a-4b4b-917f-f798f18e0f15",
"name": "Şube Uzaktan Destek Sözleşmesi", "name": "Public.products.remoteSupportContract",
"description": "Kullanıcıların/Şubelerin talebi doğrultusunda imzalanan servis sözleşmesi ile Sözsoft kapsamı ve detayı sözleşmelerimizde yer alan iş birliği hizmeti sağlamaktadır.", "description": "Public.products.remoteSupportContract.desc",
"category": "Üyelik", "category": "Public.products.categories.Üyelik",
"order": 3, "order": 3,
"monthlyPrice": 5000, "monthlyPrice": 5000,
"yearlyPrice": 5000, "yearlyPrice": 5000,
@ -26763,9 +27527,9 @@
}, },
{ {
"id": "03cfae0b-4e3a-4b4b-917f-f798f18e0f11", "id": "03cfae0b-4e3a-4b4b-917f-f798f18e0f11",
"name": "Kullanıcı Lisansı", "name": "Public.products.userLicense",
"description": "Şube/şubelerinizde sistemin kaç personel tarafından kullandırılacağı kararına bağlı olarak belirlenir. Kullanıcı lisansları aylık/yıllık kullanım olarak sunulmaktadır.", "description": "Public.products.userLicense.desc",
"category": "Lisans", "category": "Public.products.categories.Lisans",
"order": 4, "order": 4,
"monthlyPrice": 900, "monthlyPrice": 900,
"yearlyPrice": 3500, "yearlyPrice": 3500,
@ -26774,9 +27538,9 @@
}, },
{ {
"id": "36d98c72-6a62-4fa1-b942-2689eb42e4d4", "id": "36d98c72-6a62-4fa1-b942-2689eb42e4d4",
"name": "Öğretmen Lisansı", "name": "Public.products.teacherLicense",
"description": "Şube/şubelerinizde sistemin kaç öğretmen tarafından kullandırılacağı kararına bağlı olarak belirlenir. Öğretmen lisansları aylık/yıllık kullanım olarak sunulmaktadır.", "description": "Public.products.teacherLicense.desc",
"category": "Lisans", "category": "Public.products.categories.Lisans",
"order": 5, "order": 5,
"monthlyPrice": 500, "monthlyPrice": 500,
"yearlyPrice": 1700, "yearlyPrice": 1700,
@ -26785,9 +27549,9 @@
}, },
{ {
"id": "9a80f69d-46e5-4b92-91dc-fbb4d2f90c1f", "id": "9a80f69d-46e5-4b92-91dc-fbb4d2f90c1f",
"name": "Mobil Raporlama Arayüzü", "name": "Public.products.mobileReporting",
"description": "kursyazilimi.com mobil arayüzü ile şubeler bazında akıllı telefon teknolojisi ile yönetim imkanı sağlamaktadır.", "description": "Public.products.mobileReporting.desc",
"category": "Lisans", "category": "Public.products.categories.Lisans",
"order": 6, "order": 6,
"monthlyPrice": 400, "monthlyPrice": 400,
"yearlyPrice": 1400, "yearlyPrice": 1400,
@ -26796,9 +27560,9 @@
}, },
{ {
"id": "66324548-8500-4f06-8b1c-1a31e8d25c39", "id": "66324548-8500-4f06-8b1c-1a31e8d25c39",
"name": "Uzaktan Şube Eğitimi (5 Saat)", "name": "Public.products.remoteBranchTraining",
"description": "Sistem kullanımı sırasında kullanıcı hataları ortaya çıkabilir, sözsoft kullanıcı hatalarının giderilmesi yönünde destek hizmeti sunmaktadır.", "description": "Public.products.remoteBranchTraining.desc",
"category": "Ek Hizmetler", "category": "Public.products.categories.Ek Hizmetler",
"order": 7, "order": 7,
"monthlyPrice": 3700, "monthlyPrice": 3700,
"yearlyPrice": 3700, "yearlyPrice": 3700,
@ -26807,10 +27571,10 @@
}, },
{ {
"id": "b0b51a46-cf33-423f-b93f-2e2a3b8e27c0", "id": "b0b51a46-cf33-423f-b93f-2e2a3b8e27c0",
"name": "Saatlik Ek Hizmet", "name": "Public.products.extraHourlyService",
"description": "Şubelerin standart hizmetlerin dışında istedikleri özel çalışmaları için belirlenen saatlik hizmet bedelidir.", "description": "Public.products.extraHourlyService.desc",
"order": 8, "order": 8,
"category": "Ek Hizmetler", "category": "Public.products.categories.Ek Hizmetler",
"monthlyPrice": 2000, "monthlyPrice": 2000,
"yearlyPrice": 2000, "yearlyPrice": 2000,
"isQuantityBased": true, "isQuantityBased": true,
@ -26818,9 +27582,9 @@
}, },
{ {
"id": "15b813c6-4905-412b-999a-c303b91b3152", "id": "15b813c6-4905-412b-999a-c303b91b3152",
"name": "5,000 SMS SOFTWARE", "name": "Public.products.sms5k",
"description": "http://www.smsyukle.com adresinden yüklenebilir. İhtiyacınız olan SMS adedini belirleyerek, SMS satın almak için lütfen SMS yükle linkine tıklayın veya bize ulaşın", "description": "Public.products.sms5k.desc",
"category": "Ek Hizmetler", "category": "Public.products.categories.Ek Hizmetler",
"order": 9, "order": 9,
"monthlyPrice": 750, "monthlyPrice": 750,
"yearlyPrice": 750, "yearlyPrice": 750,
@ -26829,9 +27593,9 @@
}, },
{ {
"id": "e2c940b4-4a35-4f3d-8600-b1c2b9ce5179", "id": "e2c940b4-4a35-4f3d-8600-b1c2b9ce5179",
"name": "10,000 SMS SOFTWARE", "name": "Public.products.sms10k",
"description": "http://www.smsyukle.com adresinden yüklenebilir. İhtiyacınız olan SMS adedini belirleyerek, SMS satın almak için lütfen SMS yükle linkine tıklayın veya bize ulaşın", "description": "Public.products.sms10k.desc",
"category": "Ek Hizmetler", "category": "Public.products.categories.Ek Hizmetler",
"order": 10, "order": 10,
"monthlyPrice": 1350, "monthlyPrice": 1350,
"yearlyPrice": 1350, "yearlyPrice": 1350,
@ -26840,9 +27604,9 @@
}, },
{ {
"id": "1985ba1b1-f4c6-40f2-a747-0cf45c96a5b7", "id": "1985ba1b1-f4c6-40f2-a747-0cf45c96a5b7",
"name": "25,000 SMS SOFTWARE", "name": "Public.products.sms25k",
"description": "http://www.smsyukle.com adresinden yüklenebilir. İhtiyacınız olan SMS adedini belirleyerek, SMS satın almak için lütfen SMS yükle linkine tıklayın veya bize ulaşın", "description": "Public.products.sms25k.desc",
"category": "Ek Hizmetler", "category": "Public.products.categories.Ek Hizmetler",
"order": 11, "order": 11,
"monthlyPrice": 2900, "monthlyPrice": 2900,
"yearlyPrice": 2900, "yearlyPrice": 2900,
@ -26851,9 +27615,9 @@
}, },
{ {
"id": "7a7ae7c3-bef2-4978-90cf-96d1475e3492", "id": "7a7ae7c3-bef2-4978-90cf-96d1475e3492",
"name": "50,000 SMS SOFTWARE", "name": "Public.products.sms50k",
"description": "http://www.smsyukle.com adresinden yüklenebilir. İhtiyacınız olan SMS adedini belirleyerek, SMS satın almak için lütfen SMS yükle linkine tıklayın veya bize ulaşın", "description": "Public.products.sms50k.desc",
"category": "Ek Hizmetler", "category": "Public.products.categories.Ek Hizmetler",
"order": 12, "order": 12,
"monthlyPrice": 5100, "monthlyPrice": 5100,
"yearlyPrice": 5100, "yearlyPrice": 5100,
@ -26862,9 +27626,9 @@
}, },
{ {
"id": "3e6a8de1-6c48-4ff8-87f5-252735a7e89f", "id": "3e6a8de1-6c48-4ff8-87f5-252735a7e89f",
"name": "100,000 SMS SOFTWARE", "name": "Public.products.sms100k",
"description": "http://www.smsyukle.com adresinden yüklenebilir. İhtiyacınız olan SMS adedini belirleyerek, SMS satın almak için lütfen SMS yükle linkine tıklayın veya bize ulaşın", "description": "Public.products.sms100k.desc",
"category": "Ek Hizmetler", "category": "Public.products.categories.Ek Hizmetler",
"order": 13, "order": 13,
"monthlyPrice": 8800, "monthlyPrice": 8800,
"yearlyPrice": 8800, "yearlyPrice": 8800,
@ -26873,9 +27637,9 @@
}, },
{ {
"id": "2e35b9b8-f404-4b83-9737-d059c05fd44b", "id": "2e35b9b8-f404-4b83-9737-d059c05fd44b",
"name": "SMS Engelleme Hizmeti", "name": "Public.products.smsBlocking",
"description": "Şube/şubelerinizden gönderilen SMS lerin kursiyerler tarafından engelleme hizmetidir.", "description": "Public.products.smsBlocking.desc",
"category": "Ek Hizmetler", "category": "Public.products.categories.Ek Hizmetler",
"order": 14, "order": 14,
"monthlyPrice": 880, "monthlyPrice": 880,
"yearlyPrice": 880, "yearlyPrice": 880,

View file

@ -82,7 +82,7 @@ define(['./workbox-54d0af47'], (function (workbox) { 'use strict';
"revision": "3ca0b8505b4bec776b69afdba2768812" "revision": "3ca0b8505b4bec776b69afdba2768812"
}, { }, {
"url": "index.html", "url": "index.html",
"revision": "0.tihrrtufp5o" "revision": "0.4fqifsv2jio"
}], {}); }], {});
workbox.cleanupOutdatedCaches(); workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

View file

@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import { FaCalendar, FaClock, FaMinus, FaPlus, FaShoppingCart } from 'react-icons/fa'; import { FaCalendar, FaClock, FaMinus, FaPlus, FaShoppingCart } from 'react-icons/fa'
import { BillingCycle } from '@/proxy/order/models' import { BillingCycle } from '@/proxy/order/models'
import { CartState } from '@/utils/cartUtils' import { CartState } from '@/utils/cartUtils'
import { useScroll } from '@/contexts/ScrollContext' import { useScroll } from '@/contexts/ScrollContext'
@ -42,7 +42,9 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
<div className="flex items-center space-x-6"> <div className="flex items-center space-x-6">
<div className="flex items-center space-x-2 rounded-lg"> <div className="flex items-center space-x-2 rounded-lg">
<FaCalendar className="w-5 h-5 text-gray-600" /> <FaCalendar className="w-5 h-5 text-gray-600" />
<span className="font-medium text-gray-900">Faturalama Döngüsü:</span> <span className="font-medium text-gray-900">
{translate('::Public.products.billingcycle')}
</span>
</div> </div>
<div className="flex space-x-2"> <div className="flex space-x-2">
@ -51,7 +53,7 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
disabled={hasCartItems} disabled={hasCartItems}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${ className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
globalBillingCycle === 'monthly' globalBillingCycle === 'monthly'
? 'bg-blue-600 text-white shadow-md' ? 'bg-blue-600 text-white shadow-md'
: hasCartItems : hasCartItems
? 'bg-gray-200 text-gray-400 cursor-not-allowed' ? 'bg-gray-200 text-gray-400 cursor-not-allowed'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
@ -60,7 +62,7 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
hasCartItems ? 'Sepette ürün varken faturalama döngüsü değiştirilemez' : undefined hasCartItems ? 'Sepette ürün varken faturalama döngüsü değiştirilemez' : undefined
} }
> >
Aylık {translate('::Public.products.billingcycle.monthly')}
</button> </button>
<button <button
onClick={() => !hasCartItems && setGlobalBillingCycle('yearly')} onClick={() => !hasCartItems && setGlobalBillingCycle('yearly')}
@ -76,7 +78,7 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
hasCartItems ? 'Sepette ürün varken faturalama döngüsü değiştirilemez' : undefined hasCartItems ? 'Sepette ürün varken faturalama döngüsü değiştirilemez' : undefined
} }
> >
Yıllık {translate('::Public.products.billingcycle.yearly')}
</button> </button>
</div> </div>
</div> </div>
@ -85,7 +87,9 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<FaClock className="w-5 h-5 text-gray-600" /> <FaClock className="w-5 h-5 text-gray-600" />
<span className="font-medium text-gray-900">Periyod:</span> <span className="font-medium text-gray-900">
{translate('::Public.products.period')}
</span>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
@ -105,7 +109,9 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
<div className="flex items-center space-x-2 bg-gray-50 px-4 py-2 rounded-lg"> <div className="flex items-center space-x-2 bg-gray-50 px-4 py-2 rounded-lg">
<span className="font-bold text-lg text-blue-600">{globalPeriod}</span> <span className="font-bold text-lg text-blue-600">{globalPeriod}</span>
<span className="text-sm text-gray-600"> <span className="text-sm text-gray-600">
{globalBillingCycle === 'monthly' ? 'Ay' : 'Yıl'} {globalBillingCycle === 'monthly'
? translate('::Public.products.billingcycle.month')
: translate('::Public.products.billingcycle.year')}
</span> </span>
</div> </div>
@ -139,16 +145,6 @@ export const BillingControls: React.FC<BillingControlsProps> = ({
</button> </button>
</div> </div>
</div> </div>
{/* {globalBillingCycle === 'yearly' && globalPeriod > 1 && (
<div className="mt-3 text-center">
<div className="inline-flex items-center px-3 py-1 bg-emerald-100 text-emerald-800 text-sm font-medium rounded-full">
<span>
{globalPeriod} yıllık abonelik ile %{Math.round((1 - (globalBillingCycle === 'yearly' ? 1 : 12)) * 100)} tasarruf
</span>
</div>
</div>
)} */}
</div> </div>
</div> </div>
) )

View file

@ -1,70 +1,68 @@
import React from 'react'; import React from 'react'
import { import { FaTimes, FaMinus, FaPlus, FaShoppingBag, FaTrash } from 'react-icons/fa'
FaTimes, import { BillingCycle, BasketItem } from '@/proxy/order/models'
FaMinus, import { useLocalization } from '@/utils/hooks/useLocalization'
FaPlus,
FaShoppingBag,
FaTrash
} from 'react-icons/fa';
import { BillingCycle, BasketItem } from '@/proxy/order/models';
interface CartState { interface CartState {
items: BasketItem[]; items: BasketItem[]
total: number; total: number
globalBillingCycle: BillingCycle; globalBillingCycle: BillingCycle
globalPeriod: number; globalPeriod: number
} }
interface CartProps { interface CartProps {
isOpen: boolean; isOpen: boolean
onClose: () => void; onClose: () => void
onCheckout: () => void; onCheckout: () => void
cartState: CartState; cartState: CartState
updateQuantity: (id: string, quantity: number) => void; updateQuantity: (id: string, quantity: number) => void
removeItem: (id: string) => void; removeItem: (id: string) => void
clearCart: () => void; clearCart: () => void
} }
export const Cart: React.FC<CartProps> = ({ export const Cart: React.FC<CartProps> = ({
isOpen, isOpen,
onClose, onClose,
onCheckout, onCheckout,
cartState, cartState,
updateQuantity, updateQuantity,
removeItem, removeItem,
clearCart clearCart,
}) => { }) => {
const { translate } = useLocalization()
const handleClearCart = () => { const handleClearCart = () => {
if (window.confirm('Sepetteki tüm ürünleri silmek istediğinizden emin misiniz?')) { if (window.confirm('Sepetteki tüm ürünleri silmek istediğinizden emin misiniz?')) {
clearCart(); clearCart()
} }
}; }
const formatPrice = (price: number) => { const formatPrice = (price: number) => {
return new Intl.NumberFormat('tr-TR', { return new Intl.NumberFormat('tr-TR', {
style: 'currency', style: 'currency',
currency: 'TRY', currency: 'TRY',
minimumFractionDigits: 0 minimumFractionDigits: 0,
}).format(price); }).format(price)
}; }
if (!isOpen) return null; if (!isOpen) return null
return ( return (
<div className="fixed inset-0 z-50 overflow-hidden"> <div className="fixed inset-0 z-50 overflow-hidden">
<div className="absolute inset-0 bg-black bg-opacity-50" onClick={onClose} /> <div className="absolute inset-0 bg-black bg-opacity-50" onClick={onClose} />
<div className="absolute right-0 top-0 h-full w-full max-w-md bg-white shadow-xl"> <div className="absolute right-0 top-0 h-full w-full max-w-md bg-white shadow-xl">
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
<div className="flex items-center justify-between p-6 border-b border-gray-200"> <div className="flex items-center justify-between p-6 border-b border-gray-200">
<h2 className="text-lg font-semibold text-gray-900">Sepetim</h2> <h2 className="text-lg font-semibold text-gray-900">
{translate('::Public.cart.title')}
</h2>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
{cartState.items.length > 0 && ( {cartState.items.length > 0 && (
<button <button
onClick={handleClearCart} onClick={handleClearCart}
className="p-2 text-red-500 hover:bg-red-50 rounded-lg transition-colors" className="p-2 text-red-500 hover:bg-red-50 rounded-lg transition-colors"
title="Sepeti Temizle" title={translate('::Public.cart.clear')}
> >
<FaTrash className="w-5 h-5" /> <FaTrash className="w-5 h-5" />
</button> </button>
@ -82,16 +80,19 @@ export const Cart: React.FC<CartProps> = ({
{cartState.items.length === 0 ? ( {cartState.items.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-gray-500"> <div className="flex flex-col items-center justify-center h-full text-gray-500">
<FaShoppingBag className="w-16 h-16 mb-4" /> <FaShoppingBag className="w-16 h-16 mb-4" />
<p className="text-lg font-medium">Sepetiniz boş</p> <p className="text-lg font-medium">{translate('::Public.cart.empty.title')}</p>
<p className="text-sm">Ürün eklemek için katalogdan seçim yapın</p> <p className="text-sm">{translate('::Public.cart.empty.subtitle')}</p>
</div> </div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
{cartState.items.map((item: BasketItem, index: number) => ( {cartState.items.map((item: BasketItem, index: number) => (
<div key={`${item.product.id}-${item.billingCycle}-${index}`} className="border border-gray-200 rounded-lg p-4"> <div
key={`${item.product.id}-${item.billingCycle}-${index}`}
className="border border-gray-200 rounded-lg p-4"
>
<div className="flex justify-between items-start mb-2"> <div className="flex justify-between items-start mb-2">
<h4 className="font-medium text-gray-900 text-sm leading-tight"> <h4 className="font-medium text-gray-900 text-sm leading-tight">
{item.product.name} {translate('::' + item.product.name)}
</h4> </h4>
<button <button
onClick={() => removeItem(item.product.id)} onClick={() => removeItem(item.product.id)}
@ -100,21 +101,28 @@ export const Cart: React.FC<CartProps> = ({
<FaTimes className="w-4 h-4" /> <FaTimes className="w-4 h-4" />
</button> </button>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="text-sm text-gray-600"> <div className="text-sm text-gray-600">
{item.product.isQuantityBased && item.quantity > 1 && ( {item.product.isQuantityBased && item.quantity > 1 && (
<span className="font-medium text-gray-900">{item.quantity}x - </span> <span className="font-medium text-gray-900">{item.quantity}x - </span>
)} )}
{item.billingCycle === 'monthly' ? 'Aylık' : {item.billingCycle === 'monthly'
item.billingCycle === 'yearly' ? 'Yıllık' : 'Aylık'} ? translate('::Public.cart.monthly')
: item.billingCycle === 'yearly'
? translate('::Public.cart.yearly')
: translate('::Public.cart.monthly')}
{cartState.globalPeriod > 1 && item.product.isQuantityBased && ( {cartState.globalPeriod > 1 && item.product.isQuantityBased && (
<span className="text-xs text-gray-500"> <span className="text-xs text-gray-500">
({cartState.globalPeriod} {item.billingCycle === 'monthly' ? 'Ay' : 'Yıl'}) ({cartState.globalPeriod}{' '}
{item.billingCycle === 'monthly'
? translate('::Public.cart.month')
: translate('::Public.cart.year')}
)
</span> </span>
)} )}
</div> </div>
{item.product.isQuantityBased && ( {item.product.isQuantityBased && (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<button <button
@ -133,7 +141,7 @@ export const Cart: React.FC<CartProps> = ({
</div> </div>
)} )}
</div> </div>
<div className="mt-2 font-semibold text-blue-600"> <div className="mt-2 font-semibold text-blue-600">
{formatPrice(item.totalPrice)} {formatPrice(item.totalPrice)}
</div> </div>
@ -146,22 +154,24 @@ export const Cart: React.FC<CartProps> = ({
{cartState.items.length > 0 && ( {cartState.items.length > 0 && (
<div className="border-t border-gray-200 p-6"> <div className="border-t border-gray-200 p-6">
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<span className="text-lg font-semibold text-gray-900">Toplam:</span> <span className="text-lg font-semibold text-gray-900">
{translate('::Public.cart.total')}
</span>
<span className="text-xl font-bold text-blue-600"> <span className="text-xl font-bold text-blue-600">
{formatPrice(cartState.total)} {formatPrice(cartState.total)}
</span> </span>
</div> </div>
<button <button
onClick={onCheckout} onClick={onCheckout}
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-4 rounded-lg transition-colors duration-200" className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-4 rounded-lg transition-colors duration-200"
> >
Ödemeye Geç {translate('::Public.cart.checkout')}
</button> </button>
</div> </div>
)} )}
</div> </div>
</div> </div>
</div> </div>
); )
}; }

View file

@ -5,6 +5,7 @@ import {
} from 'react-icons/fa'; } from 'react-icons/fa';
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { ROUTES_ENUM } from '@/routes/route.constant' import { ROUTES_ENUM } from '@/routes/route.constant'
import { useLocalization } from '@/utils/hooks/useLocalization';
interface OrderSuccessProps { interface OrderSuccessProps {
orderId: string orderId: string
@ -13,24 +14,25 @@ interface OrderSuccessProps {
export const OrderSuccess: React.FC<OrderSuccessProps> = ({ orderId, onBackToShop }) => { export const OrderSuccess: React.FC<OrderSuccessProps> = ({ orderId, onBackToShop }) => {
const navigate = useNavigate() const navigate = useNavigate()
const { translate } = useLocalization()
return ( return (
<div className="max-w-2xl mx-auto text-center"> <div className="max-w-2xl mx-auto text-center">
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-8"> <div className="bg-white rounded-xl shadow-lg border border-gray-200 p-8">
<div className="mb-6"> <div className="mb-6">
<FaCheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" /> <FaCheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" />
<h2 className="text-2xl font-bold text-gray-900 mb-2">Siparişiniz Başarıyla Alındı!</h2> <h2 className="text-2xl font-bold text-gray-900 mb-2">{translate('::Public.order.success.title')}</h2>
<p className="text-gray-600"> <p className="text-gray-600">
Sipariş numaranız: <span className="font-semibold text-blue-600">#{orderId}</span> {translate('::Public.order.success.number')} <span className="font-semibold text-blue-600">#{orderId}</span>
</p> </p>
</div> </div>
<div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-6"> <div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-6">
<h3 className="font-semibold text-green-800 mb-2">Sonraki Adımlar</h3> <h3 className="font-semibold text-green-800 mb-2">{translate('::Public.order.success.nextSteps')}</h3>
<ul className="text-sm text-green-700 space-y-1 text-left"> <ul className="text-sm text-green-700 space-y-1 text-left">
<li> Siparişiniz 24 saat içinde işleme alınacaktır</li> <li> {translate('::Public.order.success.step1')}</li>
<li> Lisans bilgileri e-posta adresinize gönderilecektir</li> <li> {translate('::Public.order.success.step2')}</li>
<li> Kurulum desteği için ekibimizle iletişime geçebilirsiniz</li> <li> {translate('::Public.order.success.step3')}</li>
</ul> </ul>
</div> </div>
@ -40,7 +42,7 @@ export const OrderSuccess: React.FC<OrderSuccessProps> = ({ orderId, onBackToSho
className="flex items-center justify-center px-6 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors" className="flex items-center justify-center px-6 py-3 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
> >
<FaArrowLeft className="w-4 h-4 mr-2" /> <FaArrowLeft className="w-4 h-4 mr-2" />
Ana Sayfa'ya Dön {translate('::Public.order.success.backHome')}
</button> </button>
</div> </div>
</div> </div>

View file

@ -1,19 +1,19 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { import {
FaCreditCard, FaCreditCard,
FaLock, FaLock,
FaArrowLeft, FaArrowLeft,
FaUser, FaUser,
FaBuilding, FaBuilding,
FaMailBulk, FaMailBulk,
FaPhone, FaPhone,
FaMapMarkerAlt, FaMapMarkerAlt,
FaMap, FaMap,
FaGlobe, FaGlobe,
FaMoneyBillWave, FaMoneyBillWave,
FaDollarSign, FaDollarSign,
FaUserPlus FaUserPlus,
} from 'react-icons/fa'; } from 'react-icons/fa'
import { import {
BillingCycle, BillingCycle,
BasketItem, BasketItem,
@ -22,6 +22,7 @@ import {
} from '@/proxy/order/models' } from '@/proxy/order/models'
import { OrderService } from '@/services/order.service' import { OrderService } from '@/services/order.service'
import { CustomTenantDto } from '@/proxy/config/models' import { CustomTenantDto } from '@/proxy/config/models'
import { useLocalization } from '@/utils/hooks/useLocalization'
interface CartState { interface CartState {
items: BasketItem[] items: BasketItem[]
@ -52,6 +53,7 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({
cardName: '', cardName: '',
}) })
const { translate } = useLocalization()
const [paymentMethods, setPaymentMethods] = useState<PaymentMethodDto[]>([]) const [paymentMethods, setPaymentMethods] = useState<PaymentMethodDto[]>([])
const [installmentOptions, setInstallmentOptions] = useState<InstallmentOptionDto[]>([]) const [installmentOptions, setInstallmentOptions] = useState<InstallmentOptionDto[]>([])
const [loading, setLoading] = useState<boolean>(true) const [loading, setLoading] = useState<boolean>(true)
@ -116,7 +118,9 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({
return ( return (
<div className="mx-auto px-4 lg:px-8 mb-6"> <div className="mx-auto px-4 lg:px-8 mb-6">
{loading ? ( {loading ? (
<div className="text-center py-12 text-gray-500">Yükleniyor...</div> <div className="text-center py-12 text-gray-500">
{translate('::Public.payment.loading')}
</div>
) : ( ) : (
<form onSubmit={handleSubmit} className="flex flex-col lg:flex-row gap-6"> <form onSubmit={handleSubmit} className="flex flex-col lg:flex-row gap-6">
{/* 3 Sütun: Ödeme Yöntemi | Taksit Seçenekleri | Sipariş Özeti */} {/* 3 Sütun: Ödeme Yöntemi | Taksit Seçenekleri | Sipariş Özeti */}
@ -125,88 +129,115 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({
{tenant && ( {tenant && (
<> <>
<h2 className="text-lg font-semibold mb-4 flex items-center"> <h2 className="text-lg font-semibold mb-4 flex items-center">
<FaUser className="w-5 h-5 text-green-600 mr-2" /> Müşteri Bilgileri <FaUser className="w-5 h-5 text-green-600 mr-2" />{' '}
{translate('::Public.payment.customerInfo')}
</h2> </h2>
<div className="pt-4 border-t border-gray-200"> <div className="pt-4 border-t border-gray-200">
<div className="grid grid-cols-1 gap-y-3 text-sm text-gray-700"> <div className="grid grid-cols-1 gap-y-3 text-sm text-gray-700">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaUser className="w-4 h-4 text-gray-500" /> <FaUser className="w-4 h-4 text-gray-500" />
<span className="font-medium">Kurum Kodu:</span> <span className="font-medium">
{translate('::Public.payment.customer.code')}
</span>
<span>{tenant.name}</span> <span>{tenant.name}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaUser className="w-4 h-4 text-gray-500" /> <FaUser className="w-4 h-4 text-gray-500" />
<span className="font-medium">Kurucu:</span> <span className="font-medium">
{translate('::Public.payment.customer.founder')}
</span>
<span>{tenant.founder}</span> <span>{tenant.founder}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaBuilding className="w-4 h-4 text-gray-500" /> <FaBuilding className="w-4 h-4 text-gray-500" />
<span className="font-medium">Şirket:</span> <span className="font-medium">
{translate('::Public.payment.customer.company')}
</span>
<span>{tenant.institutionName}</span> <span>{tenant.institutionName}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaMailBulk className="w-4 h-4 text-gray-500" /> <FaMailBulk className="w-4 h-4 text-gray-500" />
<span className="font-medium">E-posta:</span> <span className="font-medium">
{translate('::Public.payment.customer.email')}
</span>
<span>{tenant.email}</span> <span>{tenant.email}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaPhone className="w-4 h-4 text-gray-500" /> <FaPhone className="w-4 h-4 text-gray-500" />
<span className="font-medium">Telefon:</span> <span className="font-medium">
{translate('::Public.payment.customer.phone')}
</span>
<span>{tenant.phone}</span> <span>{tenant.phone}</span>
</div> </div>
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<FaMapMarkerAlt className="w-4 h-4 text-gray-500 mt-0.5" /> <FaMapMarkerAlt className="w-4 h-4 text-gray-500 mt-0.5" />
<div> <div>
<span className="font-medium">Adres:</span> <span className="font-medium">
{translate('::Public.payment.customer.address')}:
</span>
<div>{tenant.address}</div> <div>{tenant.address}</div>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaGlobe className="w-4 h-4 text-gray-500" /> <FaGlobe className="w-4 h-4 text-gray-500" />
<span className="font-medium">Ülke:</span> <span className="font-medium">
{translate('::Public.payment.customer.country')}
</span>
<span>{tenant.country}</span> <span>{tenant.country}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaGlobe className="w-4 h-4 text-gray-500" /> <FaGlobe className="w-4 h-4 text-gray-500" />
<span className="font-medium">Şehir:</span> <span className="font-medium">
{translate('::Public.payment.customer.city')}
</span>
<span>{tenant.city}</span> <span>{tenant.city}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaMap className="w-4 h-4 text-gray-500" /> <FaMap className="w-4 h-4 text-gray-500" />
<span className="font-medium">İlçe:</span> <span className="font-medium">
{translate('::Public.payment.customer.district')}
</span>
<span>{tenant.district}</span> <span>{tenant.district}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaMapMarkerAlt className="w-4 h-4 text-gray-500" /> <FaMapMarkerAlt className="w-4 h-4 text-gray-500" />
<span className="font-medium">Posta Kodu:</span> <span className="font-medium">
{translate('::Public.payment.customer.postalCode')}:
</span>
<span>{tenant.postalCode}</span> <span>{tenant.postalCode}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaMoneyBillWave className="w-4 h-4 text-gray-500" /> <FaMoneyBillWave className="w-4 h-4 text-gray-500" />
<span className="font-medium">Vergi Dairesi:</span> <span className="font-medium">
{translate('::Public.payment.customer.taxOffice')}:
</span>
<span>{tenant.taxOffice}</span> <span>{tenant.taxOffice}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaDollarSign className="w-4 h-4 text-gray-500" /> <FaDollarSign className="w-4 h-4 text-gray-500" />
<span className="font-medium">Vergi No:</span> <span className="font-medium">
{translate('::Public.payment.customer.taxNumber')}:
</span>
<span>{tenant.vknTckn}</span> <span>{tenant.vknTckn}</span>
</div> </div>
{tenant.reference && ( {tenant.reference && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaUserPlus className="w-4 h-4 text-gray-500" /> <FaUserPlus className="w-4 h-4 text-gray-500" />
<span className="font-medium">Referans:</span> <span className="font-medium">
{translate('::Public.payment.customer.reference')}:
</span>
<span>{tenant.reference}</span> <span>{tenant.reference}</span>
</div> </div>
)} )}
@ -221,7 +252,8 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({
{/* 1. Sütun: Ödeme Yöntemi */} {/* 1. Sütun: Ödeme Yöntemi */}
<div className="bg-white rounded-xl shadow border p-6"> <div className="bg-white rounded-xl shadow border p-6">
<h2 className="text-lg font-semibold mb-4 flex items-center"> <h2 className="text-lg font-semibold mb-4 flex items-center">
<FaLock className="w-5 h-5 text-green-600 mr-2" /> Ödeme Yöntemi <FaLock className="w-5 h-5 text-green-600 mr-2" />{' '}
{translate('::Public.payment.method.title')}
</h2> </h2>
<div className="space-y-2"> <div className="space-y-2">
{paymentMethods.map((method) => ( {paymentMethods.map((method) => (
@ -245,7 +277,9 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({
<div> <div>
<div className="font-medium text-gray-900">{method.name}</div> <div className="font-medium text-gray-900">{method.name}</div>
<div className="text-sm text-gray-600"> <div className="text-sm text-gray-600">
{method.id === 'credit-card' ? 'Taksit seçenekleri mevcut' : 'Komisyon yok'} {method.id === 'credit-card'
? translate('::Public.payment.method.installmentsAvailable')
: translate('::Public.payment.method.noCommission')}
</div> </div>
</div> </div>
</label> </label>
@ -256,7 +290,9 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({
{/* Taksit Seçenekleri */} {/* Taksit Seçenekleri */}
{selectedPaymentMethod === 'credit-card' && ( {selectedPaymentMethod === 'credit-card' && (
<div className="bg-white rounded-xl shadow border p-4"> <div className="bg-white rounded-xl shadow border p-4">
<h3 className="text-md font-medium text-gray-800 mb-2">Taksit Seçenekleri</h3> <h3 className="text-md font-medium text-gray-800 mb-2">
{translate('::Public.payment.installments.title')}
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3"> <div className="grid grid-cols-2 md:grid-cols-3 gap-3">
{installmentOptions.map((option) => ( {installmentOptions.map((option) => (
<label <label
@ -277,13 +313,15 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({
/> />
<div className="font-semibold text-base mb-1">{option.name}</div> <div className="font-semibold text-base mb-1">{option.name}</div>
<div className="text-gray-500 mb-1"> <div className="text-gray-500 mb-1">
Komisyon:{' '} {translate('::Public.payment.installments.commission')}{' '}
<span className="font-semibold"> <span className="font-semibold">
%{(option.commission * 100).toFixed(1)} %{(option.commission * 100).toFixed(1)}
</span> </span>
</div> </div>
<div className="text-blue-700 font-bold mb-1"> <div className="text-blue-700 font-bold mb-1">
{option.id > 1 ? `${option.id} x aylık` : 'Tek ödeme'} {option.id > 1
? `${option.id} ${translate('::Public.payment.installments.monthly')}`
: translate('::Public.payment.installments.single')}
</div> </div>
<div className="font-extrabold text-lg text-gray-900 mt-1"> <div className="font-extrabold text-lg text-gray-900 mt-1">
{formatPrice( {formatPrice(
@ -302,14 +340,16 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({
{/* Kart Bilgileri */} {/* Kart Bilgileri */}
{selectedPaymentMethod !== 'bank-transfer' && ( {selectedPaymentMethod !== 'bank-transfer' && (
<div className="bg-white rounded-xl shadow border p-6 space-y-3"> <div className="bg-white rounded-xl shadow border p-6 space-y-3">
<h3 className="text-md font-medium text-gray-800 mb-3">Kart Bilgileri</h3> <h3 className="text-md font-medium text-gray-800 mb-3">
{translate('::Public.payment.card.title')}
</h3>
<input <input
type="text" type="text"
required required
value={paymentData.cardName} value={paymentData.cardName}
onChange={(e) => handleInputChange('cardName', e.target.value)} onChange={(e) => handleInputChange('cardName', e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg" className="w-full px-4 py-2 border border-gray-300 rounded-lg"
placeholder="Kart Üzerindeki İsim" placeholder={translate('::Public.payment.card.name')}
/> />
<input <input
type="text" type="text"
@ -317,7 +357,7 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({
value={paymentData.cardNumber} value={paymentData.cardNumber}
onChange={(e) => handleInputChange('cardNumber', e.target.value)} onChange={(e) => handleInputChange('cardNumber', e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg" className="w-full px-4 py-2 border border-gray-300 rounded-lg"
placeholder="Kart Numarası" placeholder={translate('::Public.payment.card.number')}
maxLength={19} maxLength={19}
/> />
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
@ -345,7 +385,9 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({
{/* Sipariş Özeti ve Butonlar */} {/* Sipariş Özeti ve Butonlar */}
<div className="bg-white rounded-xl shadow border p-6"> <div className="bg-white rounded-xl shadow border p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Sipariş Özeti</h3> <h3 className="text-lg font-semibold text-gray-900 mb-4">
{translate('::Public.payment.summary.title')}
</h3>
<div className="space-y-4 mb-4"> <div className="space-y-4 mb-4">
{cartState.items.map((item, index) => ( {cartState.items.map((item, index) => (
@ -370,18 +412,18 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({
<div className="space-y-1 text-sm border-t border-gray-200 pt-4 text-gray-600"> <div className="space-y-1 text-sm border-t border-gray-200 pt-4 text-gray-600">
<div className="flex justify-between"> <div className="flex justify-between">
<span>Ara Toplam:</span> <span>{translate('::Public.payment.summary.subtotal')}</span>
<span>{formatPrice(cartState.total)}</span> <span>{formatPrice(cartState.total)}</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span>Komisyon:</span> <span>{translate('::Public.payment.summary.commission')}</span>
<span>{formatPrice(commission)}</span> <span>{formatPrice(commission)}</span>
</div> </div>
{selectedPaymentMethod === 'credit-card' && {selectedPaymentMethod === 'credit-card' &&
selectedInstallment?.id && selectedInstallment?.id &&
selectedInstallment.id > 1 && ( selectedInstallment.id > 1 && (
<div className="flex justify-between text-blue-600"> <div className="flex justify-between text-blue-600">
<span>Aylık Taksit:</span> <span>{translate('::Public.payment.summary.monthlyInstallment')}: </span>
<span> <span>
{formatPrice(finalTotal / selectedInstallment.id)} x{' '} {formatPrice(finalTotal / selectedInstallment.id)} x{' '}
{selectedInstallment.id} {selectedInstallment.id}
@ -389,7 +431,7 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({
</div> </div>
)} )}
<div className="flex justify-between text-base font-bold pt-2 text-gray-900"> <div className="flex justify-between text-base font-bold pt-2 text-gray-900">
<span>Toplam:</span> <span>{translate('::Public.payment.summary.total')}</span>
<span className="text-blue-600">{formatPrice(finalTotal)}</span> <span className="text-blue-600">{formatPrice(finalTotal)}</span>
</div> </div>
</div> </div>
@ -402,14 +444,16 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({
className="flex items-center px-6 py-3 border border-gray-300 text-gray-700 rounded-lg" className="flex items-center px-6 py-3 border border-gray-300 text-gray-700 rounded-lg"
> >
<FaArrowLeft className="w-4 h-4 mr-2" /> <FaArrowLeft className="w-4 h-4 mr-2" />
Geri {translate('::Public.payment.buttons.back')}
</button> </button>
<button <button
type="submit" type="submit"
className="flex items-center justify-center px-6 py-3 bg-green-600 text-white rounded-lg" className="flex items-center justify-center px-6 py-3 bg-green-600 text-white rounded-lg"
> >
<FaCreditCard className="w-5 h-5 mr-2" /> <FaCreditCard className="w-5 h-5 mr-2" />
{selectedPaymentMethod === 'bank-transfer' ? 'Siparişi Tamamla' : 'Ödeme Yap'} {selectedPaymentMethod === 'bank-transfer'
? translate('::Public.payment.buttons.completeOrder')
: translate('::Public.payment.buttons.pay')}
</button> </button>
</div> </div>
</div> </div>

View file

@ -1,81 +1,91 @@
import React, { useState } from 'react'; import React, { useState } from 'react'
import { FaPlus, FaMinus } from 'react-icons/fa'; import { FaPlus, FaMinus } from 'react-icons/fa'
import { BillingCycle, Product, ProductDto } from '@/proxy/order/models'; import { BillingCycle, Product, ProductDto } from '@/proxy/order/models'
import { CartState } from '@/utils/cartUtils'; import { CartState } from '@/utils/cartUtils'
import { useLocalization } from '@/utils/hooks/useLocalization'
interface ProductCardProps { interface ProductCardProps {
product: ProductDto; product: ProductDto
globalBillingCycle: BillingCycle; globalBillingCycle: BillingCycle
globalPeriod: number; globalPeriod: number
addItem: (product: ProductDto, quantity: number, billingCycle: BillingCycle) => void; addItem: (product: ProductDto, quantity: number, billingCycle: BillingCycle) => void
cartState: CartState; cartState: CartState
} }
export const ProductCard: React.FC<ProductCardProps> = ({ export const ProductCard: React.FC<ProductCardProps> = ({
product, product,
globalBillingCycle, globalBillingCycle,
globalPeriod, globalPeriod,
addItem, addItem,
cartState cartState,
}) => { }) => {
const [quantity, setQuantity] = useState(1); const [quantity, setQuantity] = useState(1)
const { translate } = useLocalization()
// Direct check with current cart state // Direct check with current cart state
const isInCart = cartState.items.some( const isInCart = cartState.items.some(
item => item.product.id === product.id && item.billingCycle === globalBillingCycle (item) => item.product.id === product.id && item.billingCycle === globalBillingCycle,
); )
const formatPrice = (price: number) => { const formatPrice = (price: number) => {
return new Intl.NumberFormat('tr-TR', { return new Intl.NumberFormat('tr-TR', {
style: 'currency', style: 'currency',
currency: 'TRY', currency: 'TRY',
minimumFractionDigits: 0 minimumFractionDigits: 0,
}).format(price); }).format(price)
}; }
const getCurrentPrice = () => { const getCurrentPrice = () => {
return globalBillingCycle === 'monthly' ? product.monthlyPrice! : product.yearlyPrice!; return globalBillingCycle === 'monthly' ? product.monthlyPrice! : product.yearlyPrice!
}; }
const getTotalPrice = () => { const getTotalPrice = () => {
return getCurrentPrice() * quantity * globalPeriod; return getCurrentPrice() * quantity * globalPeriod
}; }
const handleAddToCart = () => { const handleAddToCart = () => {
// Non-quantity products should not be added if already in cart // Non-quantity products should not be added if already in cart
if (!product.isQuantityBased && isInCart) { if (!product.isQuantityBased && isInCart) {
return; return
} }
// For non-quantity products, always use quantity 1 // For non-quantity products, always use quantity 1
const quantityToAdd = !product.isQuantityBased ? 1 : quantity; const quantityToAdd = !product.isQuantityBased ? 1 : quantity
addItem(product, quantityToAdd, globalBillingCycle); addItem(product, quantityToAdd, globalBillingCycle)
setQuantity(1); setQuantity(1)
}; }
const getUnitText = () => { const getUnitText = () => {
switch (product.unit) { switch (product.unit) {
case 'monthly': return globalBillingCycle === 'monthly' ? '/ Ay' : '/ Yıl'; case 'monthly':
case 'yearly': return '/ Yıl'; return globalBillingCycle === 'monthly'
default: return ''; ? translate('::Public.products.price.perMonthSuffix')
: translate('::Public.products.price.perYearSuffix')
case 'yearly':
return translate('::Public.products.price.perYearSuffix')
default:
return ''
} }
}; }
const getPeriodText = () => { const getPeriodText = () => {
const periodText = globalBillingCycle === 'monthly' ? 'Ay' : 'Yıl'; const periodText =
return globalPeriod > 1 ? ` (${globalPeriod} ${periodText})` : ''; globalBillingCycle === 'monthly'
}; ? translate('::Public.products.period.month')
: translate('::Public.products.period.year')
return globalPeriod > 1 ? ` (${globalPeriod} ${periodText})` : ''
}
// Check if product has valid price for current billing cycle // Check if product has valid price for current billing cycle
const hasValidPrice = () => { const hasValidPrice = () => {
if (globalBillingCycle === 'monthly' && !product.monthlyPrice) return false; if (globalBillingCycle === 'monthly' && !product.monthlyPrice) return false
if (globalBillingCycle === 'yearly' && !product.yearlyPrice) return false; if (globalBillingCycle === 'yearly' && !product.yearlyPrice) return false
return true; return true
}; }
// If product doesn't have valid price, don't render it at all // If product doesn't have valid price, don't render it at all
if (!hasValidPrice()) { if (!hasValidPrice()) {
return null; return null
} }
return ( return (
@ -89,27 +99,25 @@ export const ProductCard: React.FC<ProductCardProps> = ({
/> />
</div> </div>
)} )}
<div className="p-6 flex flex-col flex-1"> <div className="p-6 flex flex-col flex-1">
<div className="mb-2"> <div className="mb-2">
<span className="inline-block px-3 py-1 bg-blue-100 text-blue-800 text-sm font-medium rounded-full"> <span className="inline-block px-3 py-1 bg-blue-100 text-blue-800 text-sm font-medium rounded-full">
{product.category} {translate('::' + product.category)}
</span> </span>
</div> </div>
<h3 className="text-lg font-semibold text-gray-900 mb-2 line-clamp-2"> <h3 className="text-lg font-semibold text-gray-900 mb-2 line-clamp-2">{translate('::' + product.name)}</h3>
{product.name}
</h3> <p className="text-gray-600 text-sm mb-4 line-clamp-3">{translate('::' + product.description)}</p>
<p className="text-gray-600 text-sm mb-4 line-clamp-3">
{product.description}
</p>
{/* Quantity and Yearly Savings above price */} {/* Quantity and Yearly Savings above price */}
<div className="mb-4 space-y-3"> <div className="mb-4 space-y-3">
{product.isQuantityBased && ( {product.isQuantityBased && (
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<span className="text-sm font-medium text-gray-700">Adet:</span> <span className="text-sm font-medium text-gray-700">
{translate('::Public.products.quantity')}
</span>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<button <button
onClick={() => setQuantity(Math.max(1, quantity - 1))} onClick={() => setQuantity(Math.max(1, quantity - 1))}
@ -130,7 +138,9 @@ export const ProductCard: React.FC<ProductCardProps> = ({
{globalBillingCycle === 'yearly' && product.monthlyPrice && product.yearlyPrice && ( {globalBillingCycle === 'yearly' && product.monthlyPrice && product.yearlyPrice && (
<div className="text-sm text-emerald-600 font-medium"> <div className="text-sm text-emerald-600 font-medium">
Yıllık ile %{Math.round((1 - (product.yearlyPrice / (product.monthlyPrice * 12))) * 100)} tasarruf {translate('::Public.products.savings.yearly', {
percent: Math.round((1 - product.yearlyPrice / (product.monthlyPrice * 12)) * 100),
})}
</div> </div>
)} )}
</div> </div>
@ -140,40 +150,36 @@ export const ProductCard: React.FC<ProductCardProps> = ({
<div className="mb-4"> <div className="mb-4">
<div className="text-2xl font-bold text-gray-900"> <div className="text-2xl font-bold text-gray-900">
{formatPrice(getCurrentPrice())} {formatPrice(getCurrentPrice())}
<span className="text-sm font-normal text-gray-500 ml-1"> <span className="text-sm font-normal text-gray-500 ml-1">{getUnitText()}</span>
{getUnitText()}
</span>
</div> </div>
{globalPeriod > 1 && ( {globalPeriod > 1 && (
<div className="text-lg font-semibold text-blue-600 mt-1"> <div className="text-lg font-semibold text-blue-600 mt-1">
Toplam: {formatPrice(getTotalPrice())} {translate('::Public.products.total')} {formatPrice(getTotalPrice())}
<span className="text-sm font-normal text-gray-500 ml-1"> <span className="text-sm font-normal text-gray-500 ml-1">{getPeriodText()}</span>
{getPeriodText()}
</span>
</div> </div>
)} )}
</div> </div>
{(() => { {(() => {
const isNonQuantityProduct = !product.isQuantityBased; const isNonQuantityProduct = !product.isQuantityBased
const isDisabled = isNonQuantityProduct && isInCart; const isDisabled = isNonQuantityProduct && isInCart
return ( return (
<button <button
onClick={handleAddToCart} onClick={handleAddToCart}
disabled={isDisabled} disabled={isDisabled}
className={`w-full font-medium py-3 px-4 rounded-lg transition-colors duration-200 transform ${ className={`w-full font-medium py-3 px-4 rounded-lg transition-colors duration-200 transform ${
isDisabled isDisabled
? 'bg-gray-400 text-gray-700 cursor-not-allowed' ? 'bg-gray-400 text-gray-700 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700 text-white hover:scale-[1.02] active:scale-[0.98]' : 'bg-blue-600 hover:bg-blue-700 text-white hover:scale-[1.02] active:scale-[0.98]'
}`} }`}
> >
{isDisabled ? 'Sepette Mevcut' : 'Sepete Ekle'} {isDisabled ? translate('::Public.products.inCart') : translate('::Public.products.addToCart')}
</button> </button>
); )
})()} })()}
</div> </div>
</div> </div>
</div> </div>
); )
}; }

View file

@ -4,6 +4,7 @@ import { ProductCard } from './ProductCard'
import { addItemToCart, CartState } from '@/utils/cartUtils' import { addItemToCart, CartState } from '@/utils/cartUtils'
import { BillingCycle, ProductDto } from '@/proxy/order/models' import { BillingCycle, ProductDto } from '@/proxy/order/models'
import { OrderService } from '@/services/order.service' import { OrderService } from '@/services/order.service'
import { useLocalization } from '@/utils/hooks/useLocalization';
interface ProductCatalogProps { interface ProductCatalogProps {
searchQuery: string searchQuery: string
@ -18,6 +19,8 @@ export const ProductCatalog: React.FC<ProductCatalogProps> = ({
cartState, cartState,
setCartState, setCartState,
}) => { }) => {
const { translate } = useLocalization()
const [selectedCategory, setSelectedCategory] = useState<string>('all') const [selectedCategory, setSelectedCategory] = useState<string>('all')
const [products, setProducts] = useState<ProductDto[]>([]) const [products, setProducts] = useState<ProductDto[]>([])
const [loading, setLoading] = useState<boolean>(true) const [loading, setLoading] = useState<boolean>(true)
@ -87,7 +90,7 @@ export const ProductCatalog: React.FC<ProductCatalogProps> = ({
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 sticky top-40"> <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 sticky top-40">
<div className="flex items-center space-x-2 mb-4"> <div className="flex items-center space-x-2 mb-4">
<FaFilter className="w-5 h-5 text-gray-600" /> <FaFilter className="w-5 h-5 text-gray-600" />
<h3 className="font-semibold text-gray-900">Kategoriler</h3> <h3 className="font-semibold text-gray-900">{translate('::Public.products.categories')}</h3>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
{categories.map((category) => ( {categories.map((category) => (
@ -100,7 +103,7 @@ export const ProductCatalog: React.FC<ProductCatalogProps> = ({
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
}`} }`}
> >
<span>{category === 'all' ? 'Tüm Ürünler' : category}</span> <span>{category === 'all' ? translate('::Public.products.categories.all') : translate('::' + category)}</span>
<span <span
className={`text-xs px-2 py-1 rounded-full ${ className={`text-xs px-2 py-1 rounded-full ${
selectedCategory === category selectedCategory === category
@ -118,13 +121,13 @@ export const ProductCatalog: React.FC<ProductCatalogProps> = ({
<div className="mt-6"> <div className="mt-6">
<div className="flex items-center space-x-2 mb-3"> <div className="flex items-center space-x-2 mb-3">
<FaSearch className="w-5 h-5 text-gray-600" /> <FaSearch className="w-5 h-5 text-gray-600" />
<h3 className="font-semibold text-gray-900">Arama</h3> <h3 className="font-semibold text-gray-900">{translate('::Public.products.search.title')}</h3>
</div> </div>
<div className="relative"> <div className="relative">
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" /> <FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<input <input
type="text" type="text"
placeholder="Ürün ara..." placeholder={translate('::Public.products.search.placeholder')}
value={searchQuery} value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)} onChange={(e) => onSearchChange(e.target.value)}
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all" className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
@ -136,7 +139,7 @@ export const ProductCatalog: React.FC<ProductCatalogProps> = ({
<main className="flex-1"> <main className="flex-1">
{loading ? ( {loading ? (
<div className="text-center py-12 text-gray-500">Yükleniyor...</div> <div className="text-center py-12 text-gray-500">{translate('::Public.products.loading')}</div>
) : ( ) : (
<> <>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
@ -156,8 +159,8 @@ export const ProductCatalog: React.FC<ProductCatalogProps> = ({
<div className="text-gray-400 mb-2"> <div className="text-gray-400 mb-2">
<FaFilter className="w-12 h-12 mx-auto" /> <FaFilter className="w-12 h-12 mx-auto" />
</div> </div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Ürün bulunamadı</h3> <h3 className="text-lg font-medium text-gray-900 mb-2">{translate('::Public.products.empty.title')}</h3>
<p className="text-gray-600">Arama kriterlerinizi değiştirmeyi deneyin.</p> <p className="text-gray-600">{translate('::Public.products.empty.description')}</p>
</div> </div>
)} )}
</> </>

View file

@ -17,6 +17,7 @@ import {
} from 'react-icons/fa' } from 'react-icons/fa'
import React, { useState } from 'react' import React, { useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useLocalization } from '@/utils/hooks/useLocalization'
interface TenantFormProps { interface TenantFormProps {
onSubmit: (tenant: CustomTenantDto) => void onSubmit: (tenant: CustomTenantDto) => void
@ -27,6 +28,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
const [isExisting, setIsExisting] = useState<boolean>(true) const [isExisting, setIsExisting] = useState<boolean>(true)
const [formData, setFormData] = useState<Partial<CustomTenantDto>>({}) const [formData, setFormData] = useState<Partial<CustomTenantDto>>({})
const navigate = useNavigate() const navigate = useNavigate()
const { translate } = useLocalization()
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
@ -73,7 +75,8 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
<div className="w-full lg:w-1/3 bg-white rounded-xl shadow-lg border border-gray-200 p-6"> <div className="w-full lg:w-1/3 bg-white rounded-xl shadow-lg border border-gray-200 p-6">
<div className="mb-6"> <div className="mb-6">
<h2 className="text-lg font-semibold mb-4 flex items-center"> <h2 className="text-lg font-semibold mb-4 flex items-center">
<FaUser className="w-5 h-5 text-green-600 mr-2" /> Müşteri Bilgileri <FaUser className="w-5 h-5 text-green-600 mr-2" />{' '}
{translate('::Public.products.tenantForm.customerInfo')}
</h2> </h2>
</div> </div>
@ -87,8 +90,12 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
: 'border-gray-200 hover:border-blue-300' : 'border-gray-200 hover:border-blue-300'
}`} }`}
> >
<div className="font-semibold text-gray-900 mb-2">Mevcut Müşteri</div> <div className="font-semibold text-gray-900 mb-2">
<div className="text-sm text-gray-600">Kurum kodunuz ile giriş yapın</div> {translate('::Public.products.tenantForm.existing.title')}
</div>
<div className="text-sm text-gray-600">
{translate('::Public.products.tenantForm.existing.desc')}
</div>
</button> </button>
<button <button
@ -99,8 +106,12 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
: 'border-gray-200 hover:border-blue-300' : 'border-gray-200 hover:border-blue-300'
}`} }`}
> >
<div className="font-semibold text-gray-900 mb-2">Yeni Müşteri</div> <div className="font-semibold text-gray-900 mb-2">
<div className="text-sm text-gray-600">Kayıt olun ve hemen başlayın</div> {translate('::Public.products.tenantForm.new.title')}
</div>
<div className="text-sm text-gray-600">
{translate('::Public.products.tenantForm.new.desc')}
</div>
</button> </button>
</div> </div>
</div> </div>
@ -111,7 +122,9 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
{isExisting ? ( {isExisting ? (
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2">Kurum Kodu *</label> <label className="block text-sm font-medium text-gray-700 mb-2">
{translate('::Public.products.tenantForm.orgCode')}
</label>
<div className="relative flex flex-col sm:flex-row sm:items-stretch"> <div className="relative flex flex-col sm:flex-row sm:items-stretch">
<div className="relative w-full"> <div className="relative w-full">
<FaBuilding className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" /> <FaBuilding className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
@ -134,10 +147,10 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
<button <button
type="button" type="button"
onClick={getTenantInfo} onClick={getTenantInfo}
className="flex items-center justify-center gap-2 px-4 py-3 bg-blue-600 text-white rounded-lg sm:rounded-r-lg sm:rounded-l-none hover:bg-blue-700 transition-colors min-w-[148px]" className="flex items-center justify-center gap-2 px-4 py-3 bg-blue-600 text-white rounded-lg sm:rounded-r-lg sm:rounded-l-none hover:bg-blue-700 transition-colors min-w-[180px]"
> >
<FaSearch className="w-4 h-4" /> <FaSearch className="w-4 h-4" />
Kurumu Bul {translate('::Public.products.tenantForm.searchOrg')}
</button> </button>
</div> </div>
@ -145,76 +158,100 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
<div className="grid grid-cols-1 gap-y-3 text-sm text-gray-700 p-3"> <div className="grid grid-cols-1 gap-y-3 text-sm text-gray-700 p-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaUser className="w-4 h-4 text-gray-500" /> <FaUser className="w-4 h-4 text-gray-500" />
<span className="font-medium">Kurucu:</span> <span className="font-medium">
{translate('::Public.products.tenantForm.founder')}
</span>
<span>{formData.founder}</span> <span>{formData.founder}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaBuilding className="w-4 h-4 text-gray-500" /> <FaBuilding className="w-4 h-4 text-gray-500" />
<span className="font-medium">Şirket:</span> <span className="font-medium">
{translate('::Public.products.tenantForm.company')}
</span>
<span>{formData.institutionName}</span> <span>{formData.institutionName}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaEnvelope className="w-4 h-4 text-gray-500" /> <FaEnvelope className="w-4 h-4 text-gray-500" />
<span className="font-medium">E-posta:</span> <span className="font-medium">
{translate('::Public.products.tenantForm.email')}:
</span>
<span>{formData.email}</span> <span>{formData.email}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaPhone className="w-4 h-4 text-gray-500" /> <FaPhone className="w-4 h-4 text-gray-500" />
<span className="font-medium">Mobile:</span> <span className="font-medium">
{translate('::Public.products.tenantForm.mobile')}:
</span>
<span>{formData.mobile}</span> <span>{formData.mobile}</span>
</div> </div>
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<FaMapPin className="w-4 h-4 text-gray-500 mt-0.5" /> <FaMapPin className="w-4 h-4 text-gray-500 mt-0.5" />
<div> <div>
<span className="font-medium">Adres:</span> <span className="font-medium">
{translate('::Public.products.tenantForm.address')}:
</span>
<div>{formData.address}</div> <div>{formData.address}</div>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaGlobe className="w-4 h-4 text-gray-500" /> <FaGlobe className="w-4 h-4 text-gray-500" />
<span className="font-medium">Ülke:</span> <span className="font-medium">
{translate('::Public.products.tenantForm.country')}:
</span>
<span>{formData.country}</span> <span>{formData.country}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaGlobe className="w-4 h-4 text-gray-500" /> <FaGlobe className="w-4 h-4 text-gray-500" />
<span className="font-medium">Şehir:</span> <span className="font-medium">
{translate('::Public.products.tenantForm.city')}:
</span>
<span>{formData.city}</span> <span>{formData.city}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaMap className="w-4 h-4 text-gray-500" /> <FaMap className="w-4 h-4 text-gray-500" />
<span className="font-medium">İlçe:</span> <span className="font-medium">
{translate('::Public.products.tenantForm.district')}:
</span>
<span>{formData.district}</span> <span>{formData.district}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaMapPin className="w-4 h-4 text-gray-500" /> <FaMapPin className="w-4 h-4 text-gray-500" />
<span className="font-medium">Posta Kodu:</span> <span className="font-medium">
{translate('::Public.products.tenantForm.postalCode')}:
</span>
<span>{formData.postalCode}</span> <span>{formData.postalCode}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaMoneyBillWave className="w-4 h-4 text-gray-500" /> <FaMoneyBillWave className="w-4 h-4 text-gray-500" />
<span className="font-medium">Vergi Dairesi:</span> <span className="font-medium">
{translate('::Public.products.tenantForm.taxOffice')}:
</span>
<span>{formData.taxOffice}</span> <span>{formData.taxOffice}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaDollarSign className="w-4 h-4 text-gray-500" /> <FaDollarSign className="w-4 h-4 text-gray-500" />
<span className="font-medium">Vergi No:</span> <span className="font-medium">
{translate('::Public.products.tenantForm.taxNumber')}:
</span>
<span>{formData.vknTckn}</span> <span>{formData.vknTckn}</span>
</div> </div>
{formData.reference && ( {formData.reference && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaUserPlus className="w-4 h-4 text-gray-500" /> <FaUserPlus className="w-4 h-4 text-gray-500" />
<span className="font-medium">Referans:</span> <span className="font-medium">
{translate('::Public.products.tenantForm.reference')}:
</span>
<span>{formData.reference}</span> <span>{formData.reference}</span>
</div> </div>
)} )}
@ -226,7 +263,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Şirket Adı * {translate('::Public.products.tenantForm.orgName')}
</label> </label>
<div className="relative"> <div className="relative">
<FaBuilding className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" /> <FaBuilding className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
@ -242,7 +279,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
</div> </div>
</div> </div>
<div> <div>
<label className="text-sm font-medium text-gray-700">Kurucu *</label> <label className="text-sm font-medium text-gray-700">{translate('::Public.products.tenantForm.founder')}</label>
<div className="relative"> <div className="relative">
<FaUser className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" /> <FaUser className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<input <input
@ -259,7 +296,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
<label className="text-sm font-medium text-gray-700">Telefon *</label> <label className="text-sm font-medium text-gray-700">{translate('::Public.products.tenantForm.phone')}</label>
<div className="relative"> <div className="relative">
<FaPhone className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" /> <FaPhone className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<input <input
@ -273,7 +310,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
</div> </div>
</div> </div>
<div> <div>
<label className="text-sm font-medium text-gray-700">E-posta *</label> <label className="text-sm font-medium text-gray-700">{translate('::Public.products.tenantForm.email')}</label>
<div className="relative"> <div className="relative">
<FaEnvelope className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" /> <FaEnvelope className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<input <input
@ -289,7 +326,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
</div> </div>
<div> <div>
<label className="text-sm font-medium text-gray-700">Adres *</label> <label className="text-sm font-medium text-gray-700">{translate('::Public.products.tenantForm.address')}</label>
<div className="relative"> <div className="relative">
<FaMapPin className="absolute left-3 top-3 text-gray-400 w-5 h-5" /> <FaMapPin className="absolute left-3 top-3 text-gray-400 w-5 h-5" />
<textarea <textarea
@ -305,7 +342,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
<label className="text-sm font-medium text-gray-700">Ülke</label> <label className="text-sm font-medium text-gray-700">{translate('::Public.products.tenantForm.country')}</label>
<div className="relative"> <div className="relative">
<FaGlobe className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" /> <FaGlobe className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<input <input
@ -318,7 +355,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
</div> </div>
</div> </div>
<div> <div>
<label className="text-sm font-medium text-gray-700">Şehir *</label> <label className="text-sm font-medium text-gray-700">{translate('::Public.products.tenantForm.city')}</label>
<div className="relative"> <div className="relative">
<FaGlobe className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" /> <FaGlobe className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<input <input
@ -335,7 +372,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
<label className="text-sm font-medium text-gray-700">İlçe *</label> <label className="text-sm font-medium text-gray-700">{translate('::Public.products.tenantForm.district')}</label>
<div className="relative"> <div className="relative">
<FaMapPin className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" /> <FaMapPin className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<input <input
@ -349,7 +386,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
</div> </div>
</div> </div>
<div> <div>
<label className="text-sm font-medium text-gray-700">Posta Kodu *</label> <label className="text-sm font-medium text-gray-700">{translate('::Public.products.tenantForm.postalCode')}</label>
<div className="relative"> <div className="relative">
<FaMapPin className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" /> <FaMapPin className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<input <input
@ -366,7 +403,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
<label className="text-sm font-medium text-gray-700">Vergi Dairesi *</label> <label className="text-sm font-medium text-gray-700">{translate('::Public.products.tenantForm.taxOffice')}</label>
<div className="relative"> <div className="relative">
<FaBuilding className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" /> <FaBuilding className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<input <input
@ -380,7 +417,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
</div> </div>
</div> </div>
<div> <div>
<label className="text-sm font-medium text-gray-700">Vergi No *</label> <label className="text-sm font-medium text-gray-700">{translate('::Public.products.tenantForm.taxNumber')}</label>
<div className="relative"> <div className="relative">
<FaDollarSign className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" /> <FaDollarSign className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<input <input
@ -396,12 +433,12 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
</div> </div>
<div> <div>
<label className="text-sm font-medium text-gray-700">Referans</label> <label className="text-sm font-medium text-gray-700">{translate('::Public.products.tenantForm.reference')}</label>
<div className="relative"> <div className="relative">
<FaUserPlus className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" /> <FaUserPlus className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<input <input
type="text" type="text"
placeholder="Referans kişi veya kodu" placeholder={translate('::Public.products.tenantForm.reference')}
value={formData.reference || ''} value={formData.reference || ''}
onChange={(e) => handleInputChange('reference', e.target.value)} onChange={(e) => handleInputChange('reference', e.target.value)}
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
@ -418,14 +455,14 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
className="flex items-center px-6 py-3 border border-gray-300 text-gray-700 rounded-lg" className="flex items-center px-6 py-3 border border-gray-300 text-gray-700 rounded-lg"
> >
<FaArrowLeft className="w-4 h-4 mr-2" /> <FaArrowLeft className="w-4 h-4 mr-2" />
Geri {translate('::Public.products.tenantForm.buttons.back')}
</button> </button>
<button <button
type="submit" type="submit"
className="flex items-center justify-center px-6 py-3 bg-green-600 text-white rounded-lg" className="flex items-center justify-center px-6 py-3 bg-green-600 text-white rounded-lg"
> >
<FaArrowRight className="w-5 h-5 mr-2" /> <FaArrowRight className="w-5 h-5 mr-2" />
Devam Et {translate('::Public.products.tenantForm.buttons.continue')}
</button> </button>
</div> </div>
</form> </form>

View file

@ -26,7 +26,7 @@ const _LanguageSelector = ({ className }: CommonProps) => {
}, [currentLang, languageList]) }, [currentLang, languageList])
const { translate } = useLocalization() const { translate } = useLocalization()
const selectedLanguage = ( const selectedLanguage = (
<div className={classNames(className, 'flex items-center')}> <div className={classNames(className, 'flex items-center')}>
{loading ? ( {loading ? (
@ -62,28 +62,26 @@ const _LanguageSelector = ({ className }: CommonProps) => {
} }
return ( return (
<Tooltip title={translate('::App.Languages.Language')}> <Dropdown renderTitle={selectedLanguage} placement="bottom-end">
<Dropdown renderTitle={selectedLanguage} placement="bottom-end"> {languageList?.map((lang) => (
{languageList?.map((lang) => ( <Dropdown.Item
<Dropdown.Item key={lang.cultureName}
key={lang.cultureName} className="justify-between"
className="justify-between" eventKey={lang.cultureName}
eventKey={lang.cultureName} onClick={() => onLanguageSelect(lang.cultureName)}
onClick={() => onLanguageSelect(lang.cultureName)} >
> <span className="flex items-center">
<span className="flex items-center"> <Avatar
<Avatar size={18}
size={18} shape="circle"
shape="circle" src={`/img/countries/${lang.cultureName ?? 'default'}.png`}
src={`/img/countries/${lang.cultureName ?? 'default'}.png`} />
/> <span className="ltr:ml-2 rtl:mr-2">{lang.displayName}</span>
<span className="ltr:ml-2 rtl:mr-2">{lang.displayName}</span> </span>
</span> {currentLang === lang.cultureName && <FaCheck className="text-emerald-500 text-lg" />}
{currentLang === lang.cultureName && <FaCheck className="text-emerald-500 text-lg" />} </Dropdown.Item>
</Dropdown.Item> ))}
))} </Dropdown>
</Dropdown>
</Tooltip>
) )
} }

View file

@ -1,10 +1,25 @@
const getBaseResources = (state, resourceNames) => { // Type definitions
interface LocalizationResource {
texts: Record<string, string>
baseResources: string[]
}
interface LocalizationState {
resources: Record<string, LocalizationResource>
}
interface LocalizationKey {
key: string
defaultValue?: string
}
const getBaseResources = (state: LocalizationState, resourceNames: string[]): Record<string, string> => {
if (!resourceNames.length) { if (!resourceNames.length) {
return {} return {}
} }
// [{}, {}, {}] // [{}, {}, {}]
const texts = resourceNames.map((resourceName) => { const texts = resourceNames.map((resourceName: string) => {
const resource = state.resources[resourceName] const resource = state.resources[resourceName]
return { return {
...resource.texts, ...resource.texts,
@ -15,8 +30,8 @@ const getBaseResources = (state, resourceNames) => {
return Object.assign({}, ...texts) return Object.assign({}, ...texts)
} }
export const setLocalization = (state) => { export const setLocalization = (state: LocalizationState): Record<string, Record<string, string>> => {
const values = {} const values: Record<string, Record<string, string>> = {}
Object.entries(state.resources).forEach(([key, value]) => { Object.entries(state.resources).forEach(([key, value]) => {
values[key] = { values[key] = {
...value.texts, ...value.texts,
@ -27,23 +42,40 @@ export const setLocalization = (state) => {
return values return values
} }
export const getLocalization = (texts, defaultResourceName, key, ...interpolateParams) => { // 🔑 Interpolation helper
if (!key) key = '' const interpolate = (text: string, params: Record<string, string | number>) => {
if (!params || typeof params !== 'object') return text
return Object.entries(params).reduce((acc, [key, value]) => {
const pattern = new RegExp(`{{\\s*${key}\\s*}}`, 'g')
return acc.replace(pattern, String(value))
}, text)
}
export const getLocalization = (
texts: Record<string, Record<string, string>>,
defaultResourceName: string | undefined,
key: string | LocalizationKey,
params?: Record<string, string | number>
): string => {
let keyString = ''
let defaultValue = '' let defaultValue = ''
if (typeof key !== 'string') { if (typeof key === 'string') {
defaultValue = key.defaultValue keyString = key || ''
key = key.key } else {
defaultValue = key.defaultValue || ''
keyString = key.key
} }
const keys = key.split('::') const keys = keyString.split('::')
const warn = (message) => { const warn = (message: string) => {
if (import.meta.env.DEV) console.warn(message) if (import.meta.env.DEV) console.warn(message)
} }
if (keys.length < 2) { if (keys.length < 2) {
warn('The localization source separator (::) not found.') warn('The localization source separator (::) not found.')
return defaultValue || key return defaultValue || keyString
} }
if (!texts) return defaultValue || keys[1] if (!texts) return defaultValue || keys[1]
@ -56,7 +88,6 @@ export const getLocalization = (texts, defaultResourceName, key, ...interpolateP
if (!sourceName) { if (!sourceName) {
warn('Localization source name is not specified and the defaultResourceName was not defined!') warn('Localization source name is not specified and the defaultResourceName was not defined!')
return defaultValue || sourceKey return defaultValue || sourceKey
} }
@ -71,11 +102,12 @@ export const getLocalization = (texts, defaultResourceName, key, ...interpolateP
return defaultValue || sourceKey return defaultValue || sourceKey
} }
//TODO: Interpolate
// interpolateParams = interpolateParams.filter((params) => params != null)
// if (localization) localization = interpolate(localization, interpolateParams)
if (typeof localization !== 'string') localization = '' if (typeof localization !== 'string') localization = ''
return localization || defaultValue || key // ✅ Interpolation using the helper function
if (localization && params && typeof params === 'object') {
localization = interpolate(localization, params)
}
return localization || defaultValue || keyString
} }

View file

@ -1,14 +1,26 @@
import { getLocalization } from '@/services/localization.service' import { getLocalization } from '@/services/localization.service'
import { useStoreState } from '@/store' import { useStoreState } from '@/store'
type TranslateParams = Record<string, string | number>
const useLocalization = () => { const useLocalization = () => {
const { texts, config } = useStoreState((state) => state.abpConfig) const { texts, config } = useStoreState((state) => state.abpConfig)
function translate(localizationKey: string, defaultResourceName?: string) { function translate(
localizationKey: string,
params?: TranslateParams,
defaultResourceName?: string
): string {
// Guard against undefined texts
if (!texts) {
return localizationKey
}
return getLocalization( return getLocalization(
texts, texts,
defaultResourceName ?? config?.localization.defaultResourceName, defaultResourceName ?? config?.localization?.defaultResourceName,
localizationKey, localizationKey,
params
) )
} }

View file

@ -216,12 +216,12 @@ function RolesPermission({
<div className="flex flex-col md:flex-row gap-4 mb-2"> <div className="flex flex-col md:flex-row gap-4 mb-2">
<div style={{ width: '30%' }}> <div style={{ width: '30%' }}>
<Checkbox name="all" checked={isAllSelected} onChange={onSelectAll}> <Checkbox name="all" checked={isAllSelected} onChange={onSelectAll}>
{translate('::SelectAllInAllTabs', 'AbpPermissionManagement')} {translate('AbpPermissionManagement::SelectAllInAllTabs')}
</Checkbox> </Checkbox>
</div> </div>
<div style={{ width: '70%' }}> <div style={{ width: '70%' }}>
<Checkbox name="group" checked={isAllSelectedForGroup} onChange={onSelectAll}> <Checkbox name="group" checked={isAllSelectedForGroup} onChange={onSelectAll}>
{translate('::SelectAllInThisTab', 'AbpPermissionManagement')} {translate('AbpPermissionManagement::SelectAllInThisTab')}
</Checkbox> </Checkbox>
</div> </div>
</div> </div>

View file

@ -226,12 +226,12 @@ function UsersPermission({
<div className="flex flex-col md:flex-row gap-4"> <div className="flex flex-col md:flex-row gap-4">
<div style={{ width: '30%' }}> <div style={{ width: '30%' }}>
<Checkbox name="all" checked={isAllSelected} onChange={onSelectAll}> <Checkbox name="all" checked={isAllSelected} onChange={onSelectAll}>
{translate('::SelectAllInAllTabs', 'AbpPermissionManagement')} {translate('AbpPermissionManagement::SelectAllInAllTabs')}
</Checkbox> </Checkbox>
</div> </div>
<div style={{ width: '70%' }}> <div style={{ width: '70%' }}>
<Checkbox name="group" checked={isAllSelectedForGroup} onChange={onSelectAll}> <Checkbox name="group" checked={isAllSelectedForGroup} onChange={onSelectAll}>
{translate('::SelectAllInThisTab', 'AbpPermissionManagement')} {translate('AbpPermissionManagement::SelectAllInThisTab')}
</Checkbox> </Checkbox>
</div> </div>
</div> </div>

View file

@ -123,19 +123,19 @@ const Blog = () => {
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<button <button
onClick={() => handleCategoryChange('')} onClick={() => handleCategoryChange('')}
className={`px-4 py-2 rounded-lg transition-colors ${ className={`px-4 py-2 bg-blue-100 text-blue-800 text-sm font-medium rounded-lg transition-colors ${
selectedCategory === '' selectedCategory === ''
? 'bg-blue-600 text-white' ? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`} }`}
> >
Tümü {translate('::App.Reports.Dashboard.All')}
</button> </button>
{categories.map((category) => ( {categories.map((category) => (
<button <button
key={category.id} key={category.id}
onClick={() => handleCategoryChange(category.id)} onClick={() => handleCategoryChange(category.id)}
className={`px-4 py-2 rounded-lg transition-colors ${ className={`px-4 py-2 bg-blue-100 text-blue-800 text-sm font-medium rounded-lg transition-colors ${
selectedCategory === category.id selectedCategory === category.id
? 'bg-blue-600 text-white' ? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'