theme-provider #20

Merged
andika merged 12 commits from theme-provider into dev 2026-01-27 08:41:49 +00:00
3 changed files with 330 additions and 107 deletions
Showing only changes of commit 333ce62710 - Show all commits

View File

@@ -350,44 +350,83 @@ const AdminRoles = () => {
Select permissions for this role. You can also add permissions later. Select permissions for this role. You can also add permissions later.
</p> </p>
<div className="border rounded-lg p-4 max-h-64 overflow-y-auto scrollbar-dashboard"> <div className="border rounded-lg p-4 max-h-64 overflow-y-auto scrollbar-dashboard">
{Object.entries(groupedPermissions).map(([module, perms]) => ( {Object.entries(groupedPermissions).map(([module, perms]) => {
<div key={module} className="mb-4"> const moduleCodes = perms.map(perm => perm.code);
<button const selectedCount = moduleCodes.filter(code => formData.permissions.includes(code)).length;
onClick={() => toggleModule(module)} const hasPermissions = moduleCodes.length > 0;
className="flex items-center w-full text-left font-medium mb-2 hover:text-blue-600" const isAllSelected = hasPermissions && selectedCount === moduleCodes.length;
> const isNoneSelected = selectedCount === 0;
{expandedModules[module] ? (
<ChevronUp className="w-4 h-4 mr-1" /> return (
) : ( <div key={module} className="mb-4">
<ChevronDown className="w-4 h-4 mr-1" /> <div className="flex items-center justify-between mb-2">
)} <button
{module.charAt(0).toUpperCase() + module.slice(1)} ({perms.length}) type="button"
</button> onClick={() => toggleModule(module)}
{expandedModules[module] && ( className="flex items-center text-left font-medium hover:text-blue-600"
<div className="space-y-2 ml-5"> >
{perms.map(perm => ( {expandedModules[module] ? (
<div key={perm.code} className="flex items-center"> <ChevronUp className="w-4 h-4 mr-1" />
<Checkbox ) : (
checked={formData.permissions.includes(perm.code)} <ChevronDown className="w-4 h-4 mr-1" />
onCheckedChange={() => { )}
setFormData(prev => ({ {module.charAt(0).toUpperCase() + module.slice(1)} ({perms.length})
...prev, </button>
permissions: prev.permissions.includes(perm.code) <div className="flex items-center gap-3">
? prev.permissions.filter(p => p !== perm.code) <button
: [...prev.permissions, perm.code] type="button"
})); onClick={() => {
}} setFormData(prev => ({
/> ...prev,
<label className="ml-2 text-sm"> permissions: [...new Set([...prev.permissions, ...moduleCodes])]
<span className="font-medium">{perm.name}</span> }));
<span className="text-gray-500 ml-2">({perm.code})</span> }}
</label> disabled={!hasPermissions || isAllSelected}
</div> className="text-xs font-medium text-gray-500 hover:text-brand-purple disabled:opacity-50 disabled:cursor-not-allowed"
))} >
Select all
</button>
<button
type="button"
onClick={() => {
setFormData(prev => ({
...prev,
permissions: prev.permissions.filter(code => !moduleCodes.includes(code))
}));
}}
disabled={!hasPermissions || isNoneSelected}
className="text-xs font-medium text-gray-500 hover:text-brand-purple disabled:opacity-50 disabled:cursor-not-allowed"
>
Deselect all
</button>
</div>
</div> </div>
)} {expandedModules[module] && (
</div> <div className="space-y-2 ml-5">
))} {perms.map(perm => (
<div key={perm.code} className="flex items-center">
<Checkbox
checked={formData.permissions.includes(perm.code)}
onCheckedChange={() => {
setFormData(prev => ({
...prev,
permissions: prev.permissions.includes(perm.code)
? prev.permissions.filter(p => p !== perm.code)
: [...prev.permissions, perm.code]
}));
}}
/>
<label className="ml-2 text-sm">
<span className="font-medium">{perm.name}</span>
<span className="text-gray-500 ml-2">({perm.code})</span>
</label>
</div>
))}
</div>
)}
</div>
);
})}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -5,20 +5,34 @@ import MemberFooter from '../../components/MemberFooter';
import { Card } from '../../components/ui/card'; import { Card } from '../../components/ui/card';
import { Button } from '../../components/ui/button'; import { Button } from '../../components/ui/button';
import { Badge } from '../../components/ui/badge'; import { Badge } from '../../components/ui/badge';
import { Input } from '../../components/ui/input';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { Scale, ExternalLink, History, Check } from 'lucide-react'; import { Scale, ExternalLink, History, Check, Search, Calendar } from 'lucide-react';
export default function Bylaws() { export default function Bylaws() {
const [currentBylaws, setCurrentBylaws] = useState(null); const [currentBylaws, setCurrentBylaws] = useState(null);
const [history, setHistory] = useState([]); const [history, setHistory] = useState([]);
const [years, setYears] = useState([]);
const [selectedYear, setSelectedYear] = useState(null);
const [searchTerm, setSearchTerm] = useState('');
const [showHistory, setShowHistory] = useState(false); const [showHistory, setShowHistory] = useState(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
fetchCurrentBylaws(); fetchCurrentBylaws();
fetchYears();
fetchHistory(); fetchHistory();
}, []); }, []);
const fetchYears = async () => {
try {
const response = await api.get('/bylaws/years');
setYears(response.data);
} catch (error) {
console.error('Failed to load years');
}
};
const fetchCurrentBylaws = async () => { const fetchCurrentBylaws = async () => {
try { try {
const response = await api.get('/bylaws/current'); const response = await api.get('/bylaws/current');
@@ -32,15 +46,46 @@ export default function Bylaws() {
} }
}; };
const fetchHistory = async () => { const fetchHistory = async (year = null) => {
try { try {
const response = await api.get('/bylaws/history'); const url = year ? `/bylaws/history?year=${year}` : '/bylaws/history';
const response = await api.get(url);
setHistory(response.data); setHistory(response.data);
} catch (error) { } catch (error) {
console.error('Failed to load bylaws history'); console.error('Failed to load bylaws history');
} }
}; };
const handleYearFilter = (year) => {
setSelectedYear(year);
fetchHistory(year);
};
const clearFilter = () => {
setSelectedYear(null);
fetchHistory();
};
const filteredHistory = history.filter(bylaws =>
bylaws.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
(bylaws.version && bylaws.version.toLowerCase().includes(searchTerm.toLowerCase()))
);
const groupByYear = (items) => {
const grouped = {};
items.forEach(item => {
const year = new Date(item.effective_date).getFullYear();
if (!grouped[year]) {
grouped[year] = [];
}
grouped[year].push(item);
});
return grouped;
};
const groupedHistory = groupByYear(filteredHistory.filter(b => !b.is_current));
const sortedYears = Object.keys(groupedHistory).sort((a, b) => b - a);
const formatDate = (dateString) => { const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('en-US', { return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric', year: 'numeric',
@@ -75,6 +120,44 @@ export default function Bylaws() {
<p className="text-brand-purple mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Review the official governing bylaws and policies of the LOAF community. Review the official governing bylaws and policies of the LOAF community.
</p> </p>
{/* Filters */}
<div className="flex gap-4 flex-wrap items-center">
{/* Search */}
<div className="relative flex-1 min-w-[300px]">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-brand-purple " />
<Input
type="text"
placeholder="Search bylaws..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 border-[var(--neutral-800)] focus:border-brand-purple "
/>
</div>
{/* Year Filter */}
<div className="flex gap-2 flex-wrap">
<Button
onClick={clearFilter}
variant={selectedYear === null ? "default" : "outline"}
size="sm"
className={selectedYear === null ? "bg-brand-purple hover:bg-[var(--purple-muted)] text-white" : "border-brand-lavender text-brand-lavender "}
>
All Years
</Button>
{years.map(year => (
<Button
key={year}
onClick={() => handleYearFilter(year)}
variant={selectedYear === year ? "default" : "outline"}
size="sm"
className={selectedYear === year ? "bg-brand-purple text-white" : "border-brand-purple text-brand-purple "}
>
{year}
</Button>
))}
</div>
</div>
</div> </div>
{/* Current Bylaws */} {/* Current Bylaws */}
@@ -124,7 +207,7 @@ export default function Bylaws() {
)} )}
{/* Version History Toggle */} {/* Version History Toggle */}
{history.length > 1 && ( {filteredHistory.filter(b => !b.is_current).length > 0 && (
<div className="mb-6"> <div className="mb-6">
<Button <Button
onClick={() => setShowHistory(!showHistory)} onClick={() => setShowHistory(!showHistory)}
@@ -132,41 +215,48 @@ export default function Bylaws() {
className="w-full border-[var(--neutral-800)] text-brand-purple hover:bg-[var(--lavender-300)] rounded-full flex items-center justify-center gap-2" className="w-full border-[var(--neutral-800)] text-brand-purple hover:bg-[var(--lavender-300)] rounded-full flex items-center justify-center gap-2"
> >
<History className="h-4 w-4" /> <History className="h-4 w-4" />
{showHistory ? 'Hide' : 'View'} Version History ({history.length - 1} previous {history.length - 1 === 1 ? 'version' : 'versions'}) {showHistory ? 'Hide' : 'View'} Version History ({filteredHistory.filter(b => !b.is_current).length} previous {filteredHistory.filter(b => !b.is_current).length === 1 ? 'version' : 'versions'})
</Button> </Button>
</div> </div>
)} )}
{/* Version History */} {/* Version History */}
{showHistory && history.length > 1 && ( {showHistory && filteredHistory.filter(b => !b.is_current).length > 0 && (
<div className="space-y-4"> <div className="space-y-6">
<h3 className="text-xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}> {sortedYears.map(year => (
Previous Versions <div key={year}>
</h3> <h3 className="text-xl font-semibold text-[var(--purple-ink)] mb-4 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
{history.filter(b => !b.is_current).map(bylaws => ( <Calendar className="h-5 w-5" />
<Card key={bylaws.id} className="p-6 bg-[var(--lavender-600)] rounded-xl border border-[var(--neutral-800)]"> {year}
<div className="flex items-center justify-between"> </h3>
<div> <div className="space-y-4">
<h4 className="text-lg font-semibold text-[var(--purple-ink)] mb-1" style={{ fontFamily: "'Inter', sans-serif" }}> {groupedHistory[year].map(bylaws => (
{bylaws.title} <Card key={bylaws.id} className="p-6 bg-[var(--lavender-600)] rounded-xl border border-[var(--neutral-800)]">
</h4> <div className="flex items-center justify-between">
<div className="flex items-center gap-3 text-sm text-brand-purple "> <div>
<span>Version {bylaws.version}</span> <h4 className="text-lg font-semibold text-[var(--purple-ink)] mb-1" style={{ fontFamily: "'Inter', sans-serif" }}>
<span></span> {bylaws.title}
<span>Effective {formatDate(bylaws.effective_date)}</span> </h4>
</div> <div className="flex items-center gap-3 text-sm text-brand-purple ">
</div> <span>Version {bylaws.version}</span>
<Button <span></span>
onClick={() => window.open(bylaws.document_url, '_blank')} <span>Effective {formatDate(bylaws.effective_date)}</span>
variant="outline" </div>
size="sm" </div>
className="border-brand-purple text-brand-purple hover:bg-[var(--lavender-300)] rounded-full flex items-center gap-2" <Button
> onClick={() => window.open(bylaws.document_url, '_blank')}
<ExternalLink className="h-4 w-4" /> variant="outline"
View size="sm"
</Button> className="border-brand-purple text-brand-purple hover:bg-[var(--lavender-300)] rounded-full flex items-center gap-2"
>
<ExternalLink className="h-4 w-4" />
View
</Button>
</div>
</Card>
))}
</div> </div>
</Card> </div>
))} ))}
</div> </div>
)} )}

View File

@@ -5,20 +5,36 @@ import MemberFooter from '../../components/MemberFooter';
import { Card } from '../../components/ui/card'; import { Card } from '../../components/ui/card';
import { Button } from '../../components/ui/button'; import { Button } from '../../components/ui/button';
import { Badge } from '../../components/ui/badge'; import { Badge } from '../../components/ui/badge';
import { Input } from '../../components/ui/input';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { DollarSign, ExternalLink, TrendingUp } from 'lucide-react'; import { DollarSign, ExternalLink, TrendingUp, Search, Calendar } from 'lucide-react';
export default function Financials() { export default function Financials() {
const [reports, setReports] = useState([]); const [reports, setReports] = useState([]);
const [years, setYears] = useState([]);
const [selectedYear, setSelectedYear] = useState(null);
const [searchTerm, setSearchTerm] = useState('');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
fetchYears();
fetchReports(); fetchReports();
}, []); }, []);
const fetchReports = async () => { const fetchYears = async () => {
try { try {
const response = await api.get('/financials'); const response = await api.get('/financials/years');
setYears(response.data);
} catch (error) {
console.error('Failed to load years');
}
};
const fetchReports = async (year = null) => {
try {
setLoading(true);
const url = year ? `/financials?year=${year}` : '/financials';
const response = await api.get(url);
setReports(response.data); setReports(response.data);
} catch (error) { } catch (error) {
toast.error('Failed to load financial reports'); toast.error('Failed to load financial reports');
@@ -27,6 +43,36 @@ export default function Financials() {
} }
}; };
const handleYearFilter = (year) => {
setSelectedYear(year);
fetchReports(year);
};
const clearFilter = () => {
setSelectedYear(null);
fetchReports();
};
const filteredReports = reports.filter(report =>
report.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
report.year.toString().includes(searchTerm)
);
const groupByYear = (items) => {
const grouped = {};
items.forEach(item => {
const year = item.year;
if (!grouped[year]) {
grouped[year] = [];
}
grouped[year].push(item);
});
return grouped;
};
const groupedReports = groupByYear(filteredReports);
const sortedYears = Object.keys(groupedReports).sort((a, b) => b - a);
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-background"> <div className="min-h-screen bg-background">
@@ -53,56 +99,104 @@ export default function Financials() {
<p className="text-brand-purple mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple mb-6" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
Access annual financial reports and stay informed about LOAF's fiscal responsibility. Access annual financial reports and stay informed about LOAF's fiscal responsibility.
</p> </p>
{/* Filters */}
<div className="flex gap-4 flex-wrap items-center">
{/* Search */}
<div className="relative flex-1 min-w-[300px]">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-brand-purple " />
<Input
type="text"
placeholder="Search financial reports..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 border-[var(--neutral-800)] focus:border-brand-purple "
/>
</div>
{/* Year Filter */}
<div className="flex gap-2 flex-wrap">
<Button
onClick={clearFilter}
variant={selectedYear === null ? "default" : "outline"}
size="sm"
className={selectedYear === null ? "bg-brand-purple hover:bg-[var(--purple-muted)] text-white" : "border-brand-lavender text-brand-lavender "}
>
All Years
</Button>
{years.map(year => (
<Button
key={year}
onClick={() => handleYearFilter(year)}
variant={selectedYear === year ? "default" : "outline"}
size="sm"
className={selectedYear === year ? "bg-brand-purple text-white" : "border-brand-purple text-brand-purple "}
>
{year}
</Button>
))}
</div>
</div>
</div> </div>
{/* Reports List */} {/* Reports List */}
{reports.length === 0 ? ( {filteredReports.length === 0 ? (
<Card className="p-12 text-center bg-background rounded-2xl border border-[var(--neutral-800)]"> <Card className="p-12 text-center bg-background rounded-2xl border border-[var(--neutral-800)]">
<TrendingUp className="h-16 w-16 text-[var(--neutral-800)] mx-auto mb-4" /> <TrendingUp className="h-16 w-16 text-[var(--neutral-800)] mx-auto mb-4" />
<p className="text-brand-purple text-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}> <p className="text-brand-purple text-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
No financial reports available yet No financial reports found
</p> </p>
</Card> </Card>
) : ( ) : (
<div className="space-y-6"> <div className="space-y-8">
{reports.map(report => ( {sortedYears.map(year => (
<Card key={report.id} className="p-8 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-lg transition-shadow"> <div key={year}>
<div className="flex items-center gap-6"> <h2 className="text-2xl font-semibold text-[var(--purple-ink)] mb-4 flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
{/* Year Badge */} <Calendar className="h-6 w-6" />
<div className="bg-gradient-to-br from-brand-purple to-[var(--purple-ink)] p-6 rounded-xl text-white min-w-[120px] text-center"> {year}
<DollarSign className="h-8 w-8 mx-auto mb-2" /> </h2>
<div className="text-3xl font-bold" style={{ fontFamily: "'Inter', sans-serif" }}> <div className="space-y-6">
{report.year} {groupedReports[year].map(report => (
</div> <Card key={report.id} className="p-8 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-lg transition-shadow">
<div className="text-sm opacity-90">Fiscal Year</div> <div className="flex items-center gap-6">
</div> {/* Year Badge */}
<div className="bg-gradient-to-br from-brand-purple to-[var(--purple-ink)] p-6 rounded-xl text-white min-w-[120px] text-center">
<DollarSign className="h-8 w-8 mx-auto mb-2" />
<div className="text-3xl font-bold" style={{ fontFamily: "'Inter', sans-serif" }}>
{report.year}
</div>
<div className="text-sm opacity-90">Fiscal Year</div>
</div>
{/* Report Details */} {/* Report Details */}
<div className="flex-1"> <div className="flex-1">
<h3 className="text-2xl font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}> <h3 className="text-2xl font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
{report.title} {report.title}
</h3> </h3>
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<Badge variant="outline" className="border-brand-purple text-brand-purple "> <Badge variant="outline" className="border-brand-purple text-brand-purple ">
{report.document_type === 'google_drive' ? 'Google Drive' : report.document_type.toUpperCase()} {report.document_type === 'google_drive' ? 'Google Drive' : report.document_type.toUpperCase()}
</Badge> </Badge>
</div> </div>
<Button <Button
onClick={() => window.open(report.document_url, '_blank')} onClick={() => window.open(report.document_url, '_blank')}
className="bg-brand-purple text-white hover:bg-[var(--purple-muted)] rounded-full flex items-center gap-2" className="bg-brand-purple text-white hover:bg-[var(--purple-muted)] rounded-full flex items-center gap-2"
> >
<ExternalLink className="h-4 w-4" /> <ExternalLink className="h-4 w-4" />
View Report View Report
</Button> </Button>
</div> </div>
</div>
</Card>
))}
</div> </div>
</Card> </div>
))} ))}
</div> </div>
)} )}
{/* Transparency Note */} {/* Transparency Note */}
{reports.length > 0 && ( {filteredReports.length > 0 && (
<Card className="mt-8 p-6 bg-[var(--lavender-600)] border border-[var(--neutral-800)]"> <Card className="mt-8 p-6 bg-[var(--lavender-600)] border border-[var(--neutral-800)]">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<TrendingUp className="h-5 w-5 text-brand-purple mt-1" /> <TrendingUp className="h-5 w-5 text-brand-purple mt-1" />