Update pricing UX, billing flows, and API rules
This commit is contained in:
145
app/resources/views/dashboard/admin/pricing.blade.php
Normal file
145
app/resources/views/dashboard/admin/pricing.blade.php
Normal 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
|
||||
Reference in New Issue
Block a user