← Blog

Next.js’te bir e-ticaret yönetim paneli kurmak: siparişler, iadeler ve stok

Bir Next.js mağaza yönetim panelinin zor kısımları: iki sipariş durumu, kısmi toplu hatalar, kuruşu kuruşuna tutan iade hesabı, stok ayarlamaları ve indirimler.

Preet Suthar · · 6 dk okuma

  • Next.js
  • React
  • E-commerce
  • Dashboard

Bir e-ticaret yönetim paneli bir CRUD uygulamasına benzer: bir sipariş tablosu, sipariş başına bir sayfa, ürün başına bir form. Tablolar kolay kısımdır. Zor kısım, bir mağaza yönetim panelindeki her sayının para ya da stok olması ve müşteriler arka planda alışverişe devam ederken ikisinin de doğru kalmak zorunda olmasıdır.

Next.js’te bir mağazanın yönetim tarafını kurarken öğrendiklerim bunlar: dürüst durumları olan sipariş tabloları, yapılacak işi gösteren bir sipariş sayfası, kuruşu kuruşuna tutan iadeler, canlı siparişlerden sağ çıkan stok düzenlemeleri ve satıcıların okuyabildiği indirimler.

Siparişler: bir değil, iki durum

İlk hata tek bir status sütunudur. Bir siparişin birbirinden bağımsız en az iki durumu vardır: ödeme (beklemede, yetkilendirildi, ödendi, kısmen iade edildi, iade edildi) ve gönderim (gönderilmedi, kısmen gönderildi, gönderildi). "Ödendi ve gönderilmedi", bir satıcının her sabah üzerinden çalıştığı listedir ve tek bir durum sütunu bunu ifade edemez.

Bunları iki hap olarak gösterin. Anlamı renge taşıtmayın. WCAG, rengin bilgiyi göstermenin tek yolu olmamasını ister ve yeşil "Ödendi"nin yanındaki açık turuncu "Kısmen ödendi" birçok kişi için zor ayırt edilir. Bir metin etiketi artı küçük bir glif, yani boş, yarım ya da dolu bir daire, tek bakışta okunur.

Kayıtlı görünümler URL’de yaşar

Satıcılar siparişlere göz atmaz, listeler üzerinden çalışır: Gönderilmemiş, Ödenmemiş, Açık. Kayıtlı bir görünüm yalnızca filtreler, bir sıralama ve bir aramadır. Bir bağlantı aynı listeyi açsın ve Geri çalışsın diye sorgunun tamamını URL’de tutun ve Next.js’te sunucuda okuyun:

type SearchParams = Promise<Record<string, string | string[] | undefined>>

export default async function OrdersPage({
  searchParams,
}: {
  searchParams: SearchParams
}) {
  const query = parseOrdersQuery(await searchParams)
  const page = await listOrders(query)
  return <OrdersTable query={query} page={page} />
}

Mevcut filtreler kayıtlı görünümden farklıysa sekmeyi değiştirilmiş olarak işaretleyin ve Sıfırla, Görünümü güncelle ve Yeni görünüm olarak kaydet seçeneklerini sunun; tıpkı bir elektronik tablonun bir filtrenin açık olduğunu söylemesi gibi.

Yeni siparişler imlecin altındaki satırı kaydırmamalı

Siparişler, satıcı listeye bakarken gelir. Yeni satırlar her şeyi aşağı iterse tıklamak üzere oldukları onay kutusu farklı bir siparişe dönüşür. Bir satır odaktayken ya da bir şey seçiliyken yeni siparişleri bir "1 yeni sipariş · Göster" hapının arkasında bekletin ve onları yalnızca devam eden bir şey yokken en üste ekleyin.

Toplu işlemler kısmen başarısız olur

"6 siparişi gönderildi olarak işaretle" bazen 5 tanesini gönderir. Biri beklemededir ya da bir dakika önce iptal edilmiştir. API’yi buna göre tasarlayın: başarısız olan id’leri her birinin nedeniyle döndürün ve tam olarak bunu bildirin.

type BulkResult = {
  succeeded: string[]
  failed: { id: string; reason: string }[]
}

Mesaj da kendiliğinden ortaya çıkar: "5 sipariş gönderildi. 1 sipariş gönderilemedi: #1031 beklemede", o siparişe bir bağlantıyla birlikte. Ya hep ya hiç türünde bir hata toast’ı, satıcıyı hangilerinin geçtiğini tahmin etmeye bırakır.

Sipariş sayfası kalanı gösterir

Sipariş sayfasında ürünleri nasıl satın alındıklarına göre değil, başlarına ne geldiğine göre ayırın. Her gönderinin kargo firması ve takip bilgisiyle kendi kartı olur. İade edilen ürünlerin de kendi kartı olur. En önemli kart ise hâlâ gönderilecek olanlardır:

function unfulfilledQuantity(line: OrderLine) {
  return line.quantity - line.fulfilledQuantity - line.refundedQuantity
}

Altındaki zaman akışı siparişin olaylarından kurulmalı; veriden gelen kısa başlıklarla ("$564.41 tutarındaki ödeme tahsil edildi", takip numarası bağlantılı "UPS ile gönderildi"), tam zaman bir tooltip’te ve aynı türden tekrarlanan olaylar tek bir satırda katlanmış olarak. Personel yorumları aynı akışta yer alır ve iyimser biçimde gönderilir; başarısız olurlarsa yeniden deneme seçeneğiyle.

İade hesabı kuruşu kuruşuna tutmalı

İadeler, yönetim paneli kodunun parayı en sık yanlış hesapladığı yerdir. Bir müşteri $10 sipariş indirimi ve vergiyle üç ürün aldı ve birini iade ediyor. Ne kadar geri alır?

Sipariş düzeyindeki tutarları her satırın fiyatıyla orantılı olarak satırlara dağıtın. İndirimleri fiyata göre, vergiyi indirim sonrası fiyata göre. Tam sayı cent olarak bölmek kalanlar bırakır; en büyük kalan yöntemi bunları dağıtır, böylece parçaların toplamı her zaman orijinale eşit olur:

function allocate(total: number, weights: number[]): number[] {
  const sum = weights.reduce((a, b) => a + b, 0)
  if (sum === 0) return weights.map(() => 0)
  const exact = weights.map((weight) => (total * weight) / sum)
  const parts = exact.map(Math.floor)
  let left = total - parts.reduce((a, b) => a + b, 0)
  const byFraction = exact
    .map((value, index) => ({ index, fraction: value - Math.floor(value) }))
    .sort((a, b) => b.fraction - a.fraction)
  for (const { index } of byFraction) {
    if (left === 0) break
    parts[index] += 1
    left -= 1
  }
  return parts
}

allocate(1000, [3000, 3000, 3000]), toplamı tam olarak 1000 olan [334, 333, 333] sonucunu verir.

İkinci bir tuzak var. Bir satırın bir birimini bugün, diğer ikisini gelecek hafta iade edin; toplamlar üçünü birden iade etmekle eşleşmelidir. Her iadeyi kendi başına yuvarlamak yerine her birime satırın payından kümülatif bir dilim verin:

function unitsShare(lineShare: number, quantity: number, from: number, count: number) {
  const upTo = (units: number) => Math.floor((lineShare * units) / quantity)
  return upTo(from + count) - upTo(from)
}

from, kaç birimin zaten iade edildiğidir. İadeler nasıl bölünürse bölünsün, dilimlerin toplamı lineShare’e eşit olur.

İade ekranının geri kalanı durum yönetimidir. Kargo, ödenen tutardan zaten iade edilen tutar düşülerek iade edilebilir. Bir sipariş bir hediye kartı ile bir kart arasında bölündüyse para, ödeme yöntemlerine sabit bir sırayla geri döner. Yeniden denenen bir istek asla iki kez iade yapmasın diye bir idempotency anahtarı gönderin, büyük iadelerde onay isteyin ve "bankada beklemede" durumunu başarı ya da başarısızlık olarak değil, kendi durumu olarak gösterin.

Stok: üzerine yazmayın, ayarlayın

Naif stok düzenleyicisi yeni sayıyı gönderir: "Sand / Large 47". Satıcı yazarken iki sipariş geldiyse 47’yi kaydetmek onları sessizce geri alır.

Bunun yerine değişiklikleri ayarlamalar olarak gönderin. "5 teslim alındı" bir farktır ve farkları arada olan her şeyin üzerine uygulamak güvenlidir. Satıcı gerçekten bir sayımı kastediyorsa, örneğin bir dönemsel sayımdan sonra "47’ye ayarla", bunu gördükleri miktara karşı bir farka dönüştürün ve o miktar değiştiyse sunucunun reddetmesine izin verin:

type Adjustment = {
  variantId: string
  delta: number
  basedOn: number
  reason: "received" | "cycle_count" | "damaged" | "correction"
}

Reddedilen bir satır, "Miktar başka bir cihaz tarafından 40 olarak değiştirildi" gibi bir mesajla taslakta kalır ve Yeniden uygula onu yeni sayının üzerine tekrar gönderir. Shopify’ın inventoryAdjustQuantities mutation’ı da changeFromQuantity alanıyla aynı şekilde çalışır.

Sayıyı da bölün. Eldeki stok binadaki stoktur, ama ayrılmış stok zaten satılmıştır ve kullanılamayan stok hasarlıdır ya da bekletilmektedir. Satıcıların kullanılabilir stoğu eldeki stoktan ayrı görmesi gerekir, yoksa zaten satılmış stoğu satarlar.

Cümle olarak indirimler

İndirim formları genellikle uzun bir alan listesidir ve satıcılar bir müşteri şikâyet edene kadar ne kurduklarını anlayamaz. Teklifi düzenlenebilir parçaları olan tek bir cümle olarak yazın, "Müşteriler Ring Vase ve Glass Carafe’ta %15 indirim kazanır"; minimum alışverişi, müşteri uygunluğunu, kullanım sınırlarını, birleştirmeleri ve zamanlamayı mevcut değerlerini gösteren daraltılmış satırlara koyun ve indirimin uygulandığı örnek bir ödeme sayfasını önizleyin.

Kullanım sınırı olmayan %100 indirim gibi riskli kurulumlar hakkında uyarın ve zamanlamayı tarayıcının değil, mağazanın saat diliminde yapın.

Tek mağaza, her sayfa

Son parça tutarlılıktır. Bir ürünü iade edin; siparişin ödeme durumu, zaman akışı, müşterinin toplam harcaması ve stoğa geri eklediğiniz miktar birlikte değişmelidir. Her mutation’ın değiştirdiği varlıkları döndürmesini sağlayın ve onları gösteren her görünümü güncelleyin ya da yeniden doğrulayın. Kaydedilmemiş ürün, stok ya da indirim düzenlemelerinden ayrılmayı da engelleyin, tarayıcının Geri düğmesi dahil.

Bunun için HextaUI blokları

HextaUI Pro’daki yönetim bloklarının etrafında kurulduğu sorunlar bunlar. Orders Table’da URL’de tutulan kayıtlı görünümler, ilerleme glifleriyle ayrı ödeme ve gönderim hapları ve kısmi başarısızlıkları bildiren toplu işlemler var. Refund hesabını, en büyük kalan yöntemini kullanan ve testleri olan saf bir fonksiyon olan calculateRefund(order, selection) içinde yapar. Inventory üzerine yazmaları değil ayarlamaları kaydeder ve satıcıların reddedilen satırları yeniden uygulamasına izin verir. Discount Builder teklifi, canlı bir ödeme sayfası önizlemesinin yanında bir cümle olarak düzenler.

Commerce Dashboard bunları Order Detail, Product Editor ve Sales Overview ile birlikte, ortak bir store üzerinde tek bir mağaza yönetim paneline toplar; varsayılan olarak hash rotalarıyla ya da router prop’u üzerinden Next.js rotalarınızla çalışır ve her eylemi Shopify Admin API’ye eşleyen örnekler içerir. Canlı önizleme panelin tamamıdır; bir siparişi iade edip her sayfanın güncellenmesini izleyebilirsiniz.

SSS

Bir siparişin bir durumu mu olmalı, iki mi? En az iki: ödeme ve gönderim birbirinden bağımsızdır ve satıcılar bunların kombinasyonlarına göre filtreler.

Bir indirimi iade edilen ürünlere nasıl dağıtırım? Tam sayı cent olarak, her satırın fiyatıyla orantılı şekilde, parçaların toplamı tam tutsun diye en büyük kalan yöntemiyle.

Stok düzenlemeleri yeni miktarı mı göndermeli, değişikliği mi? Değişikliği. Farklar, satıcı düzenleme yaparken verilen siparişlerden sağ çıkar; "şuna ayarla" türü bir sayım ise dayandığı miktara karşı kontrol edilmelidir.

Bir yönetim panelinin filtreleri nerede yaşamalı? URL’de; böylece görünümler paylaşılabilir, yeniden yüklenebilir ve Geri ile gezilebilir.