import React, { useEffect, useMemo, useState } from “react”;
import {
Users,
Bell,
Upload,
CheckCircle,
AlertTriangle,
Calendar,
Search,
Clock,
Download,
Plus,
X,
Trash2,
PhoneCall,
Sparkles,
Loader2,
Send,
LogIn,
Shield,
Settings,
Home,
FileText,
LogOut,
} from “lucide-react”;
/**
* VAT Control Panel — Website (single-file React)
* ————————————————-
* What this gives you:
* – Landing page (public)
* – Login page (simple demo auth)
* – Dashboard app (your panel) with improved logic:
* ✅ clock updates (deadline banner stays correct)
* ✅ local-date safe storage (no timezone drift)
* ✅ AI retry spinner fixed (no flicker)
* ✅ call note prefilled per client
* ✅ export JSON backup
*
* Production notes:
* – Replace demo auth with real auth (Supabase / NextAuth)
* – Replace /api/generate-call-script with a real server route
*/
// ———- Small helpers ———-
const cn = (…classes) => classes.filter(Boolean).join(” “);
function todayLocalYYYYMMDD(date = new Date()) {
const yyyy = date.getFullYear();
const mm = String(date.getMonth() + 1).padStart(2, “0”);
const dd = String(date.getDate()).padStart(2, “0”);
return `${yyyy}-${mm}-${dd}`;
}
function monthKeyFrom(year, monthIndex) {
const mm = String(monthIndex + 1).padStart(2, “0”);
return `${year}-${mm}`;
}
function safeParseJSON(str, fallback) {
try {
return JSON.parse(str);
} catch {
return fallback;
}
}
// ———- UI primitives ———-
const Card = ({ className, children }) => (
{children}
);
const Pill = ({ className, children }) => (
{children}
);
const IconButton = ({ title, onClick, className, children, disabled }) => (
{children}
);
// ———- App ———-
export default function VATControlPanelWebsite() {
// — “Routing” (single-file demo) —
const [route, setRoute] = useState(() => {
const saved = localStorage.getItem(“vat_route_v1”);
return saved || “landing”; // landing | login | app
});
useEffect(() => {
localStorage.setItem(“vat_route_v1”, route);
}, [route]);
// — Demo Auth —
const [session, setSession] = useState(() => {
const saved = localStorage.getItem(“vat_session_v1”);
return saved ? safeParseJSON(saved, null) : null;
});
useEffect(() => {
localStorage.setItem(“vat_session_v1”, JSON.stringify(session));
}, [session]);
const logout = () => {
setSession(null);
setRoute(“landing”);
};
// Guard
useEffect(() => {
if (route === “app” && !session) setRoute(“login”);
}, [route, session]);
return (
{route === “landing” && (
setRoute(“login”)}
onOpenApp={() => setRoute(session ? “app” : “login”)}
/>
)}
{route === “login” && (
setRoute(“landing”)}
onSuccess={(user) => {
setSession(user);
setRoute(“app”);
}}
/>
)}
{route === “app” && session && (
setRoute(“landing”)}
onLogout={logout}
/>
)}
);
}
// ———- Landing ———-
function Landing({ onLogin, onOpenApp }) {
return (
VAT
VAT Control Panel
Client tracking • deadline control • call logs
Login
Open Dashboard
Track VAT files.
Hit every deadline.
A clean dashboard to track client submissions by month, flag overdue
accounts, log calls, and export backups. Simple, fast, and built
for real consultancy workflow.
Start Tracking
Admin Login
} title=”Deadline Alerts” desc=”Countdown + late period flags.” />
} title=”Call Logs” desc=”Notes + follow-up tracking.” />
} title=”Submission Mark” desc=”One click received status.” />
} title=”Export Backup” desc=”Download JSON anytime.” />
Preview
Monthly VAT Cycle
Made for Bangladesh workflow
Track files before the 10th, chase overdue clients from 11th–20th,
and start next month cycle after 20th.
© {new Date().getFullYear()} VAT Control Panel • Built for consultancy ops
);
}
function Feature({ icon, title, desc }) {
return (
);
}
function PreviewRow({ label, status }) {
const styles =
status === “UP TO DATE”
? “bg-green-100 text-green-700 border-green-200”
: status === “OVERDUE”
? “bg-red-100 text-red-700 border-red-200”
: “bg-amber-100 text-amber-700 border-amber-200”;
return (
{label}
{status === “UP TO DATE” ? (
) : (
)}
{status}
);
}
// ———- Login (demo) ———-
function Login({ onBack, onSuccess }) {
const [email, setEmail] = useState(() => localStorage.getItem(“vat_admin_email”) || “”);
const [password, setPassword] = useState(“”);
const [error, setError] = useState(“”);
const submit = (e) => {
e.preventDefault();
setError(“”);
// Demo auth rule:
// – first time: set password in localStorage
// – later: must match
const existing = localStorage.getItem(“vat_admin_pass”);
if (!email.trim()) return setError(“Enter your email.”);
if (password.length < 4) return setError("Password must be at least 4 characters.");
localStorage.setItem("vat_admin_email", email.trim());
if (!existing) {
localStorage.setItem("vat_admin_pass", password);
onSuccess({ email: email.trim(), role: "Admin" });
return;
}
if (password !== existing) {
return setError("Wrong password.");
}
onSuccess({ email: email.trim(), role: "Admin" });
};
return (
Back
Admin Login
Demo login (replace with real auth in production)
Production Upgrade
Make it real login
Supabase Auth (email/password) or NextAuth
Database sync so all devices see the same clients
Role access: Admin / Staff / Viewer
Server API for AI so your key stays secure
);
}
// ———- Dashboard App ———-
function DashboardApp({ user, onExit, onLogout }) {
// Settings (persisted)
const [deadlineDay, setDeadlineDay] = useState(() => Number(localStorage.getItem(“vat_deadline_day”) || 10));
const [resetDay, setResetDay] = useState(() => Number(localStorage.getItem(“vat_reset_day”) || 20));
const [firmName, setFirmName] = useState(() => localStorage.getItem(“vat_firm_name”) || “Parves & Associates”);
useEffect(() => {
localStorage.setItem(“vat_deadline_day”, String(deadlineDay));
localStorage.setItem(“vat_reset_day”, String(resetDay));
localStorage.setItem(“vat_firm_name”, firmName);
}, [deadlineDay, resetDay, firmName]);
// Live time
const [currentTime, setCurrentTime] = useState(() => new Date());
useEffect(() => {
const id = setInterval(() => setCurrentTime(new Date()), 60_000);
return () => clearInterval(id);
}, []);
const currentDay = currentTime.getDate();
const currentMonth = currentTime.getMonth();
const currentYear = currentTime.getFullYear();
// Cycle logic: after resetDay -> next month cycle
const targetMonthIndex = currentDay > resetDay ? (currentMonth + 1) % 12 : currentMonth;
const targetYear = currentDay > resetDay && currentMonth === 11 ? currentYear + 1 : currentYear;
const targetMonthName = new Date(targetYear, targetMonthIndex).toLocaleString(“default”, { month: “long” });
const targetPeriodKey = monthKeyFrom(targetYear, targetMonthIndex); // YYYY-MM
// Data persistence
const [clients, setClients] = useState(() => {
const saved = localStorage.getItem(“vat_clients_v5”);
if (saved) return safeParseJSON(saved, []);
return [
{
id: 1,
name: “Acme Corp”,
// store lastUpload as YYYY-MM-DD local
lastUpload: “2026-02-05”,
documents: 12,
lastCall: “2026-02-01”,
callNotes: “Requested missing fuel receipts”,
},
{
id: 2,
name: “Global Logistics Ltd”,
lastUpload: “N/A”,
documents: 0,
lastCall: “2026-02-08”,
callNotes: “Left voicemail regarding Q1 filing”,
},
];
});
useEffect(() => {
localStorage.setItem(“vat_clients_v5”, JSON.stringify(clients));
}, [clients]);
// UI State
const [searchTerm, setSearchTerm] = useState(“”);
const [showAddModal, setShowAddModal] = useState(false);
const [showUploadModal, setShowUploadModal] = useState(null);
const [showCallModal, setShowCallModal] = useState(null);
const [showSettings, setShowSettings] = useState(false);
const [newClientName, setNewClientName] = useState(“”);
const [callNote, setCallNote] = useState(“”);
// AI
const [isAiLoading, setIsAiLoading] = useState(false);
const [aiSuggestion, setAiSuggestion] = useState(“”);
const exportBackup = () => {
const blob = new Blob([JSON.stringify({ firmName, deadlineDay, resetDay, clients }, null, 2)], {
type: “application/json”,
});
const url = URL.createObjectURL(blob);
const a = document.createElement(“a”);
a.href = url;
a.download = `vat_backup_${todayLocalYYYYMMDD()}.json`;
a.click();
};
// — Server-side AI call (recommended) —
async function callAI(prompt, systemInstruction) {
setIsAiLoading(true);
setAiSuggestion(“”);
const delays = [1000, 2000, 4000, 8000, 16000];
try {
let lastErr = null;
for (let i = 0; i < delays.length; i++) {
try {
const response = await fetch("/api/generate-call-script", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt, systemInstruction }),
});
if (!response.ok) throw new Error(`API failed: ${response.status}`);
const result = await response.json();
// Flexible parsing (works with multiple providers)
const text =
result?.text ||
result?.candidates?.[0]?.content?.parts?.[0]?.text ||
result?.output ||
"";
setAiSuggestion(String(text).trim() || "✨ No suggestion returned.");
return;
} catch (e) {
lastErr = e;
await new Promise((res) => setTimeout(res, delays[i]));
}
}
throw lastErr;
} catch {
// Local fallback (so UI still works in demo)
setAiSuggestion(
“✨ AI Strategy (Offline Mode)\n\nHi, this is a quick VAT follow-up call.\n1) Confirm this month’s document set for filing.\n2) Ask for missing invoices/receipts (fuel, utilities, bank).\n3) Set a delivery date (today/tomorrow).\n4) Confirm any changes in sales/purchase volume.\n\nClose: ‘Please send the files today so we can submit before the deadline.’”
);
} finally {
setIsAiLoading(false);
}
}
const generateCallStrategy = (client) => {
const status = getStatus(client.lastUpload);
const systemPrompt = “You are an expert VAT consultant assistant. Keep output short, clear, and actionable.”;
const userPrompt = `Generate a short call script for client “${client.name}”.\nStatus: ${status}.\nPeriod: ${targetMonthName} ${targetYear} (${targetPeriodKey}).\nLast call notes: ${client.callNotes || “None”}.\nInclude: greeting, what to request, deadline reminder, and a firm closing line.`;
callAI(userPrompt, systemPrompt);
};
const addClient = (e) => {
e.preventDefault();
if (!newClientName.trim()) return;
const newClient = {
id: Date.now(),
name: newClientName.trim(),
lastUpload: “N/A”,
documents: 0,
lastCall: “N/A”,
callNotes: “”,
};
setClients([newClient, …clients]);
setNewClientName(“”);
setShowAddModal(false);
};
const handleUpload = (clientId) => {
const today = todayLocalYYYYMMDD();
setClients((prev) =>
prev.map((client) => {
if (client.id === clientId) {
return {
…client,
documents: (client.documents || 0) + 1,
lastUpload: today,
};
}
return client;
})
);
setShowUploadModal(null);
};
const handleCallLog = (clientId) => {
const today = todayLocalYYYYMMDD();
setClients((prev) =>
prev.map((client) => {
if (client.id === clientId) {
return {
…client,
lastCall: today,
callNotes: callNote,
};
}
return client;
})
);
setCallNote(“”);
setShowCallModal(null);
};
const deleteClient = (id) => {
if (window.confirm(“Are you sure you want to remove this client?”)) {
setClients(clients.filter((c) => c.id !== id));
}
};
// Status logic based on target period month
const getStatus = (lastUpload) => {
// never uploaded
if (lastUpload === “N/A”) {
return currentDay > deadlineDay && currentDay <= resetDay ? "OVERDUE" : "PENDING";
}
// If stored as YYYY-MM-DD
const upload = new Date(`${lastUpload}T00:00:00`);
const uploadMonth = upload.getMonth();
const uploadYear = upload.getFullYear();
const isUpToDate = uploadMonth === targetMonthIndex && uploadYear === targetYear;
if (isUpToDate) return "UP TO DATE";
if (currentDay > deadlineDay && currentDay <= resetDay) return "OVERDUE";
return "PENDING";
};
const getStatusStyles = (status) => {
switch (status) {
case “UP TO DATE”:
return “bg-green-100 text-green-700 border-green-200”;
case “OVERDUE”:
return “bg-red-100 text-red-700 border-red-200”;
default:
return “bg-amber-100 text-amber-700 border-amber-200”;
}
};
const filteredClients = useMemo(() => {
return clients.filter((client) =>
client.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [clients, searchTerm]);
// UI
return (
{/* Sidebar */}
VAT
setShowSettings(true)}
>
{/* Top bar */}
{/* Action Bar */}
{/* Main Table */}
Client
Status ({targetMonthName})
History
Last Lead Call
Actions
{filteredClients.map((client) => {
const status = getStatus(client.lastUpload);
const statusStyle = getStatusStyles(status);
const lastCallDate =
client.lastCall !== “N/A”
? new Date(`${client.lastCall}T00:00:00`)
: null;
const callOverdue =
!lastCallDate ||
new Date() – lastCallDate > 1000 * 60 * 60 * 24 * 7;
return (
{client.name.charAt(0)}
{client.name}
ID: {String(client.id).slice(-5)}
{status === “UP TO DATE” ? (
) : (
)}
{status}
{client.documents} Submissions
Last: {client.lastUpload}
{client.lastCall === “N/A”
? “No log”
: client.lastCall}
{client.callNotes && (
“{client.callNotes}”
)}
{
setShowCallModal(client);
setAiSuggestion(“”);
setCallNote(client.callNotes || “”);
}}
className=”bg-indigo-50 text-indigo-600 hover:bg-indigo-600 hover:text-white”
>
setShowUploadModal(client)}
className=”bg-slate-100 text-slate-600 hover:bg-slate-800 hover:text-white”
>
deleteClient(client.id)}
className=”text-slate-300 hover:text-red-500 shadow-none”
>
);
})}
{/* Call Log Modal */}
{showCallModal && (
Period: {targetMonthName} {targetYear}
{showCallModal.name}
{(aiSuggestion || isAiLoading) && (
AI Strategy
{isAiLoading ? (
Thinking…
) : (
{aiSuggestion}
)}
)}
setShowCallModal(null)}
className=”absolute top-6 right-6 p-2 hover:bg-slate-100 rounded-full transition-colors”
>
)}
{/* Upload Confirmation */}
{showUploadModal && (
Filing Received?
This marks {showUploadModal.name} as “Up to Date” for{” “}
{targetMonthName} {targetYear}
.
handleUpload(showUploadModal.id)}
className=”w-full py-4 bg-indigo-600 text-white rounded-2xl font-black shadow-xl mb-3″
>
Confirm Submission
setShowUploadModal(null)}
className=”text-slate-400 text-sm font-black uppercase hover:text-slate-600 transition-colors”
>
Cancel
)}
{/* Add Client Modal */}
{showAddModal && (
New Client
setShowAddModal(false)}
className=”p-2 hover:bg-slate-100 rounded-full”
>
)}
{/* Settings Modal */}
{showSettings && (
Settings
Customize your monthly cycle
Firm Name
setFirmName(e.target.value)}
className=”mt-1 w-full px-4 py-3 rounded-2xl bg-slate-50 border border-slate-200 focus:ring-2 focus:ring-indigo-500 outline-none”
/>
Current Cycle
Target: {targetMonthName} {targetYear}
Late Period: {deadlineDay + 1}–{resetDay}
Period Key: {targetPeriodKey}
Export Backup
{
if (window.confirm(“Reset all local data? This cannot be undone.”)) {
localStorage.removeItem(“vat_clients_v5″);
window.location.reload();
}
}}
className=”flex-1 py-3 rounded-2xl bg-red-50 text-red-700 border border-red-200 font-black hover:bg-red-100 transition”
>
Reset Data
setShowSettings(false)}
className=”absolute top-6 right-6 p-2 hover:bg-slate-100 rounded-full transition-colors”
>
)}
);
}