Update pricing UX, billing flows, and API rules

This commit is contained in:
Dwindi Ramadhana
2026-02-12 00:52:40 +07:00
parent cf065fab1e
commit a905256353
202 changed files with 22348 additions and 301 deletions

View File

@@ -0,0 +1,145 @@
@extends('dashboard.app')
@section('page_title', 'Admin Pricing')
@section('page_subtitle', 'Manage plan pricing and rollout changes.')
@section('dashboard_content')
@if (session('status'))
<div class="mb-6 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700 dark:border-emerald-500/30 dark:bg-emerald-500/15 dark:text-emerald-200">
{{ session('status') }}
</div>
@endif
<div class="grid gap-6 lg:grid-cols-3">
@foreach ([
['label' => 'Plans', 'value' => number_format(($plans ?? collect())->count()), 'note' => 'Active tiers'],
['label' => 'Pending changes', 'value' => number_format(($changes ?? collect())->count()), 'note' => 'Latest snapshots'],
['label' => 'Currency', 'value' => optional(($plans ?? collect())->first())->currency ?? 'IDR', 'note' => 'Default'],
] as $card)
<div class="rounded-2xl glass-card p-5">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">{{ $card['label'] }}</div>
<div class="mt-3 text-3xl font-semibold text-white">{{ $card['value'] }}</div>
<div class="mt-2 text-sm text-gray-400">{{ $card['note'] }}</div>
</div>
@endforeach
</div>
<form method="POST" action="{{ route('dashboard.admin.pricing.update') }}" class="mt-8 grid gap-6 lg:grid-cols-3" data-loading-form>
@csrf
@forelse ($plans ?? [] as $plan)
<div class="rounded-2xl glass-card p-6">
<div class="flex items-center justify-between">
<div class="text-sm font-semibold uppercase tracking-[0.2em] text-gray-400">{{ $plan->code }}</div>
<span class="rounded-full bg-slate-200 px-3 py-1 text-xs font-semibold text-slate-700 dark:bg-white/10 dark:text-gray-200">{{ $plan->status }}</span>
</div>
<div class="mt-4 grid gap-3 text-sm text-gray-300">
<label class="block">
Name
<input name="plans[{{ $loop->index }}][name]" value="{{ $plan->name }}" class="mt-2 w-full rounded-xl border border-white/10 px-3 py-2 text-gray-200 placeholder-gray-500 theme-surface">
</label>
<label class="block">
Code
<input name="plans[{{ $loop->index }}][code]" value="{{ $plan->code }}" class="mt-2 w-full rounded-xl border border-white/10 px-3 py-2 text-gray-200 placeholder-gray-500 theme-surface">
</label>
<div class="grid grid-cols-2 gap-3">
<label class="block">
IDR price
<input name="plans[{{ $loop->index }}][amount_idr]" type="number" value="{{ $plan->amount }}" class="mt-2 w-full rounded-xl border border-white/10 px-3 py-2 text-gray-200 placeholder-gray-500 theme-surface">
</label>
<label class="block">
USD price
<input name="plans[{{ $loop->index }}][amount_usd]" type="number" step="0.01" value="{{ $plan->meta['prices']['USD'] ?? '' }}" class="mt-2 w-full rounded-xl border border-white/10 px-3 py-2 text-gray-200 placeholder-gray-500 theme-surface" placeholder="e.g. 9.99">
</label>
</div>
<div class="grid grid-cols-2 gap-3">
<label class="block">
Period
<select name="plans[{{ $loop->index }}][period]" class="mt-2 w-full rounded-xl border border-white/10 px-3 py-2 text-gray-200 theme-surface">
<option value="" @selected(empty($plan->period))>None</option>
<option value="month" @selected($plan->period === 'month')>Monthly</option>
<option value="year" @selected($plan->period === 'year')>Annual</option>
</select>
</label>
<label class="block">
Status
<select name="plans[{{ $loop->index }}][status]" class="mt-2 w-full rounded-xl border border-white/10 px-3 py-2 text-gray-200 theme-surface">
<option value="active" @selected($plan->status === 'active')>Active</option>
<option value="inactive" @selected($plan->status === 'inactive')>Inactive</option>
</select>
</label>
</div>
<div class="rounded-xl border border-white/10 px-3 py-2 text-xs text-gray-400 theme-surface">
<div class="font-semibold text-gray-300">PayPal Plan IDs</div>
<div class="mt-2 space-y-1">
<div>Sandbox: <span class="text-gray-200">{{ $plan->meta['paypal']['sandbox']['plan']['id'] ?? '—' }}</span></div>
<div>Live: <span class="text-gray-200">{{ $plan->meta['paypal']['live']['plan']['id'] ?? '—' }}</span></div>
<div>Last sync: <span class="text-gray-200">{{ $plan->meta['paypal']['live']['plan']['synced_at'] ?? $plan->meta['paypal']['sandbox']['plan']['synced_at'] ?? '—' }}</span></div>
</div>
</div>
</div>
</div>
@empty
<div class="rounded-2xl border border-dashed border-white/10 bg-white/5 p-6 text-sm text-gray-400">
No pricing plans yet. Use reset to defaults to create base plans.
</div>
@endforelse
<div class="lg:col-span-3 flex flex-wrap gap-3">
<button class="rounded-xl bg-white/10 border border-white/10 px-4 py-2 text-sm font-semibold text-white hover:bg-white/20 transition-colors" data-loading-btn>Save pricing</button>
<button form="pricing-reset" class="rounded-xl border border-white/10 px-4 py-2 text-sm font-semibold text-gray-200 hover:bg-white/5 transition-colors" data-loading-btn>Reset to defaults</button>
<button form="pricing-paypal-sync" class="rounded-xl border border-white/10 px-4 py-2 text-sm font-semibold text-gray-200 hover:bg-white/5 transition-colors" data-loading-btn>Sync PayPal plans</button>
</div>
</form>
<form id="pricing-reset" method="POST" action="{{ route('dashboard.admin.pricing.reset') }}" data-loading-form>
@csrf
</form>
<form id="pricing-paypal-sync" method="POST" action="{{ route('dashboard.admin.pricing.paypal_sync') }}" data-loading-form>
@csrf
</form>
<div class="mt-8 rounded-2xl glass-card p-6">
<div class="flex items-center justify-between">
<div>
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Change log</div>
<div class="mt-2 text-lg font-semibold text-white">Recent pricing updates</div>
</div>
<form method="POST" action="{{ route('dashboard.admin.pricing.snapshot') }}" data-loading-form>
@csrf
<button class="rounded-xl bg-white/10 border border-white/10 px-4 py-2 text-sm font-semibold text-white hover:bg-white/20 transition-colors" data-loading-btn>Create update</button>
</form>
</div>
<div class="mt-6 space-y-4 text-sm text-gray-300">
@forelse ($changes ?? [] as $change)
<div class="flex items-center justify-between rounded-xl border border-white/10 px-4 py-3 theme-surface">
<div>
<div class="font-semibold text-white">Pricing change #{{ $change->id }}</div>
<div class="text-xs text-gray-400">{{ $change->created_at?->toDateString() }} · {{ $change->admin_ref ?? 'system' }}</div>
</div>
<span class="rounded-full bg-emerald-100 px-3 py-1 text-xs font-semibold text-emerald-800 dark:bg-emerald-500/20 dark:text-emerald-200">Published</span>
</div>
@empty
<div class="text-sm text-gray-400">No pricing changes recorded yet.</div>
@endforelse
</div>
</div>
<script>
(() => {
const forms = document.querySelectorAll('[data-loading-form]');
const buttons = document.querySelectorAll('[data-loading-btn]');
const setBusy = (on) => {
buttons.forEach(btn => {
btn.disabled = on;
btn.dataset.label = btn.dataset.label || btn.textContent;
btn.textContent = on ? 'Processing…' : btn.dataset.label;
btn.classList.toggle('opacity-70', on);
btn.classList.toggle('cursor-wait', on);
});
};
forms.forEach(form => {
form.addEventListener('submit', () => setBusy(true));
});
})();
</script>
@endsection