Files
License-Tracker-Pro/App.tsx
T
Philip e045354892 feat: Initialize License Tracker Pro v1.2 project
Adds initial project structure, dependencies, and basic configuration for the License Tracker Pro v1.2 application. This includes setting up Vite for the build process, defining core types, and incorporating essential UI components like icons and a timeline. The project is now ready for development.
2026-01-26 18:55:43 -08:00

256 lines
14 KiB
TypeScript

import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { License, User, AppSettings } from './types';
import Timeline from './components/Timeline';
import LicenseList from './components/LicenseList';
import LicenseForm from './components/LicenseForm';
import Header from './components/Header';
import LoginForm from './components/LoginForm';
import UserManagement from './components/UserManagement';
import AdminSettings from './components/AdminSettings';
import AuditLogs from './components/AuditLogs';
import DashboardAlerts from './components/DashboardAlerts';
import { PlusIcon, TrashIcon } from './components/Icons';
import { fetchLicenses, saveLicense, deleteLicense, exportDatabase, importDatabase, checkSession, logout, fetchSettings, DEFAULT_SETTINGS, runNotifications } from './graphService';
const App: React.FC = () => {
const [licenses, setLicenses] = useState<License[]>([]);
const [currentUser, setCurrentUser] = useState<User | null>(null);
const [appSettings, setAppSettings] = useState<AppSettings>(DEFAULT_SETTINGS);
const [currentView, setCurrentView] = useState<string>('dashboard');
const [isModalOpen, setIsModalOpen] = useState(false);
const [showLoginModal, setShowLoginModal] = useState(false);
const [editingLicense, setEditingLicense] = useState<License | null>(null);
const [timelineDate, setTimelineDate] = useState(new Date());
const [searchQuery, setSearchQuery] = useState('');
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [sortBy, setSortBy] = useState<'name' | 'price_desc' | 'price_asc' | 'end_soon'>('name');
const [showArchived, setShowArchived] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const initApp = useCallback(async () => {
setIsLoading(true);
try {
// Trigger background notification checks
runNotifications().catch(e => console.debug("Notifications silent run:", e));
const [settings, session, data] = await Promise.all([
fetchSettings().catch(() => DEFAULT_SETTINGS),
checkSession().catch(() => ({ authenticated: false })),
fetchLicenses().catch(() => [])
]);
if (settings) {
setAppSettings({ ...DEFAULT_SETTINGS, ...settings });
}
if (session?.authenticated) setCurrentUser(session.user);
setLicenses(Array.isArray(data) ? data : []);
} catch (e) {
console.error("Init Error", e);
setLicenses([]);
} finally { setIsLoading(false); }
}, []);
useEffect(() => { initApp(); }, [initApp]);
const handleFormSubmit = async (licenseData: any) => {
try {
await saveLicense(licenseData);
const freshData = await fetchLicenses();
setLicenses(Array.isArray(freshData) ? freshData : []);
setIsModalOpen(false);
setEditingLicense(null);
} catch (err: any) {
alert("Save Failed: " + err.message);
}
};
const handleImport = async (e: React.ChangeEvent<HTMLInputElement>) => {
if(e.target.files?.[0]) {
try {
const text = await e.target.files[0].text();
const res = await importDatabase(text);
// CRITICAL: Clear all UI filters before refreshing data
setSearchQuery('');
setSelectedTags([]);
setShowArchived(false);
await initApp();
const count = res.count || 'all';
alert(`Restore Complete. ${count} licenses were successfully recovered to the repository.`);
} catch (err: any) {
alert("Restore failed: " + err.message);
} finally {
e.target.value = '';
}
}
};
const allTags = useMemo(() => {
const tags = new Set<string>();
if (Array.isArray(licenses)) {
licenses.forEach(l => l.tags?.forEach(t => tags.add(t)));
}
return Array.from(tags).sort();
}, [licenses]);
const toggleTag = (tag: string) => {
setSelectedTags(prev =>
prev.includes(tag)
? prev.filter(t => t !== tag)
: [...prev, tag]
);
};
const filteredAndSorted = useMemo(() => {
const safeLicenses = Array.isArray(licenses) ? licenses : [];
let filtered = safeLicenses.filter(l => {
if (!l) return false;
const statusMatch = showArchived ? !l.isActive : l.isActive;
if (!statusMatch) return false;
if (selectedTags.length > 0) {
const hasAllTags = selectedTags.every(tag => l.tags?.includes(tag));
if (!hasAllTags) return false;
}
const search = searchQuery.toLowerCase();
return !search ||
(l.licenseName && l.licenseName.toLowerCase().includes(search)) ||
(l.companyName && l.companyName.toLowerCase().includes(search));
});
return filtered.sort((a, b) => {
if (sortBy === 'price_desc') return b.purchasePrice - a.purchasePrice;
if (sortBy === 'price_asc') return a.purchasePrice - b.purchasePrice;
if (sortBy === 'end_soon') {
const da = a.endDate ? new Date(a.endDate).getTime() : Infinity;
const db = b.endDate ? new Date(b.endDate).getTime() : Infinity;
return da - db;
}
return (a.licenseName || '').localeCompare(b.licenseName || '');
});
}, [licenses, searchQuery, showArchived, selectedTags, sortBy]);
return (
<div className="min-h-screen bg-slate-950 text-gray-200 p-4 sm:p-8">
<div className="max-w-7xl mx-auto">
<Header
user={currentUser}
onLogout={async () => { await logout(); setCurrentUser(null); initApp(); }}
onLoginClick={() => setShowLoginModal(true)}
onExport={async () => {
try {
const j = await exportDatabase();
const jsonString = JSON.stringify(j, null, 2);
const b = new Blob([jsonString], {type:'application/json'});
const u = URL.createObjectURL(b);
const a = document.createElement('a');
a.href = u;
a.download = `licenses_backup_${new Date().toISOString().split('T')[0]}.json`;
a.click();
URL.revokeObjectURL(u);
} catch (err: any) {
alert("Export failed: " + err.message);
}
}}
onImport={handleImport}
currentView={currentView}
onChangeView={setCurrentView}
/>
{currentView === 'users' ? <UserManagement /> : currentView === 'logs' ? <AuditLogs /> : currentView === 'settings' ? <AdminSettings onSettingsUpdate={setAppSettings} /> : (
<main className="animate-in fade-in duration-500">
<DashboardAlerts licenses={filteredAndSorted} alertDays={appSettings.alert_days} />
<Timeline licenses={filteredAndSorted} timelineDate={timelineDate} onNavigate={(dir) => setTimelineDate(d => { const n = new Date(d); n.setFullYear(d.getFullYear() + (dir === 'next' ? 1 : -1)); return n; })} fiscalStartMonth={appSettings.fiscal_start_month} />
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
<h2 className="text-2xl font-black uppercase tracking-tighter text-white">{showArchived ? 'Archive' : 'Active Inventory'}</h2>
<div className="flex gap-2">
<button onClick={() => setShowArchived(!showArchived)} className={`px-4 py-2 rounded-xl text-xs font-bold uppercase tracking-widest transition-all border ${showArchived ? 'bg-cyan-600 border-cyan-500 text-white' : 'bg-slate-900 border-slate-700 hover:border-cyan-500'}`}>
{showArchived ? 'View Active' : 'View Archived'}
</button>
{currentUser && (
<button onClick={() => { setEditingLicense(null); setIsModalOpen(true); }} className="bg-cyan-600 text-white px-5 py-2 rounded-xl font-bold hover:bg-cyan-500 shadow-lg shadow-cyan-900/30 transition-all active:scale-95 flex items-center">
<PlusIcon className="mr-2 w-4 h-4"/> New License
</button>
)}
</div>
</div>
<div className="mb-6 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="md:col-span-2">
<input
placeholder="Search records by name or company..."
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
className="w-full bg-slate-900 border border-slate-800 rounded-xl px-4 py-3 text-sm focus:border-cyan-500 outline-none transition-all shadow-inner"
/>
</div>
<select
value={sortBy}
onChange={e => setSortBy(e.target.value as any)}
className="bg-slate-900 border border-slate-800 rounded-xl px-4 py-3 text-sm focus:border-cyan-500 outline-none transition-all"
>
<option value="name">Sort by Name</option>
<option value="price_desc">Price: High to Low</option>
<option value="price_asc">Price: Low to High</option>
<option value="end_soon">Expiration: Soonest</option>
</select>
</div>
<div className="bg-slate-900/40 p-4 rounded-2xl border border-slate-800/50">
<div className="flex items-center justify-between mb-3">
<label className="text-[10px] font-black uppercase tracking-widest text-slate-500">Filter by Tags</label>
{selectedTags.length > 0 && (
<button
onClick={() => setSelectedTags([])}
className="text-[10px] font-bold text-red-400 hover:text-red-300 uppercase tracking-tighter"
>
Clear Filters ({selectedTags.length})
</button>
)}
</div>
<div className="flex flex-wrap gap-2">
{allTags.length === 0 ? (
<span className="text-xs text-slate-600 italic">No tags available in current inventory.</span>
) : (
allTags.map(tag => {
const isActive = selectedTags.includes(tag);
return (
<button
key={tag}
onClick={() => toggleTag(tag)}
className={`px-3 py-1.5 rounded-lg text-[11px] font-bold transition-all border ${
isActive
? 'bg-cyan-500/20 border-cyan-500 text-cyan-300 shadow-[0_0_10px_rgba(34,211,238,0.2)]'
: 'bg-slate-800/50 border-slate-700 text-slate-400 hover:border-slate-500 hover:text-slate-200'
}`}
>
{tag}
</button>
);
})
)}
</div>
</div>
</div>
{isLoading ? (
<div className="text-center py-20 text-cyan-400 font-mono text-sm animate-pulse tracking-widest">LOADING REPOSITORY...</div>
) : (
<LicenseList licenses={filteredAndSorted} onEdit={(l) => { setEditingLicense(l); setIsModalOpen(true); }} onDelete={async (id) => { if(confirm('Delete?')) { await deleteLicense(id); initApp(); } }} isReadOnly={!currentUser} />
)}
</main>
)}
</div>
<LicenseForm isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} onSubmit={handleFormSubmit} initialData={editingLicense} />
{showLoginModal && <LoginForm settings={appSettings} onLoginSuccess={(u) => { setCurrentUser(u); setShowLoginModal(false); initApp(); }} onCancel={() => setShowLoginModal(false)} />}
</div>
);
};
export default App;