juntekim.com/stripe_to_invoice/app/dashboard/page.tsx
2026-02-21 11:19:06 +00:00

292 lines
13 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
interface SubscriptionInfo {
status: "trialing" | "active" | "expired" | "canceled";
isActive: boolean;
daysRemainingInTrial: number | null;
trialEndsAt: 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 && (
<button
onClick={() => window.location.href = "/billing"}
className={`rounded-xl p-6 border w-full text-left hover:shadow-md transition cursor-pointer ${
subscriptionInfo.status === "active"
? "bg-green-50 border-green-200"
: subscriptionInfo.status === "trialing"
? "bg-blue-50 border-blue-200"
: "bg-red-50 border-red-200"
}`}
>
<h3 className="font-semibold text-slate-900 mb-2">📅 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">
Free trial active
</p>
<p className="text-sm text-blue-700 mb-3">
{subscriptionInfo.daysRemainingInTrial} days remaining until {" "}
{subscriptionInfo.trialEndsAt
? new Date(subscriptionInfo.trialEndsAt).toLocaleDateString()
: "trial ends"}
</p>
<Link
href="/billing"
className="inline-block text-sm font-medium text-blue-600 hover:text-blue-700 underline"
>
Upgrade now
</Link>
</>
)}
{subscriptionInfo.status === "expired" && (
<>
<p className="text-sm text-red-700 font-medium mb-3">
Trial expired - Upgrade to continue
</p>
<Link
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
</Link>
</>
)}
</button>
)}
{/* 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 */}
<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>
</div>
</div>
</main>
</div>
);
}