330 lines
15 KiB
TypeScript
330 lines
15 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState } from "react";
|
||
import Link from "next/link";
|
||
|
||
interface SubscriptionInfo {
|
||
status: "trialing" | "active" | "expired" | "canceled" | "canceling";
|
||
isActive: boolean;
|
||
daysRemainingInTrial: number | null;
|
||
trialEndsAt: string | null;
|
||
subscriptionEndsAt: string | null;
|
||
}
|
||
|
||
export default function DashboardPage() {
|
||
const [salesAccountCode, setSalesAccountCode] = useState("");
|
||
const [stripeClearingAccountCode, setStripeClearingAccountCode] = useState("");
|
||
const [stripeAccountId, setStripeAccountId] = useState("");
|
||
const [xeroTenantId, setXeroTenantId] = useState("");
|
||
const [loading, setLoading] = useState(true);
|
||
const [saved, setSaved] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [subscriptionInfo, setSubscriptionInfo] = useState<SubscriptionInfo | null>(null);
|
||
|
||
useEffect(() => {
|
||
Promise.all([
|
||
fetch("/api/dashboard/xero-settings").then((res) => res.json()),
|
||
fetch("/api/dashboard/connections").then((res) => res.json()),
|
||
fetch("/api/subscription/status").then((res) => res.json()),
|
||
]).then(([settings, connections, subscription]) => {
|
||
setSalesAccountCode(settings.salesAccountCode ?? "");
|
||
setStripeClearingAccountCode(settings.stripeClearingAccountCode ?? "");
|
||
setStripeAccountId(connections.stripeAccountId ?? "");
|
||
setXeroTenantId(connections.xeroTenantId ?? "");
|
||
setSubscriptionInfo(subscription);
|
||
setLoading(false);
|
||
});
|
||
}, []);
|
||
|
||
async function save() {
|
||
setSaved(false);
|
||
setError(null);
|
||
|
||
try {
|
||
await fetch("/api/dashboard/xero-settings", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
salesAccountCode,
|
||
stripeClearingAccountCode,
|
||
}),
|
||
});
|
||
|
||
setSaved(true);
|
||
setTimeout(() => setSaved(false), 3000);
|
||
} catch (err) {
|
||
setError("Failed to save settings. Please try again.");
|
||
}
|
||
}
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-b from-slate-50 to-white flex items-center justify-center">
|
||
<div className="text-center">
|
||
<div className="inline-flex items-center justify-center w-12 h-12 mb-4">
|
||
<span className="w-3 h-3 bg-blue-600 rounded-full animate-spin"></span>
|
||
</div>
|
||
<p className="text-slate-600">Loading your settings…</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-b from-slate-50 to-white">
|
||
{/* Navigation */}
|
||
<nav className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-sm border-b border-slate-200">
|
||
<div className="max-w-6xl mx-auto px-6 h-16 flex items-center justify-between">
|
||
<div className="text-xl font-bold bg-gradient-to-r from-blue-600 to-blue-700 bg-clip-text text-transparent">
|
||
S2X
|
||
</div>
|
||
<div className="flex items-center gap-4">
|
||
<Link
|
||
href="/billing"
|
||
className="text-sm text-slate-600 hover:text-slate-900 transition"
|
||
>
|
||
Billing
|
||
</Link>
|
||
<div className="text-sm text-slate-600">Dashboard</div>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<main className="pt-20">
|
||
<div className="max-w-4xl mx-auto px-6 py-16">
|
||
{/* Header */}
|
||
<div className="mb-12">
|
||
<h1 className="text-4xl font-bold text-slate-900 mb-3">Dashboard</h1>
|
||
<p className="text-lg text-slate-600">
|
||
Configure your Xero account codes and manage your automation settings.
|
||
</p>
|
||
</div>
|
||
|
||
<div className="grid lg:grid-cols-3 gap-8">
|
||
{/* Main Content */}
|
||
<div className="lg:col-span-2 space-y-6">
|
||
{/* Connected Accounts */}
|
||
<div className="bg-white border border-slate-200 rounded-xl shadow-sm p-6">
|
||
<h2 className="text-lg font-semibold text-slate-900 mb-4">Connected Accounts</h2>
|
||
<div className="space-y-4">
|
||
<div className="p-4 bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg border border-blue-200">
|
||
<p className="text-sm font-medium text-slate-700 mb-1">Stripe Account</p>
|
||
<p className="text-sm text-slate-600 font-mono break-all">
|
||
{stripeAccountId || <span className="text-slate-400">Not connected</span>}
|
||
</p>
|
||
</div>
|
||
<div className="p-4 bg-gradient-to-br from-green-50 to-green-100 rounded-lg border border-green-200">
|
||
<p className="text-sm font-medium text-slate-700 mb-1">Xero Organisation</p>
|
||
<p className="text-sm text-slate-600 font-mono break-all">
|
||
{xeroTenantId || <span className="text-slate-400">Not connected</span>}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Account Configuration */}
|
||
<div className="bg-white border border-slate-200 rounded-xl shadow-sm p-6">
|
||
<h2 className="text-lg font-semibold text-slate-900 mb-6">Xero Account Codes</h2>
|
||
|
||
<div className="space-y-6">
|
||
{/* Sales Account Code */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-900 mb-2">
|
||
Sales Account Code
|
||
</label>
|
||
<input
|
||
type="text"
|
||
placeholder="e.g., 200"
|
||
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||
value={salesAccountCode}
|
||
onChange={(e) => setSalesAccountCode(e.target.value)}
|
||
/>
|
||
<p className="text-sm text-slate-600 mt-2">
|
||
The Xero account code used for sales invoice line items. This is typically your revenue/sales account.
|
||
</p>
|
||
</div>
|
||
|
||
{/* Stripe Clearing Account Code */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-900 mb-2">
|
||
Stripe Clearing Account Code
|
||
</label>
|
||
<input
|
||
type="text"
|
||
placeholder="e.g., 1200"
|
||
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||
value={stripeClearingAccountCode}
|
||
onChange={(e) => setStripeClearingAccountCode(e.target.value)}
|
||
/>
|
||
<p className="text-sm text-slate-600 mt-2">
|
||
The Xero account code that receives Stripe payments. This is typically a bank or clearing account.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Save Button and Feedback */}
|
||
<div className="mt-8 flex items-center gap-4">
|
||
<button
|
||
onClick={save}
|
||
className="px-6 py-3 bg-gradient-to-r from-blue-600 to-blue-700 text-white font-semibold rounded-lg hover:shadow-lg hover:from-blue-700 hover:to-blue-800 transition"
|
||
>
|
||
Save Settings
|
||
</button>
|
||
|
||
{saved && (
|
||
<div className="flex items-center gap-2 text-sm text-green-700 bg-green-50 px-4 py-3 rounded-lg border border-green-200">
|
||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||
</svg>
|
||
<span>Settings saved successfully</span>
|
||
</div>
|
||
)}
|
||
|
||
{error && (
|
||
<div className="flex items-center gap-2 text-sm text-red-700 bg-red-50 px-4 py-3 rounded-lg border border-red-200">
|
||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||
</svg>
|
||
<span>{error}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Sidebar */}
|
||
<div className="space-y-6">
|
||
{/* Subscription Status Card */}
|
||
{subscriptionInfo && (
|
||
<div
|
||
className={`rounded-xl p-6 border w-full cursor-pointer hover:shadow-md transition ${
|
||
subscriptionInfo.status === "active"
|
||
? "bg-green-50 border-green-200"
|
||
: subscriptionInfo.status === "trialing"
|
||
? "bg-blue-50 border-blue-200"
|
||
: subscriptionInfo.status === "canceling"
|
||
? "bg-amber-50 border-amber-200"
|
||
: "bg-red-50 border-red-200"
|
||
}`}
|
||
onClick={() => window.location.href = "/billing"}
|
||
>
|
||
<h3 className="font-semibold text-slate-900 mb-4">📅 Subscription Status</h3>
|
||
{subscriptionInfo.status === "active" && (
|
||
<p className="text-sm text-green-700">
|
||
✓ Active subscription - Unlimited access
|
||
</p>
|
||
)}
|
||
{subscriptionInfo.status === "trialing" && (
|
||
<>
|
||
<p className="text-sm text-blue-700 font-medium mb-3">
|
||
No subscription yet
|
||
</p>
|
||
<p className="text-sm text-blue-700 mb-3">
|
||
Start your subscription to unlock unlimited invoices and automation.
|
||
</p>
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
window.location.href = "/billing";
|
||
}}
|
||
className="inline-block text-sm font-medium text-blue-600 hover:text-blue-700 underline"
|
||
>
|
||
Subscribe now →
|
||
</button>
|
||
</>
|
||
)}
|
||
{subscriptionInfo.status === "canceling" && (
|
||
<>
|
||
<p className="text-sm text-amber-700 font-medium mb-2">
|
||
Scheduled for Cancellation
|
||
</p>
|
||
<p className="text-sm text-amber-700 mb-3">
|
||
Ends on{" "}
|
||
{subscriptionInfo.subscriptionEndsAt
|
||
? new Date(subscriptionInfo.subscriptionEndsAt).toLocaleDateString()
|
||
: "soon"}
|
||
</p>
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
window.location.href = "/billing";
|
||
}}
|
||
className="inline-block text-sm font-medium text-amber-600 hover:text-amber-700 underline"
|
||
>
|
||
Manage subscription →
|
||
</button>
|
||
</>
|
||
)}
|
||
{subscriptionInfo.status === "expired" && (
|
||
<>
|
||
<p className="text-sm text-red-700 font-medium mb-3">
|
||
Trial expired - Upgrade to continue
|
||
</p>
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
window.location.href = "/billing";
|
||
}}
|
||
className="inline-block px-4 py-2 bg-red-600 text-white text-sm font-medium rounded-lg hover:bg-red-700 transition"
|
||
>
|
||
Upgrade Now →
|
||
</button>
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Info Card */}
|
||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-6">
|
||
<h3 className="font-semibold text-slate-900 mb-4">ℹ️ How it works</h3>
|
||
<p className="text-sm text-slate-700 mb-4">
|
||
When a Stripe payment is received, we automatically create an invoice in Xero using:
|
||
</p>
|
||
<ul className="space-y-2 text-sm text-slate-700">
|
||
<li className="flex gap-2">
|
||
<span className="text-blue-600">→</span>
|
||
<span><strong>Sales Account:</strong> Invoice line items</span>
|
||
</li>
|
||
<li className="flex gap-2">
|
||
<span className="text-blue-600">→</span>
|
||
<span><strong>Clearing Account:</strong> Payment received</span>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
|
||
{/* Help Card */}
|
||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-6">
|
||
<h3 className="font-semibold text-slate-900 mb-4">🚀 Getting started</h3>
|
||
<ol className="space-y-2 text-sm text-slate-700">
|
||
<li><strong>1.</strong> Set the account codes above</li>
|
||
<li><strong>2.</strong> Make a test payment in Stripe</li>
|
||
<li><strong>3.</strong> Check Xero for the invoice</li>
|
||
<li><strong>4.</strong> You're ready to go!</li>
|
||
</ol>
|
||
</div>
|
||
|
||
{/* Status Card */}
|
||
{subscriptionInfo?.status === "active" ? (
|
||
<div className="bg-green-50 border border-green-200 rounded-xl p-6">
|
||
<h3 className="font-semibold text-slate-900 mb-4">✓ Status</h3>
|
||
<p className="text-sm text-green-700 font-medium mb-2">Connected & Active</p>
|
||
<p className="text-sm text-slate-600">
|
||
Your automation is ready. New Stripe payments will create invoices in Xero automatically.
|
||
</p>
|
||
</div>
|
||
) : (
|
||
<div className="bg-slate-50 border border-slate-200 rounded-xl p-6">
|
||
<h3 className="font-semibold text-slate-900 mb-4">ℹ️ Status</h3>
|
||
<p className="text-sm text-slate-700 font-medium mb-2">Ready to Use</p>
|
||
<p className="text-sm text-slate-600">
|
||
Once you subscribe, your automation will be active and Stripe payments will create invoices in Xero automatically.
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
);
|
||
}
|