Skip to content

Admin Yönetim Paneli (Admin Console)

Kurs moderasyonu, finansal yönetim, ödeme/iade, fiyat katmanları (admin.price-tiers), KYC onayları ve çoklu ödeme yöntemi desteği.

Achidemy’de Admin rolüne sahip kullanıcılar, Admin Console üzerinden kurs onaylama/reddetme, platform geliri ve bekleyen ödemeleri yönetme, iade (refund) takibi, KYC (kimlik doğrulama) onayları ve fiyat katmanları (tier) güncellemesi yapar. Bu sayfa kullanıcı ve kurs moderasyonu, finansal yönetim (toplam gelir, bekleyen ödemeler, iade), KYC onay paneli ve admin.price-tiers.tsx ile global fiyat güncellemelerini açıklar.

  • Yetki: Tüm admin route’larında session.user.role === "admin" kontrolü yapılır; değilse 401 veya 404.
  • Layout: app/routes/admin.tsx — sidebar (Kurslar, Kullanıcılar, Finans, Ödemeler, İadeler, Fiyat Katmanları, Abonelik, KYC Onayları, Şikayetler (DMCA) vb.) ve bildirim kutusu (Son Hareketler / Tüm Bildirimler).
  • Dashboard: admin._index.tsxgetAdminStats(db) ile özet metrikler (kullanıcı sayıları, onay bekleyen kurs, gelir özeti).

Dosya: app/routes/admin.courses.tsx

Loader:

  • Tüm kurslar eğitmen bilgisiyle sayfalanır (courses + users join); alanlar: id, title, status, slug, price, thumbnailUrl, instructor.name, instructor.email.
  • status değerleri: draft, pending_review, published, archived.

Liste:

  • Her satırda kurs başlığı, eğitmen adı/e-posta, durum badge’i (İnceleme Bekliyor / Yayında / Taslak), fiyat, thumbnail.
  • İnceleme Bekliyor (pending_review): “İncele” ve “Onayla” / “Reddet” aksiyonları.

Karar mekanizması:

  • Onayla: GraphQL updateCourse(id, status: "published") → kurs yayına alınır.
  • Reddet: GraphQL updateCourse(id, status: "draft") → kurs taslağa çekilir.
  • İşlem sonrası toast ve revalidate() ile liste güncellenir.

Önizleme: “İncele” ile admin.course.$slug.preview sayfasına gidilir.

Dosya: app/routes/admin.course.$slug.preview.tsx

URL: /admin/course/:slug/preview

Loader:

  • Admin oturumu kontrolü; getCourseBySlug(db, params.slug) ile kurs; getCourseCurriculum(db, course.id) ile müfredat.
  • Toplam süre ve ders sayısı müfredattan hesaplanır; bölgesel fiyat getRegionalPrice(db, course.priceTierId, country) ile alınır.

Sayfa: Kurs başlığı, açıklama, öğrenme çıktıları, müfredat (bölüm/ders listesi, video hazır bilgisi), fiyat. “Denetim Listesine Dön” ile /admin/courses’a dönülür; onay/red işlemi kurs listesi sayfasındaki butonlarla yapılır.

AI Kalite Ön İncelemesi: Sayfada AICourseReviewer bileşeni (app/components/admin/AICourseReviewer.tsx) bulunur. “Otomatik Tara” butonu ile Cloudflare AI (Llama 3.1 8B) kursun başlığını, açıklamasını, müfredatını, fiyatını ve eksik asset’leri (kapak, tanıtım videosu) analiz eder; kalite skoru, küfür tespiti ve eylem tavsiyesi sunar. Detay için Yapay Zeka Destekli Moderasyon sayfasına bakın.

  • Dosya: app/routes/admin.reports.tsx
  • URL: /admin/reports

Özellikler:

  • course_reports tablosundaki kayıtlar kurs bazlı gruplanır; her kurs için tek kart gösterilir.
  • Her kartta:
    • Kurs başlığı ve eğitmen bilgisi
    • totalReports — ilgili kurs için açılmış toplam şikayet sayısı
    • pendingReports — durumu pending olan açık şikayet sayısı
    • Son şikayetin özeti (neden, tarih)
  • Sıralama:
    • Önce pendingReports (azalan),
    • sonra totalReports (azalan),
    • ardından en yeni şikayet tarihi (azalan).
  • Her kartta:
    • /admin/reports/:courseId detay sayfasına giden “İncele” butonu
    • Alt tabloda yer alan bireysel şikayet satırları için durum dropdown’u (pending, reviewed, resolved, rejected) ve useFetcher ile anlık güncelleme.

Bu sayfa, en çok şikayet alan kursları en üstte göstererek admin’in önceliklendirme yapmasını sağlar.

  • Dosya: app/routes/admin.reports.$courseId.tsx
  • URL: /admin/reports/:courseId

Loader:

  • İlgili kurs bilgileri (courses + users join):
    • id, title, slug, status, isMarketplaceVisible
    • Eğitmen adı ve e‑posta
  • Kursa ait tüm course_reports kayıtları:
    • reason, description, status, createdAt, raporu gönderen kullanıcı bilgileri

Sayfa:

  • Üst bölümde kurs başlığı, mevcut durum, pazarda görünürlük (isMarketplaceVisible) ve açık şikayet sayısı gösterilir.
  • Alt bölümde her bir şikayet kartı:
    • Şikayet nedeni ve açıklama
    • Şikayet sahibi (ad/e‑posta)
    • Oluşturulma tarihi
    • Durum badge’i (pending, reviewed, resolved, rejected)

Action:

  • Intent: mark_under_review
  • İşlemler:
    • Kurs:
      • status = "pending_review"
      • isMarketplaceVisible = false (kurs pazardan kaldırılır)
    • İlgili kurs için durumu pending olan tüm course_reports kayıtları reviewed yapılır.
    • Eğitmene, bildirim sistemi üzerinden (createNotification) kursun şikayet/DMCA nedeniyle inceleme altına alındığını ve geçici olarak yayından kaldırıldığını anlatan bir mesaj gönderilir. Bildirim dili, getNotificationLangForUser ile kullanıcının ülkesine göre seçilir.

AI Şikayet Analizi: Sayfada Achidemy AI Moderatör (AIReportAnalyzer) paneli bulunur. “Tüm Şikayetleri Analiz Et” butonu ile Cloudflare AI (Llama 3.1 8B) tüm şikayetleri özetler; ortak nedenler, çıkarılan linkler ve kritiklik skoru üretir. Detay için Yapay Zeka Destekli Moderasyon sayfasına bakın.

Bu akış, platformun DMCA / Safe Harbor yükümlülükleri kapsamında, şikayetlerin kayıt altına alınması, incelenmesi ve kursun gerekirse pazardan kaldırılması için standart bir süreç sunar.

Dosya: app/routes/admin.finances.tsx

Loader: getAdminStats(db) ve getAdminSystemStats(db) (app/lib/db-queries.ts).

getAdminStats: earnings tablosundan (status ≠ refunded) satış türüne göre ayrıştırma:

  • Brüt satış geliri, organik/direkt/affiliate satış sayıları ve payları.
  • Abonelik: brüt abonelik geliri, platform payı (%60), eğitmen havuzu (%40).
  • Toplam iade tutarı, net platform karı (totalRevenue) vb.

getAdminSystemStats:

  • Tamamlanan öğretmen ödemeleri toplamı (payout_requests.status = 'completed').
  • Bekleyen ödemeler toplamı (payout_requests.status = 'pending').

Sayfa kartları (örnek):

  • Net Platform Karı (totalRevenue).
  • Organik Satış Payı (purchaseRevenue, %55 komisyon).
  • Bekleyen Ödemeler (system.pendingPayoutAmount).
  • Toplam İade Tutarı (totalRefundedAmount).
  • Affiliate satış payı ve affiliate komisyonu.
  • Abonelik ekonomisi: brüt gelir, platform %60, eğitmen havuzu %40.
  • “Gelir Havuzunu Dağıt” butonu → adminDistributeSubscriptionEarnings(totalPool) mutation (abonelik havuzunu eğitmenlere dağıtım).

Abonelik Analiz Merkezi ve Havuz Dağıtımı

Section titled “Abonelik Analiz Merkezi ve Havuz Dağıtımı”

Abonelik ekonomisi için ayrı bir analiz merkezi ve havuz dağıtım mantığı vardır:

  • Dosya: app/lib/db-queries.tsgetAdminSubscriptionStats(db)
    • Aktif abone sayısı, aylık abonelik fiyatı, brüt abonelik geliri, platform payı (%60), eğitmen havuzu (%40).
    • Son 6 ay için aylık trend (gross/platform/instructorPool).
  • Dosya: app/lib/payout-engine.tsdistributeMonthlySubscriptionPool(db, month, year, monthlySubscriptionPrice)
    • Abonelik gelirlerinin %40’lık eğitmen havuzunu, subscription_watch_logs tablosundaki izlenme sürelerine göre eğitmenlere dağıtır.
    • Her eğitmen için havuzdan gelen payı earnings tablosuna saleType = "subscription_pool_share" ile yazar (cent cinsinden).

Admin tarafında bu bilgiler:

  • Finans kartları ile özetlenir (brüt abonelik geliri, platform payı, eğitmen havuzu).
  • Abonelik analitik sayfasında aylık trend grafikleri olarak gösterilir.
  • “Havuzu Dağıt” aksiyonu ile ilgili ay için distributeMonthlySubscriptionPool tetiklenir.

KYC Onay Paneli (Eğitmen Kimlik Doğrulama)

Section titled “KYC Onay Paneli (Eğitmen Kimlik Doğrulama)”

Liste Sayfası: app/routes/admin.kyc-requests.tsx/admin/kyc-requests

Detay Sayfası: app/routes/admin.kyc.$userId.preview.tsx/admin/kyc/:userId/preview

Amaç: Payoneer veya Cenoa ödeme yöntemi seçen eğitmenlerin KYC (Know Your Customer) başvurularını incelemek ve onaylamak/reddetmek.

  • İstatistik kartları: Bekleyen, onaylanan, reddedilen başvuru sayıları
  • Filtre sekmeleri: Tümü / Bekleyen / Onaylanan / Reddedilen
  • Tablo: Her başvuru için yasal ad, e-posta, ülke, ödeme yöntemi (logo ile), durum badge’i, tarih
  • “İncele” butonu: Detay sayfasına yönlendirir
<div className="flex items-center gap-2">
{req.payoutMethod === "payoneer" ? (
<img src="/images/payoneer.png" alt="Payoneer" className="h-4 w-auto object-contain" />
) : req.payoutMethod === "cenoa" ? (
<img src="/images/cenoa.png" alt="Cenoa" className="h-4 w-auto object-contain" />
) : req.payoutMethod === "stripe_connect" ? (
<img src="/images/stripe.webp" alt="Stripe" className="h-4 w-auto object-contain" />
) : (
<span className="text-xs font-bold text-slate-600 uppercase">{req.payoutMethod || "-"}</span>
)}
</div>
  • Eğitmen bilgileri: Ad, e-posta, ülke
  • Kişisel bilgiler: Yasal ad, vergi/TC no
  • Ödeme bilgileri: Payoneer e-posta veya Cenoa telefon numarası
  • Kimlik belgeleri: Ön ve arka yüz görüntüleme (modal ile)
  • Onay/Red butonları: Sağ tarafta sabit konum

Action (Form POST):

intentAçıklama
approvekycStatus = "approved", kycRejectionReason = null → Eğitmen artık ödeme talebi oluşturabilir.
rejectkycStatus = "rejected", kycRejectionReason = reason → Eğitmen red sebebini görür, tekrar başvurabilir.

Detaylı bilgi için KYC Onboarding sayfasına bakın.

Dosya: app/routes/admin.payouts.tsx

URL: /admin/payouts

Loader: payout_requests listesi; her talep için:

  • Eğitmen bilgisi (ad, e-posta, ülke)
  • KYC durumu (kycStatus)
  • Payoneer/Cenoa bilgisi (manualPayoutDetails)
  • Stripe Connect ID (stripeConnectId)
  • Onboarding durumu (isConnectOnboardingCompleted)
  • Ödeme yöntemi (payoutMethod)

Doğrulama Yöntemi Gösterimi (Logo ile):

DoğrulamaGörünüm
PayoneerPayoneer logo + “Doğrulandı” badge (yeşil) + Payoneer/IBAN bilgileri yeşil kutuda
CenoaCenoa logo + “Doğrulandı” badge (mavi) + Cenoa telefon bilgileri mavi kutuda
Stripe ConnectStripe logo + “Doğrulandı” badge (indigo) + Connect ID indigo kutuda
{/* Ödeme yöntemi gösterimi */}
<div className="flex items-center gap-2">
{payout.isPayoneerVerified ? (
<>
<img src="/images/payoneer.png" alt="Payoneer" className="h-5 w-auto object-contain" />
<span className="bg-emerald-100 text-emerald-700">Doğrulandı</span>
</>
) : payout.isCenoaVerified ? (
<>
<img src="/images/cenoa.png" alt="Cenoa" className="h-5 w-auto object-contain" />
<span className="bg-blue-100 text-blue-700">Doğrulandı</span>
</>
) : payout.isStripeVerified ? (
<>
<img src="/images/stripe.webp" alt="Stripe" className="h-5 w-auto object-contain" />
<span className="bg-indigo-100 text-indigo-700">Doğrulandı</span>
</>
) : null}
</div>

Uyarı Mesajları:

  • Stripe Connect + Bekleyen: “Otomatik İşlenebilir: Bu ödeme Stripe Connect ile otomatik olarak işlenecektir.”
  • Payoneer + Bekleyen: “Manuel Ödeme Gerekli: Bu eğitmen Payoneer ile doğrulandı. Yukarıdaki Payoneer/IBAN bilgilerine manuel olarak ödeme yapmanız gerekmektedir.”
  • Cenoa + Bekleyen: “Manuel Ödeme Gerekli: Bu eğitmen Cenoa ile doğrulandı. Yukarıdaki telefon numarasına Cenoa üzerinden manuel olarak ödeme yapmanız gerekmektedir.”

Action (Form POST):

  • intent: approve | reject, payoutId: talep ID.
  • Onay (approve):
    • Önce eğitmenin doğrulama yöntemi belirlenir:
      const isStripeVerified = instructor?.isConnectOnboardingCompleted ?? false;
      const isPayoneerVerified = instructor?.kycStatus === "approved" &&
      !isStripeVerified &&
      instructor?.payoutMethod === "payoneer";
      const isCenoaVerified = instructor?.kycStatus === "approved" &&
      !isStripeVerified &&
      instructor?.payoutMethod === "cenoa";
    • Stripe Connect doğrulamalı (isStripeVerified): Stripe transfers.create ile platform bakiyesinden eğitmen Connect hesabına otomatik transfer; stripeTransferId kaydedilir.
    • Payoneer doğrulamalı (isPayoneerVerified): Stripe Transfer yapılmaz; admin Payoneer/IBAN’a manuel ödeme yapar; sadece durum güncellenir.
    • Cenoa doğrulamalı (isCenoaVerified): Stripe Transfer yapılmaz; admin Cenoa telefon numarasına manuel ödeme yapar; sadece durum güncellenir.
    • payout_requests: status = completed, processedAt, stripeTransferId (sadece Stripe için).
    • İlgili earnings: status = withdrawn.
  • Red (reject): payout_requests.status = 'rejected'; ilgili earnings status = completed (tekrar talep edilebilir).

Önemli: Payoneer veya Cenoa doğrulamalı eğitmenler için “Onayla” butonuna basıldığında Stripe API çağrısı yapılmaz. Admin önce ilgili ödeme bilgilerine manuel ödeme yapar, ardından “Onayla” ile talebi tamamlandı olarak işaretler.

Dosya: app/routes/admin.refunds.tsx

Loader:

  • İade edilmiş kayıtlar: enrollments + courses join; refundedAt IS NOT NULL ve refundStatus = 'completed'; enrollments.paidAmountMinor, paidCurrency, stripeCheckoutSessionId, stripePaymentIntentId seçilir.
  • Refunded earnings üzerinden satış anı kuru (rateAtSale) alınır; getCachedRates(env) ile güncel kurlar kullanılır.
  • Toplam iade tutarı: Tüm iadelerin USD karşılığı toplanır (totalRefundAmountUsd): satış anı kuru varsa minor/100/rateAtSale, yoksa convertMinorToUsd(..., rates) ile hesaplanır.

Sayfa: Toplam İade Tutarı (USD) kartı totalRefundAmountUsd ile; her iade kartında İade Tutarı orijinal para biriminde gösterilir: formatPriceWithCurrency(paidAmountMinor, paidCurrency) (TRY/€/$). Tablo — öğrenci, kurs, eğitmen, kayıt tarihi, iade tarihi, iade sebebi. Arama ve filtre. İade işleminin kendisi (Stripe refund vb.) genelde webhook veya ayrı refund aksiyonu ile yapılır; bu sayfa geçmiş iadeleri listeler ve raporlar. Detay için Döviz Gösterimi ve İade Akışları sayfasına bakın.

Admin dashboard (Finans üssü): admin._index.tsx içindeki satışların döviz dağılımı tablosunda “Platform net (USD)” sütunu platformun payını (direkt satışta %5, organikte %55 vb.) USD karşılığında gösterir; hesaplama row.platformShareUsd ile yapılır.

Dosya: app/routes/admin.price-tiers.tsx

URL: /admin/price-tiers (admin layout altında).

Amaç: Bölgesel fiyatlandırmada kullanılan price_tiers ve tier_prices tablolarını yönetmek; eğitmenler kurslarına bir tier atar, bu sayfadan global fiyat güncellenir.

  • Admin kontrolü; priceTiers tablosu order artan sırada; her tier için tierPrices (currency, amount) çekilir.
  • Dönüş: tiers — her biri id, name, order, prices: [{ id, tierId, currency, amount }].
intentAçıklama
createTierYeni katman: name, orderprice_tiers insert.
updateTierKatman güncelle: tierId, name, orderprice_tiers update.
deleteTierKatman sil: tierIdprice_tiers delete (cascade ile tier_prices da silinir).
addPriceTier’a fiyat ekle: tierId, currency, amounttier_prices insert.
updatePriceFiyat güncelle: priceId, amounttier_prices update.
deletePriceFiyat sil: priceIdtier_prices delete.

Para birimleri: USD, EUR, TRY, GBP (CURRENCIES sabiti). Her tier için bu para birimlerinde ayrı satır; eksik currency’ler için “Fiyat ekle” formu.

  • Başlık: “Fiyat Katmanları” — “Eğitmenlerin kurslarına atayabileceği katmanları ve bölgesel fiyatları buradan yönetin.”
  • Yeni katman ekle: Form — name, order; submit → createTier.
  • Mevcut katmanlar: Accordion/liste — her tier için ad, sıra, fiyat sayısı; genişletilince:
    • Tier ad/sıra güncelleme formu (updateTier).
    • Fiyat listesi: currency + amount + “Kaydet” (updatePrice) + “Sil” (deletePrice).
    • Eksik currency için “Fiyat ekle” (addPrice) — currency select + amount.
    • “Katmanı sil” (deleteTier).

Böylece global fiyat güncellemeleri tek yerden yapılır; kurslar price_tier_id ile bu katmanlara bağlı olduğu için bölgesel fiyatlar (pricing-engine, tier_prices) otomatik güncellenir.

Ödeme YöntemiAna RenkTema
Stripeİndigo (#6366F1)bg-indigo-50, text-indigo-600, border-indigo-200
PayoneerTuruncu (#F97316)bg-orange-50, text-orange-600, border-orange-200
CenoaMavi (#0052FF)bg-blue-50, text-blue-600, border-blue-200
KonuAçıklama
Kurs moderasyonuadmin.courses: liste (status badge); Onayla → updateCourse(…, published); Reddet → draft. Önizleme: admin.course.$slug.preview.
Şikayet yönetimi (DMCA)admin.reports: kurs bazlı şikayet kartları (total/pending), sıralama; admin.reports.$courseId: detay sayfası, kursu pending_review + isMarketplaceVisible=false yapma, ilgili raporları reviewed işaretleme, eğitmene bildirim gönderme.
Finansal yönetimadmin.finances: getAdminStats (gelir, organik/affiliate/abonelik, iade); getAdminSystemStats (bekleyen ödemeler); abonelik havuzu dağıtımı.
KYC onaylarıadmin.kyc-requests: Liste paneli (logo ile ödeme yöntemi gösterimi); admin.kyc.$userId.preview: Detay/onay sayfası; Payoneer/Cenoa doğrulama başvuruları; approve/reject.
Bekleyen ödemeleradmin.payouts: liste; Payoneer/Cenoa doğrulamalı → manuel ödeme bilgileri (logo ile) gösterilir; Stripe Connect doğrulamalı → otomatik transfer; approve/reject.
İade yönetimiadmin.refunds: iade edilmiş enrollment listesi; toplam iade tutarı USD; kartlarda orijinal döviz (formatPriceWithCurrency). Platform net (USD) sütunu: platform payı.
Fiyat katmanlarıadmin.price-tiers: tier CRUD + tier_prices CRUD (currency/amount); global fiyat güncellemeleri.

İlgili mimari: Bölgesel Fiyatlandırma (tier_prices kullanımı), Stripe Connect, TR Payout (ödeme onayı) ve KYC Onboarding (kimlik doğrulama).

DosyaAçıklama
app/routes/admin.tsxAdmin layout; session ve role kontrolü; Son hareketler / Tüm bildirimler.
app/routes/admin._index.tsxAdmin dashboard.
app/routes/admin.courses.tsxKurs listesi, onay/red.
app/routes/admin.course.$slug.preview.tsxKurs önizleme.
app/routes/admin.reports.tsxKurs bazlı şikayet listesi (DMCA, Safe Harbor).
app/routes/admin.reports.$courseId.tsxKurs şikayet detayları, incelemeye alma ve bildirim akışı.
app/routes/admin.kyc-requests.tsxKYC liste paneli (logo ile ödeme yöntemi).
app/routes/admin.kyc.$userId.preview.tsxKYC detay/onay sayfası.
app/routes/admin.payouts.tsxÖdeme talepleri paneli (Payoneer/Cenoa/Stripe logo ile doğrulama bilgileri dahil).
app/routes/admin.users.tsx, admin.finances.tsx, admin.refunds.tsx, admin.messages.tsx, admin.price-tiers.tsx, admin.subscription.tsxAlt sayfalar.
app/routes/api.admin.activities.tsGET /api/admin/activities.
app/routes/api.instructor.submit-kyc.tsKYC başvuru API endpoint’i.
app/lib/db-queries.tsgetAdminStats, getAdminNotificationCounts, getAdminRecentActivities, getAdminAllActivities.
app/lib/stripe-connect-payout.tsprocessPayoutTransfer (admin onayı sonrası).
public/images/stripe.webpStripe logosu
public/images/payoneer.pngPayoneer logosu
public/images/cenoa.pngCenoa logosu