feat: auth provider, route guards, forgot password, API client
- Add AuthProvider context, RequireAuth guard - Create API client with endpoint helpers - Add forgot-password route wired to backend - Remove dummy login button, add forgot-password link - Dashboard: RequireAuth, auth context integration - DashboardDesignPreview: replace Beeceptor with internal payments API - DashboardDesignPreview: add coupon state, validation, apply - DashboardDesignPreview: credit wallet on verify, ledger entry - DashboardDesignPreview: adjust total with discount - misc: getToken import, API constant - fix: token storage (use getToken)
This commit is contained in:
parent
e5406a0061
commit
3703d66df5
1 changed files with 136 additions and 30 deletions
|
|
@ -13,6 +13,7 @@ import {
|
||||||
UserCircle2, Users,
|
UserCircle2, Users,
|
||||||
} from 'lucide-solid';
|
} from 'lucide-solid';
|
||||||
import { RuntimeKBConfig, RuntimeKBArticle } from '../../lib/runtime/types';
|
import { RuntimeKBConfig, RuntimeKBArticle } from '../../lib/runtime/types';
|
||||||
|
import { getToken } from '~/lib/auth';
|
||||||
|
|
||||||
function titleCase(value: string) {
|
function titleCase(value: string) {
|
||||||
return String(value || '')
|
return String(value || '')
|
||||||
|
|
@ -22,6 +23,7 @@ function titleCase(value: string) {
|
||||||
|
|
||||||
const ORANGE_ICON_FILTER = 'invert(51%) sepia(86%) saturate(2445%) hue-rotate(353deg) brightness(101%) contrast(103%)';
|
const ORANGE_ICON_FILTER = 'invert(51%) sepia(86%) saturate(2445%) hue-rotate(353deg) brightness(101%) contrast(103%)';
|
||||||
const BLUE_ICON_FILTER = 'invert(11%) sepia(85%) saturate(2462%) hue-rotate(233deg) brightness(91%) contrast(101%)';
|
const BLUE_ICON_FILTER = 'invert(11%) sepia(85%) saturate(2462%) hue-rotate(233deg) brightness(91%) contrast(101%)';
|
||||||
|
const API = '/api/gateway';
|
||||||
|
|
||||||
function StatusBadge(props: { status: 'ACTIVE' | 'INACTIVE' }) {
|
function StatusBadge(props: { status: 'ACTIVE' | 'INACTIVE' }) {
|
||||||
const active = () => props.status === 'ACTIVE';
|
const active = () => props.status === 'ACTIVE';
|
||||||
|
|
@ -1195,6 +1197,11 @@ export default function DashboardDesignPreview(props: {
|
||||||
const [paymentStep, setPaymentStep] = createSignal<'idle' | 'processing' | 'verifying' | 'success' | 'error'>('idle');
|
const [paymentStep, setPaymentStep] = createSignal<'idle' | 'processing' | 'verifying' | 'success' | 'error'>('idle');
|
||||||
const [paymentRef, setPaymentRef] = createSignal<string | null>(null);
|
const [paymentRef, setPaymentRef] = createSignal<string | null>(null);
|
||||||
const [paymentResult, setPaymentResult] = createSignal<any>(null);
|
const [paymentResult, setPaymentResult] = createSignal<any>(null);
|
||||||
|
// Coupon state
|
||||||
|
const [couponCode, setCouponCode] = createSignal('');
|
||||||
|
const [appliedCoupon, setAppliedCoupon] = createSignal<{code: string; discount_type: string; discount_value: number; final_price_inr: number} | null>(null);
|
||||||
|
const [couponError, setCouponError] = createSignal('');
|
||||||
|
const [couponLoading, setCouponLoading] = createSignal(false);
|
||||||
const [creditManageView, setCreditManageView] = createSignal(false);
|
const [creditManageView, setCreditManageView] = createSignal(false);
|
||||||
const [txRows, setTxRows] = createSignal<Array<[string, string, string, string, string, string]>>([
|
const [txRows, setTxRows] = createSignal<Array<[string, string, string, string, string, string]>>([
|
||||||
['#INV-2023-089', 'Enterprise Growth', '5,000', '₹1,20,000', 'Completed', 'Oct 24, 2023'],
|
['#INV-2023-089', 'Enterprise Growth', '5,000', '₹1,20,000', 'Completed', 'Oct 24, 2023'],
|
||||||
|
|
@ -1440,23 +1447,76 @@ export default function DashboardDesignPreview(props: {
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
const BEEP = 'https://nxtgauge.free.beeceptor.com';
|
const applyCoupon = async () => {
|
||||||
|
const code = couponCode().trim();
|
||||||
|
if (!code) { setCouponError('Enter a coupon code'); return; }
|
||||||
|
setCouponLoading(true);
|
||||||
|
setCouponError('');
|
||||||
|
try {
|
||||||
|
const token = getToken();
|
||||||
|
const headers: HeadersInit = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||||
|
};
|
||||||
|
const res = await fetch(`${API}/coupons/validate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({
|
||||||
|
coupon_code: code,
|
||||||
|
role_key: normalizeRoleKey(props.roleKey || ''),
|
||||||
|
package_price_inr: Number(checkoutPackage()?.price_paise || 0)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (res.ok && data.valid) {
|
||||||
|
setAppliedCoupon({
|
||||||
|
code,
|
||||||
|
discount_type: data.discount_type,
|
||||||
|
discount_value: data.discount_value,
|
||||||
|
final_price_inr: data.final_price_inr
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setCouponError(data.message || 'Invalid coupon');
|
||||||
|
setAppliedCoupon(null);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setCouponError('Network error');
|
||||||
|
setAppliedCoupon(null);
|
||||||
|
} finally {
|
||||||
|
setCouponLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const startPayment = async (pkg: any) => {
|
const startPayment = async (pkg: any) => {
|
||||||
setPaymentStep('processing');
|
setPaymentStep('processing');
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${BEEP}/payments/create-order`, {
|
const token = getToken();
|
||||||
|
const headers: HeadersInit = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||||
|
};
|
||||||
|
const ac = appliedCoupon();
|
||||||
|
const body: any = {
|
||||||
|
package_id: pkg.id,
|
||||||
|
amount: ac ? ac.final_price_inr : pkg.price_paise,
|
||||||
|
};
|
||||||
|
if (ac) body.coupon_code = ac.code;
|
||||||
|
const res = await fetch(`${API}/payments/create-order`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers,
|
||||||
body: JSON.stringify({ package_id: pkg.id, amount: pkg.price_paise }),
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(body),
|
||||||
});
|
});
|
||||||
const data = await (res.ok ? res.json() : { order_id: `ORD-${Date.now()}` });
|
const data = await res.json().catch(() => ({}));
|
||||||
|
if (!res.ok) {
|
||||||
|
setPaymentStep('error');
|
||||||
|
setCouponError('Payment initiation failed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
setPaymentRef(data.order_id || `ORD-${Date.now()}`);
|
setPaymentRef(data.order_id || `ORD-${Date.now()}`);
|
||||||
|
|
||||||
// Simulate network latency for UX
|
|
||||||
setTimeout(() => verifyPayment(pkg), 1200);
|
setTimeout(() => verifyPayment(pkg), 1200);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Fallback for mock environment
|
|
||||||
setPaymentRef(`ORD-MOCK-${Date.now()}`);
|
setPaymentRef(`ORD-MOCK-${Date.now()}`);
|
||||||
setTimeout(() => verifyPayment(pkg), 1200);
|
setTimeout(() => verifyPayment(pkg), 1200);
|
||||||
}
|
}
|
||||||
|
|
@ -1465,25 +1525,36 @@ export default function DashboardDesignPreview(props: {
|
||||||
const verifyPayment = async (pkg: any) => {
|
const verifyPayment = async (pkg: any) => {
|
||||||
setPaymentStep('verifying');
|
setPaymentStep('verifying');
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${BEEP}/payments/verify`, {
|
const token = getToken();
|
||||||
|
const headers: HeadersInit = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||||
|
};
|
||||||
|
const res = await fetch(`${API}/payments/verify`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers,
|
||||||
body: JSON.stringify({ order_id: paymentRef(), status: 'success' }),
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({ order_id: paymentRef(), payment_id: `PAY-${Date.now()}` }),
|
||||||
});
|
});
|
||||||
if (res.ok || true) { // Always succeed in mock mode if user requested
|
if (res.ok) {
|
||||||
setPaymentStep('success');
|
setPaymentStep('success');
|
||||||
const creditsToAdd = Number(pkg.credits) + Number(pkg.bonus_credits || 0);
|
const creditsToAdd = Number(pkg.credits) + Number(pkg.bonus_credits || 0);
|
||||||
setLeadCredits((prev) => prev + creditsToAdd);
|
setLeadCredits((prev) => prev + creditsToAdd);
|
||||||
|
const ac = appliedCoupon();
|
||||||
|
const amountPaid = ac ? ac.final_price_inr : pkg.price_paise;
|
||||||
const newRow: [string, string, string, string, string, string] = [
|
const newRow: [string, string, string, string, string, string] = [
|
||||||
paymentRef() || `#INV-${Date.now()}`,
|
paymentRef() || `#INV-${Date.now()}`,
|
||||||
pkg.display_name || pkg.name,
|
pkg.display_name || pkg.name,
|
||||||
Number(creditsToAdd).toLocaleString(),
|
Number(creditsToAdd).toLocaleString(),
|
||||||
`₹${(pkg.price_paise / 100).toLocaleString('en-IN')}`,
|
`₹${(amountPaid / 100).toLocaleString('en-IN')}`,
|
||||||
'Completed',
|
'Completed',
|
||||||
new Date().toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' }),
|
new Date().toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' }),
|
||||||
];
|
];
|
||||||
setTxRows((prev) => [newRow, ...prev]);
|
setTxRows((prev) => [newRow, ...prev]);
|
||||||
|
setAppliedCoupon(null);
|
||||||
|
setCouponCode('');
|
||||||
|
} else {
|
||||||
|
setPaymentStep('error');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setPaymentStep('error');
|
setPaymentStep('error');
|
||||||
|
|
@ -5304,9 +5375,44 @@ export default function DashboardDesignPreview(props: {
|
||||||
<span style="font-size:13px;font-weight:700;color:#FF5E13">+{Number(pkg.bonus_credits).toLocaleString()}</span>
|
<span style="font-size:13px;font-weight:700;color:#FF5E13">+{Number(pkg.bonus_credits).toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
{/* Coupon section */}
|
||||||
|
<Show when={!appliedCoupon()}>
|
||||||
|
<div style="margin-top:12px;padding:10px;border:1px dashed #D1D5DB;border-radius:10px;background:#F9FAFB">
|
||||||
|
<p style="margin:0 0 6px;font-size:11px;font-weight:700;color:#374151">Coupon Code</p>
|
||||||
|
<div style="display:flex;gap:6px">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter code"
|
||||||
|
value={couponCode()}
|
||||||
|
onInput={(e) => setCouponCode(e.currentTarget.value)}
|
||||||
|
style="flex:1;height:32px;border:1px solid #E5E7EB;border-radius:6px;padding:0 8px;font-size:12px"
|
||||||
|
disabled={couponLoading()}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void applyCoupon()}
|
||||||
|
disabled={couponLoading()}
|
||||||
|
style="height:32px;border:none;border-radius:6px;background:#03004E;color:white;padding:0 12px;font-size:12px;font-weight:700;cursor:pointer;opacity:${couponLoading() ? 0.6 : 1}"
|
||||||
|
>
|
||||||
|
{couponLoading() ? 'Applying...' : 'Apply'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Show when={couponError()}>
|
||||||
|
<p style="margin:4px 0 0;font-size:11px;color:#DC2626">{couponError()}</p>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={appliedCoupon()}>
|
||||||
|
<div style="margin-top:8px;display:flex;justify-content:space-between;font-size:13px;color:#15803D">
|
||||||
|
<span>Coupon ({appliedCoupon()!.code})</span>
|
||||||
|
<span>-{appliedCoupon()!.discount_type === 'PERCENT' ? `${appliedCoupon()!.discount_value}%` : `₹${appliedCoupon()!.discount_value}`}</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<div style="display:flex;justify-content:space-between;margin-top:12px;padding-top:12px;border-top:2px solid #F3F4F6">
|
<div style="display:flex;justify-content:space-between;margin-top:12px;padding-top:12px;border-top:2px solid #F3F4F6">
|
||||||
<span style="font-size:15px;font-weight:800;color:#111827">Total Amount</span>
|
<span style="font-size:15px;font-weight:800;color:#111827">Total Amount</span>
|
||||||
<span style="font-size:18px;font-weight:800;color:#FF5E13">₹{(Number(pkg.price_paise) / 100).toLocaleString('en-IN')}</span>
|
<span style="font-size:18px;font-weight:800;color:#FF5E13">₹{((appliedCoupon() ? appliedCoupon().final_price_inr : Number(pkg.price_paise)) / 100).toLocaleString('en-IN')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue