Files
lab-app/lib/features/auth/welcome_pricing_screen.dart
T
2026-06-20 18:24:40 +03:00

1101 lines
35 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/providers/auth_provider.dart';
import '../../core/providers/locale_provider.dart';
import '../../core/router/app_router.dart';
import '../../core/theme/app_theme.dart';
import '../../core/widgets/tooth_logo.dart';
import 'auth_widgets.dart';
class WelcomePricingScreen extends ConsumerStatefulWidget {
const WelcomePricingScreen({super.key});
@override
ConsumerState<WelcomePricingScreen> createState() =>
_WelcomePricingScreenState();
}
class _WelcomePricingScreenState extends ConsumerState<WelcomePricingScreen> {
bool _yearly = false;
@override
Widget build(BuildContext context) {
final locale = ref.watch(localeProvider);
final copy = _WelcomeCopy.of(locale.languageCode);
final auth = ref.watch(authProvider);
final isDesktop = MediaQuery.sizeOf(context).width > 980;
return Scaffold(
backgroundColor: AppColors.background,
body: Stack(
children: [
if (isDesktop)
Row(
children: [
Expanded(child: _HeroPane(copy: copy, compact: false)),
Expanded(child: _ContentPane(copy: copy, yearly: _yearly)),
],
)
else
SafeArea(
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: _HeroPane(copy: copy, compact: true)),
SliverToBoxAdapter(
child: _ContentPane(copy: copy, yearly: _yearly),
),
],
),
),
Positioned(
top: MediaQuery.paddingOf(context).top + 12,
right: 12,
child: Wrap(
spacing: 8,
children: [
if (auth.isAuthenticated && auth.activeTenant != null)
OutlinedButton.icon(
onPressed: () => context.go(
auth.activeTenant!.tenant.isLab
? routeLabDashboard
: routeClinicDashboard,
),
icon: const Icon(Icons.arrow_back_rounded, size: 18),
label: Text(copy.backToApp),
style: OutlinedButton.styleFrom(
backgroundColor: Colors.white.withValues(alpha: 0.92),
foregroundColor: AppColors.textPrimary,
side: const BorderSide(color: AppColors.border),
),
),
_LanguageFab(locale: locale),
],
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: _BottomActionBar(
copy: copy,
onYearlyChanged: (value) => setState(() => _yearly = value),
yearly: _yearly,
),
),
],
),
);
}
}
class _HeroPane extends StatelessWidget {
const _HeroPane({required this.copy, required this.compact});
final _WelcomeCopy copy;
final bool compact;
@override
Widget build(BuildContext context) {
return Container(
constraints: BoxConstraints(minHeight: compact ? 360 : double.infinity),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF08111F), Color(0xFF0E325F), Color(0xFF13639C)],
),
),
child: Stack(
children: [
const AnimatedAuthBg(bright: true),
Padding(
padding: EdgeInsets.fromLTRB(
compact ? 24 : 48,
compact ? 32 : 64,
compact ? 24 : 48,
compact ? 36 : 64,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: compact ? 60 : 72,
height: compact ? 60 : 72,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.14),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withValues(alpha: 0.18),
),
),
child: Center(
child: ToothLogo(
size: compact ? 30 : 36,
color: Colors.white,
),
),
),
const SizedBox(height: 24),
Text(
'DLS',
style: TextStyle(
color: Colors.white,
fontSize: compact ? 34 : 48,
fontWeight: FontWeight.w900,
letterSpacing: 1.4,
),
),
const SizedBox(height: 12),
Text(
copy.heroTitle,
style: TextStyle(
color: Colors.white,
fontSize: compact ? 28 : 42,
fontWeight: FontWeight.w800,
height: 1.08,
letterSpacing: -0.8,
),
),
const SizedBox(height: 14),
Text(
copy.heroSubtitle,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.72),
fontSize: compact ? 14 : 17,
height: 1.6,
),
),
const SizedBox(height: 28),
Wrap(
spacing: 12,
runSpacing: 12,
children: [
_HeroMetric(
label: copy.metricClinics,
value: copy.metricClinicsValue,
),
_HeroMetric(
label: copy.metricSpeed,
value: copy.metricSpeedValue,
),
_HeroMetric(
label: copy.metricAi,
value: copy.metricAiValue,
),
],
),
const SizedBox(height: 28),
...copy.heroBullets.map(
(item) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: const EdgeInsets.only(top: 2),
width: 22,
height: 22,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.14),
borderRadius: BorderRadius.circular(999),
),
child: const Icon(
Icons.check_rounded,
size: 14,
color: Colors.white,
),
),
const SizedBox(width: 10),
Expanded(
child: Text(
item,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.92),
fontSize: compact ? 14 : 15,
height: 1.45,
),
),
),
],
),
),
),
],
),
),
],
),
);
}
}
class _ContentPane extends StatelessWidget {
const _ContentPane({required this.copy, required this.yearly});
final _WelcomeCopy copy;
final bool yearly;
@override
Widget build(BuildContext context) {
final plans = copy.buildPlans(yearly);
return Padding(
padding: EdgeInsets.fromLTRB(
24,
24,
24,
MediaQuery.of(context).padding.bottom + 110,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
copy.packageEyebrow,
style: const TextStyle(
color: AppColors.accent,
fontWeight: FontWeight.w700,
fontSize: 13,
),
),
const SizedBox(height: 8),
Text(
copy.packageTitle,
style: const TextStyle(
fontSize: 30,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
letterSpacing: -0.7,
),
),
const SizedBox(height: 10),
Text(
copy.packageSubtitle,
style: const TextStyle(
fontSize: 15,
color: AppColors.textSecondary,
height: 1.6,
),
),
const SizedBox(height: 24),
LayoutBuilder(
builder: (context, constraints) {
final wide = constraints.maxWidth > 720;
return Wrap(
spacing: 16,
runSpacing: 16,
children: plans
.map(
(plan) => SizedBox(
width: wide ? (constraints.maxWidth - 16) / 2 : null,
child: _PlanCard(plan: plan, yearly: yearly),
),
)
.toList(),
);
},
),
const SizedBox(height: 28),
_FreeUsageCard(copy: copy),
],
),
);
}
}
class _BottomActionBar extends StatelessWidget {
const _BottomActionBar({
required this.copy,
required this.yearly,
required this.onYearlyChanged,
});
final _WelcomeCopy copy;
final bool yearly;
final ValueChanged<bool> onYearlyChanged;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.fromLTRB(
16,
14,
16,
MediaQuery.paddingOf(context).bottom + 14,
),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.96),
border: const Border(top: BorderSide(color: AppColors.border)),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 18,
offset: const Offset(0, -8),
),
],
),
child: SafeArea(
top: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Expanded(
child: Container(
height: 48,
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppColors.border),
),
child: Row(
children: [
Expanded(
child: _BillingOptionButton(
label: copy.monthly,
selected: !yearly,
onTap: () => onYearlyChanged(false),
),
),
Expanded(
child: _BillingOptionButton(
label: copy.yearly,
caption: copy.yearlyDiscount,
selected: yearly,
onTap: () => onYearlyChanged(true),
),
),
],
),
),
),
const SizedBox(width: 12),
Expanded(
child: FilledButton(
onPressed: () => context.go(routeSignUp),
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(48),
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
child: Text(copy.startNow),
),
),
const SizedBox(width: 12),
OutlinedButton(
onPressed: () => context.go(routeSignIn),
style: OutlinedButton.styleFrom(
minimumSize: const Size(112, 48),
foregroundColor: AppColors.textPrimary,
side: const BorderSide(color: AppColors.border),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
child: Text(copy.signIn),
),
],
),
],
),
),
);
}
}
class _BillingOptionButton extends StatelessWidget {
const _BillingOptionButton({
required this.label,
required this.selected,
required this.onTap,
this.caption,
});
final String label;
final String? caption;
final bool selected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return InkWell(
borderRadius: BorderRadius.circular(10),
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
decoration: BoxDecoration(
color: selected ? Colors.white : Colors.transparent,
borderRadius: BorderRadius.circular(10),
boxShadow: selected
? [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
]
: null,
),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
label,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color:
selected ? AppColors.textPrimary : AppColors.textSecondary,
),
),
if (caption != null)
Text(
caption!,
style: const TextStyle(
fontSize: 11,
color: AppColors.success,
fontWeight: FontWeight.w700,
),
),
],
),
),
);
}
}
class _PlanCard extends StatelessWidget {
const _PlanCard({required this.plan, required this.yearly});
final _PlanViewModel plan;
final bool yearly;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: plan.highlighted ? const Color(0xFFF8FBFF) : Colors.white,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: plan.highlighted ? const Color(0xFF93C5FD) : AppColors.border,
width: plan.highlighted ? 1.4 : 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 18,
offset: const Offset(0, 10),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
plan.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 4),
Text(
plan.subtitle,
style: const TextStyle(
fontSize: 13,
color: AppColors.textSecondary,
height: 1.45,
),
),
],
),
),
if (plan.badge != null)
Container(
padding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFFE0F2FE),
borderRadius: BorderRadius.circular(999),
),
child: Text(
plan.badge!,
style: const TextStyle(
color: AppColors.accent,
fontWeight: FontWeight.w700,
fontSize: 12,
),
),
),
],
),
const SizedBox(height: 18),
RichText(
text: TextSpan(
children: [
TextSpan(
text: plan.price,
style: const TextStyle(
fontSize: 34,
fontWeight: FontWeight.w900,
color: AppColors.textPrimary,
letterSpacing: -1,
),
),
TextSpan(
text: plan.period,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.textSecondary,
),
),
],
),
),
const SizedBox(height: 10),
Text(
yearly ? plan.yearlyNote : plan.monthlyNote,
style: const TextStyle(
fontSize: 12,
color: AppColors.textSecondary,
),
),
const SizedBox(height: 18),
_FeatureRow(
icon: Icons.auto_awesome_rounded,
text: plan.aiCredits,
emphasized: true,
),
const SizedBox(height: 8),
...plan.features.map(
(feature) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: _FeatureRow(
icon: Icons.check_circle_outline_rounded,
text: feature,
),
),
),
const SizedBox(height: 14),
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: () => context.go(routeSignUp),
style: OutlinedButton.styleFrom(
minimumSize: const Size.fromHeight(46),
foregroundColor: plan.highlighted
? AppColors.primary
: AppColors.textPrimary,
side: BorderSide(
color:
plan.highlighted ? AppColors.primary : AppColors.border,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
child: Text(plan.cta),
),
),
],
),
);
}
}
class _FeatureRow extends StatelessWidget {
const _FeatureRow({
required this.icon,
required this.text,
this.emphasized = false,
});
final IconData icon;
final String text;
final bool emphasized;
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
icon,
size: 18,
color: emphasized ? const Color(0xFF7C3AED) : AppColors.success,
),
const SizedBox(width: 8),
Expanded(
child: Text(
text,
style: TextStyle(
fontSize: 13,
fontWeight: emphasized ? FontWeight.w700 : FontWeight.w500,
color:
emphasized ? AppColors.textPrimary : AppColors.textSecondary,
height: 1.45,
),
),
),
],
);
}
}
class _FreeUsageCard extends StatelessWidget {
const _FreeUsageCard({required this.copy});
final _WelcomeCopy copy;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFFF8FAFC),
borderRadius: BorderRadius.circular(24),
border: Border.all(color: AppColors.border),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
copy.freeUsageTitle,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 8),
Text(
copy.freeUsageSubtitle,
style: const TextStyle(
fontSize: 14,
color: AppColors.textSecondary,
height: 1.55,
),
),
const SizedBox(height: 16),
...copy.freeUsageBullets.map(
(item) => Padding(
padding: const EdgeInsets.only(bottom: 10),
child: _FeatureRow(
icon: Icons.lightbulb_outline_rounded,
text: item,
),
),
),
],
),
);
}
}
class _HeroMetric extends StatelessWidget {
const _HeroMetric({required this.label, required this.value});
final String label;
final String value;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: Colors.white.withValues(alpha: 0.12)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
value,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.72),
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
}
class _LanguageFab extends ConsumerWidget {
const _LanguageFab({required this.locale});
final Locale locale;
@override
Widget build(BuildContext context, WidgetRef ref) {
const options = [
('tr', 'TR'),
('en', 'EN'),
('de', 'DE'),
('ru', 'RU'),
('ar', 'AR'),
];
return PopupMenuButton<String>(
tooltip: 'Language',
onSelected: (value) =>
ref.read(localeProvider.notifier).setLocale(Locale(value)),
itemBuilder: (_) => options
.map(
(option) => PopupMenuItem<String>(
value: option.$1,
child: Row(
children: [
Expanded(child: Text(option.$2)),
if (locale.languageCode == option.$1)
const Icon(Icons.check_rounded, size: 16),
],
),
),
)
.toList(),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.92),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppColors.border),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.language_rounded, size: 18),
const SizedBox(width: 8),
Text(
locale.languageCode.toUpperCase(),
style: const TextStyle(fontWeight: FontWeight.w700),
),
],
),
),
);
}
}
class _PlanViewModel {
const _PlanViewModel({
required this.name,
required this.subtitle,
required this.price,
required this.period,
required this.monthlyNote,
required this.yearlyNote,
required this.aiCredits,
required this.features,
required this.cta,
this.badge,
this.highlighted = false,
});
final String name;
final String subtitle;
final String price;
final String period;
final String monthlyNote;
final String yearlyNote;
final String aiCredits;
final List<String> features;
final String cta;
final String? badge;
final bool highlighted;
}
class _WelcomeCopy {
const _WelcomeCopy({
required this.heroTitle,
required this.heroSubtitle,
required this.heroBullets,
required this.metricClinics,
required this.metricClinicsValue,
required this.metricSpeed,
required this.metricSpeedValue,
required this.metricAi,
required this.metricAiValue,
required this.packageEyebrow,
required this.packageTitle,
required this.packageSubtitle,
required this.monthly,
required this.yearly,
required this.yearlyDiscount,
required this.startNow,
required this.signIn,
required this.backToApp,
required this.freeUsageTitle,
required this.freeUsageSubtitle,
required this.freeUsageBullets,
required this.planFree,
required this.planFreeSub,
required this.planStarter,
required this.planStarterSub,
required this.planPro,
required this.planProSub,
required this.planEnterprise,
required this.planEnterpriseSub,
required this.freeCta,
required this.paidCta,
required this.enterpriseCta,
required this.recommendedBadge,
required this.enterpriseBadge,
required this.perMonth,
required this.perYear,
required this.customPrice,
required this.noCardTrial,
required this.annualPrepay,
required this.customContact,
});
final String heroTitle;
final String heroSubtitle;
final List<String> heroBullets;
final String metricClinics;
final String metricClinicsValue;
final String metricSpeed;
final String metricSpeedValue;
final String metricAi;
final String metricAiValue;
final String packageEyebrow;
final String packageTitle;
final String packageSubtitle;
final String monthly;
final String yearly;
final String yearlyDiscount;
final String startNow;
final String signIn;
final String backToApp;
final String freeUsageTitle;
final String freeUsageSubtitle;
final List<String> freeUsageBullets;
final String planFree;
final String planFreeSub;
final String planStarter;
final String planStarterSub;
final String planPro;
final String planProSub;
final String planEnterprise;
final String planEnterpriseSub;
final String freeCta;
final String paidCta;
final String enterpriseCta;
final String recommendedBadge;
final String enterpriseBadge;
final String perMonth;
final String perYear;
final String customPrice;
final String noCardTrial;
final String annualPrepay;
final String customContact;
static _WelcomeCopy of(String code) {
switch (code) {
case 'tr':
return const _WelcomeCopy(
heroTitle: 'Klinik ve laboratuvar operasyonunu tek akışta yönetin.',
heroSubtitle:
'İş takibi, finans, bağlantılar ve AI destekli operasyon yardımı tek uygulamada birleşir.',
heroBullets: [
'Klinik ve laboratuvar arasında iş akışı ve teslim süreçleri aynı dilde ilerler.',
'Hasta, ürün, fiyat ve onay süreçleri gerçek saha kullanımına göre ölçeklenir.',
'AI özellikleri kredi bazlı ilerleyecek şekilde ürünleştirilmeye hazırdır.',
],
metricClinics: 'Kurulum',
metricClinicsValue: 'Aynı gün',
metricSpeed: 'Odak',
metricSpeedValue: 'Operasyon + finans',
metricAi: 'AI yaklaşımı',
metricAiValue: 'Kredi bazlı',
packageEyebrow: 'Paketler',
packageTitle: 'Trial ve paket yapısını şimdiden net gösterelim.',
packageSubtitle:
'Bu ekran şimdilik tanıtım ve yönlendirme amaçlıdır. Paketler aylık ve yıllık olarak ayrıldı; AI kredi mantığı da görünür halde.',
monthly: 'Aylık',
yearly: 'Yıllık',
yearlyDiscount: '%20 avantaj',
startNow: 'Hemen Başla',
signIn: 'Giriş Yap',
backToApp: 'Uygulamaya dön',
freeUsageTitle: 'Ücretsiz kullanım için önerilen model',
freeUsageSubtitle:
'Monetization başlamadan önce ücretsiz katmanı ürün denemesi için güçlü ama kontrollü tutmak en sağlıklı yapı olur.',
freeUsageBullets: [
'Süresiz ücretsiz plan: 1 tenant, sınırlı ekip üyesi ve aylık düşük AI kredi.',
'Kart istemeyen 14 günlük Pro deneme: kullanıcı ilk değeri hızlı görür, bariyer düşer.',
'Referans veya ilk aktivasyon sonrası bonus kredi: AI özelliğini tattırır.',
],
planFree: 'Free',
planFreeSub: 'Temel operasyonu görmek isteyenler için',
planStarter: 'Starter',
planStarterSub: 'Yeni başlayan klinik ve laboratuvarlar için',
planPro: 'Pro',
planProSub: 'Düzenli iş trafiği ve ekip yönetimi için',
planEnterprise: 'Enterprise',
planEnterpriseSub: 'Çoklu şube, operasyon ve özel süreçler için',
freeCta: 'Ücretsiz başla',
paidCta: 'Denemeyi başlat',
enterpriseCta: 'Görüşme planla',
recommendedBadge: 'Önerilen',
enterpriseBadge: 'Kurumsal',
perMonth: '/ ay',
perYear: '/ yıl',
customPrice: 'Özel',
noCardTrial: 'Kart gerektirmeyen başlangıç',
annualPrepay: 'Yıllık peşin ödeme ile daha avantajlı',
customContact: 'Özel AI kredi ve tenant yapısı planlanır',
);
default:
return const _WelcomeCopy(
heroTitle: 'Run clinic and lab operations in one shared workflow.',
heroSubtitle:
'Jobs, finance, connections, and AI-assisted operations live in the same product surface.',
heroBullets: [
'Clinics and labs follow the same production language and handoff flow.',
'Patients, products, pricing, and approvals scale with real field usage.',
'AI features are ready to evolve into a credit-based monetization layer.',
],
metricClinics: 'Setup',
metricClinicsValue: 'Same day',
metricSpeed: 'Focus',
metricSpeedValue: 'Ops + finance',
metricAi: 'AI model',
metricAiValue: 'Credit-based',
packageEyebrow: 'Plans',
packageTitle:
'Make trial and package structure visible from day one.',
packageSubtitle:
'This screen is intentionally promotional for now. Plans are split monthly and yearly, and AI credits are visible for future packaging.',
monthly: 'Monthly',
yearly: 'Yearly',
yearlyDiscount: 'Save 20%',
startNow: 'Get Started',
signIn: 'Sign In',
backToApp: 'Back to app',
freeUsageTitle: 'Recommended free usage policy',
freeUsageSubtitle:
'Before full monetization, a controlled free tier is the healthiest way to drive adoption without hurting conversion.',
freeUsageBullets: [
'Free forever tier with 1 tenant, limited team seats, and low monthly AI credits.',
'14-day Pro trial without requiring a card to reduce onboarding friction.',
'Bonus credits after referral or first activation to showcase AI value.',
],
planFree: 'Free',
planFreeSub: 'For teams exploring the product',
planStarter: 'Starter',
planStarterSub: 'For early-stage clinics and labs',
planPro: 'Pro',
planProSub: 'For active teams with recurring job volume',
planEnterprise: 'Enterprise',
planEnterpriseSub: 'For multi-branch and custom operations',
freeCta: 'Start free',
paidCta: 'Start trial',
enterpriseCta: 'Book a demo',
recommendedBadge: 'Recommended',
enterpriseBadge: 'Enterprise',
perMonth: '/ mo',
perYear: '/ yr',
customPrice: 'Custom',
noCardTrial: 'No-card onboarding',
annualPrepay: 'Lower effective cost with annual billing',
customContact: 'Custom AI credits and tenant structure',
);
}
}
List<_PlanViewModel> buildPlans(bool yearly) => [
_PlanViewModel(
name: planFree,
subtitle: planFreeSub,
price: yearly ? '0₺' : '0₺',
period: yearly ? perYear : perMonth,
monthlyNote: noCardTrial,
yearlyNote: noCardTrial,
aiCredits: yearly ? '300 AI kredi / yıl' : '25 AI kredi / ay',
features: const [
'1 tenant / temel ekip kullanımı',
'İş ve hasta akışını deneme',
'Sınırlı rapor ve temel finans görünümü',
],
cta: freeCta,
),
_PlanViewModel(
name: planStarter,
subtitle: planStarterSub,
price: yearly ? '7.680₺' : '800₺',
period: yearly ? perYear : perMonth,
monthlyNote: noCardTrial,
yearlyNote: annualPrepay,
aiCredits: yearly ? '3.600 AI kredi / yıl' : '300 AI kredi / ay',
features: const [
'Çoklu kullanıcı ve temel tenant yönetimi',
'İş akışı, ürün ve bağlantı yönetimi',
'Temel AI yardımcı deneyimi',
],
cta: paidCta,
),
_PlanViewModel(
name: planPro,
subtitle: planProSub,
price: yearly ? '17.280₺' : '1.800₺',
period: yearly ? perYear : perMonth,
monthlyNote: noCardTrial,
yearlyNote: annualPrepay,
aiCredits: yearly ? '14.400 AI kredi / yıl' : '1.200 AI kredi / ay',
features: const [
'Gelişmiş finans ve fiyatlandırma görünürlüğü',
'Daha yüksek ekip ve tenant esnekliği',
'Öncelikli AI kullanım ve operasyon desteği',
],
cta: paidCta,
badge: recommendedBadge,
highlighted: true,
),
_PlanViewModel(
name: planEnterprise,
subtitle: planEnterpriseSub,
price: customPrice,
period: '',
monthlyNote: customContact,
yearlyNote: customContact,
aiCredits: 'Özel AI kredi havuzu ve kurallar',
features: const [
'Super admin, çoklu tenant ve özel onboarding',
'Saha sürecine göre özelleşen workflow yapısı',
'Kurumsal SLA, entegrasyon ve destek',
],
cta: enterpriseCta,
badge: enterpriseBadge,
),
];
}