Update index.html
Browse files- index.html +243 -142
index.html
CHANGED
|
@@ -3,11 +3,10 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>إدارة مالية النادي - ال
|
| 7 |
<script src="https://cdn.tailwindcss.com"></script>
|
|
|
|
| 8 |
<script src="https://unpkg.com/lucide@latest"></script>
|
| 9 |
-
<!-- إضافة Babel لتحويل JSX -->
|
| 10 |
-
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
| 11 |
<style>
|
| 12 |
@import url('https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;800&display=swap');
|
| 13 |
body { font-family: 'Cairo', sans-serif; font-size: 13px; }
|
|
@@ -21,204 +20,305 @@
|
|
| 21 |
<body class="bg-slate-50 text-right text-slate-700">
|
| 22 |
<div id="root"></div>
|
| 23 |
|
| 24 |
-
<!-- Firebase SDKs -->
|
| 25 |
-
<script type="module">
|
| 26 |
-
import { initializeApp } from "https://www.gstatic.com/firebasejs/11.1.0/firebase-app.js";
|
| 27 |
-
import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from "https://www.gstatic.com/firebasejs/11.1.0/firebase-auth.js";
|
| 28 |
-
import { getFirestore, collection, doc, onSnapshot, addDoc, deleteDoc, updateDoc } from "https://www.gstatic.com/firebasejs/11.1.0/firebase-firestore.js";
|
| 29 |
-
|
| 30 |
-
// جعل Firebase متاحاً لسكربت Babel
|
| 31 |
-
window.firebaseDocs = { collection, doc, onSnapshot, addDoc, deleteDoc, updateDoc, getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken, getFirestore, initializeApp };
|
| 32 |
-
</script>
|
| 33 |
-
|
| 34 |
<script type="text/babel">
|
| 35 |
const { useState, useEffect } = React;
|
| 36 |
-
const { initializeApp, getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken, getFirestore, collection, doc, onSnapshot, addDoc, deleteDoc, updateDoc } = window.firebaseDocs;
|
| 37 |
-
|
| 38 |
-
// Firebase Configuration
|
| 39 |
-
const firebaseConfig = JSON.parse(__firebase_config);
|
| 40 |
-
const app = initializeApp(firebaseConfig);
|
| 41 |
-
const auth = getAuth(app);
|
| 42 |
-
const db = getFirestore(app);
|
| 43 |
-
const appId = typeof __app_id !== 'undefined' ? __app_id : 'club-finance-prod';
|
| 44 |
|
| 45 |
const App = () => {
|
| 46 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
const [isAdmin, setIsAdmin] = useState(false);
|
| 48 |
const [showLoginModal, setShowLoginModal] = useState(false);
|
| 49 |
const [passwordInput, setPasswordInput] = useState('');
|
| 50 |
const [loginError, setLoginError] = useState(false);
|
| 51 |
const [activeTab, setActiveTab] = useState('dashboard');
|
| 52 |
|
| 53 |
-
const [members, setMembers] = useState(
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
const [tripOneForm, setTripOneForm] = useState({ name: '', fee: '', paid: false });
|
| 62 |
|
| 63 |
const CORRECT_PASSWORD = "CLUB@2976";
|
| 64 |
-
|
| 65 |
-
useEffect(() => {
|
| 66 |
-
const initAuth = async () => {
|
| 67 |
-
if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) {
|
| 68 |
-
await signInWithCustomToken(auth, __initial_auth_token);
|
| 69 |
-
} else {
|
| 70 |
-
await signInAnonymously(auth);
|
| 71 |
-
}
|
| 72 |
-
};
|
| 73 |
-
initAuth();
|
| 74 |
-
return onAuthStateChanged(auth, setUser);
|
| 75 |
-
}, []);
|
| 76 |
-
|
| 77 |
-
useEffect(() => {
|
| 78 |
-
if (!user) return;
|
| 79 |
-
const getPath = (name) => collection(db, 'artifacts', appId, 'public', 'data', name);
|
| 80 |
-
const unsubers = [
|
| 81 |
-
onSnapshot(getPath('members'), (s) => setMembers(s.docs.map(d => ({id: d.id, ...d.data()})))),
|
| 82 |
-
onSnapshot(getPath('trips'), (s) => setTrips(s.docs.map(d => ({id: d.id, ...d.data()})))),
|
| 83 |
-
onSnapshot(getPath('activities'), (s) => setActivities(s.docs.map(d => ({id: d.id, ...d.data()})))),
|
| 84 |
-
onSnapshot(getPath('tripOne'), (s) => setTripOne(s.docs.map(d => ({id: d.id, ...d.data()})))),
|
| 85 |
-
];
|
| 86 |
-
return () => unsubers.forEach(un => un());
|
| 87 |
-
}, [user]);
|
| 88 |
-
|
| 89 |
-
useEffect(() => { if(window.lucide) window.lucide.createIcons(); }, [activeTab, isAdmin, showLoginModal, members, tripOne]);
|
| 90 |
|
| 91 |
const handleLogin = (e) => {
|
| 92 |
e.preventDefault();
|
| 93 |
if (passwordInput === CORRECT_PASSWORD) {
|
| 94 |
-
setIsAdmin(true);
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
};
|
| 97 |
|
| 98 |
-
const
|
| 99 |
-
const
|
| 100 |
-
const
|
| 101 |
-
|
| 102 |
-
const
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
const
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
|
| 115 |
return (
|
| 116 |
-
<div className="min-h-screen bg-slate-50 flex flex-col md:flex-row-reverse">
|
|
|
|
| 117 |
{showLoginModal && (
|
| 118 |
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/
|
| 119 |
-
<div className="bg-white rounded-
|
| 120 |
-
<
|
| 121 |
-
|
| 122 |
-
<
|
| 123 |
-
|
| 124 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
</form>
|
| 126 |
-
{loginError && <p className="text-rose-500 text-[
|
| 127 |
-
<button onClick={() => setShowLoginModal(false)} className="mt-4 text-slate-400 text-[11px] font-bold">إغلاق</button>
|
| 128 |
</div>
|
| 129 |
</div>
|
| 130 |
)}
|
| 131 |
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
<
|
|
|
|
|
|
|
|
|
|
| 136 |
</div>
|
| 137 |
-
<nav className="
|
| 138 |
-
<SidebarItem id="dashboard"
|
| 139 |
-
<SidebarItem id="members"
|
| 140 |
-
<SidebarItem id="trip_one"
|
| 141 |
-
<SidebarItem id="trips"
|
| 142 |
-
<SidebarItem id="activities"
|
|
|
|
| 143 |
</nav>
|
| 144 |
-
<div className="mt-
|
| 145 |
{isAdmin ? (
|
| 146 |
-
<button onClick={() => setIsAdmin(false)} className="w-full flex items-center
|
|
|
|
|
|
|
| 147 |
) : (
|
| 148 |
-
<button onClick={() => setShowLoginModal(true)} className="w-full flex items-center
|
|
|
|
|
|
|
| 149 |
)}
|
|
|
|
|
|
|
|
|
|
| 150 |
</div>
|
| 151 |
</aside>
|
| 152 |
|
| 153 |
-
|
|
|
|
| 154 |
{activeTab === 'dashboard' && (
|
| 155 |
-
<div className="space-y-
|
| 156 |
-
<header>
|
| 157 |
-
<
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
| 161 |
-
<div className="bg-emerald-50 p-5 rounded-[2rem] border border-white shadow-sm">
|
| 162 |
-
<p className="text-[11px] font-extrabold mb-1 text-emerald-700 uppercase">انخراطات</p>
|
| 163 |
-
<h3 className="text-xl font-extrabold text-emerald-700">{totalMembers} MAD</h3>
|
| 164 |
-
</div>
|
| 165 |
-
<div className="bg-blue-50 p-5 rounded-[2rem] border border-white shadow-sm">
|
| 166 |
-
<p className="text-[11px] font-extrabold mb-1 text-blue-700 uppercase">الرحلة 1</p>
|
| 167 |
-
<h3 className="text-xl font-extrabold text-blue-700">{totalTripOne} MAD</h3>
|
| 168 |
-
</div>
|
| 169 |
-
<div className="bg-purple-50 p-5 rounded-[2rem] border border-white shadow-sm">
|
| 170 |
-
<p className="text-[11px] font-extrabold mb-1 text-purple-700 uppercase">أربا�� الرحلات</p>
|
| 171 |
-
<h3 className="text-xl font-extrabold text-purple-700">{totalTripsNet} MAD</h3>
|
| 172 |
-
</div>
|
| 173 |
-
<div className="bg-rose-50 p-5 rounded-[2rem] border border-white shadow-sm">
|
| 174 |
-
<p className="text-[11px] font-extrabold mb-1 text-rose-700 uppercase">المصاريف</p>
|
| 175 |
-
<h3 className="text-xl font-extrabold text-rose-700">{totalExpenses} MAD</h3>
|
| 176 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
</div>
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
<
|
|
|
|
|
|
|
| 181 |
</div>
|
| 182 |
</div>
|
| 183 |
)}
|
| 184 |
|
| 185 |
{activeTab === 'members' && (
|
| 186 |
-
<div className="space-y-
|
| 187 |
-
<h2 className="text-
|
| 188 |
{isAdmin && (
|
| 189 |
-
<div className="bg-white p-
|
| 190 |
-
<input type="text" placeholder="الاسم" value={memberForm.name} onChange={e=>setMemberForm({...memberForm, name: e.target.value})} className="p-
|
| 191 |
-
<input type="number" placeholder="المبلغ" value={memberForm.amount} onChange={e=>setMemberForm({...memberForm, amount: e.target.value})} className="p-
|
| 192 |
-
<
|
|
|
|
| 193 |
</div>
|
| 194 |
)}
|
| 195 |
-
<div className="bg-white rounded-
|
| 196 |
-
<table className="w-full text-right">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
</div>
|
| 198 |
</div>
|
| 199 |
)}
|
| 200 |
|
| 201 |
{activeTab === 'trip_one' && (
|
| 202 |
-
<div className="space-y-
|
| 203 |
-
<h2 className="text-
|
| 204 |
{isAdmin && (
|
| 205 |
-
<div className="bg-white p-
|
| 206 |
-
<input type="text" placeholder="الاسم" value={tripOneForm.name} onChange={e=>setTripOneForm({...tripOneForm, name: e.target.value})} className="p-
|
| 207 |
-
<input type="number" placeholder="المبلغ" value={tripOneForm.fee} onChange={e=>setTripOneForm({...tripOneForm, fee: e.target.value})} className="p-
|
| 208 |
-
<div className="flex items-center gap-2 px-
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
| 210 |
</div>
|
| 211 |
)}
|
| 212 |
-
<div className="bg-white rounded-
|
| 213 |
-
<table className="w-full text-right">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
</div>
|
| 215 |
</div>
|
| 216 |
)}
|
| 217 |
-
|
| 218 |
-
{/* باقي الأقسام تتبع نفس المنطق */}
|
| 219 |
-
{activeTab === 'trips' && <div className="p-10 text-center text-slate-400 font-bold italic">قسم الرحلات - يرجى إضافة البيانات سحابياً</div>}
|
| 220 |
-
{activeTab === 'activities' && <div className="p-10 text-center text-slate-400 font-bold italic">قسم الأنشطة - يرجى إضافة البيانات سحابياً</div>}
|
| 221 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
</main>
|
| 223 |
</div>
|
| 224 |
);
|
|
@@ -230,5 +330,6 @@
|
|
| 230 |
|
| 231 |
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
| 232 |
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
|
|
|
| 233 |
</body>
|
| 234 |
</html>
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>إدارة مالية النادي - الواجهة الذكية</title>
|
| 7 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
|
| 9 |
<script src="https://unpkg.com/lucide@latest"></script>
|
|
|
|
|
|
|
| 10 |
<style>
|
| 11 |
@import url('https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;800&display=swap');
|
| 12 |
body { font-family: 'Cairo', sans-serif; font-size: 13px; }
|
|
|
|
| 20 |
<body class="bg-slate-50 text-right text-slate-700">
|
| 21 |
<div id="root"></div>
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
<script type="text/babel">
|
| 24 |
const { useState, useEffect } = React;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
const App = () => {
|
| 27 |
+
const STORAGE_KEYS = {
|
| 28 |
+
MEMBERS: 'club_members_data',
|
| 29 |
+
TRIPS: 'club_trips_data',
|
| 30 |
+
ACTIVITIES: 'club_activities_data',
|
| 31 |
+
TRIP_ONE: 'club_trip_one_data'
|
| 32 |
+
};
|
| 33 |
+
|
| 34 |
const [isAdmin, setIsAdmin] = useState(false);
|
| 35 |
const [showLoginModal, setShowLoginModal] = useState(false);
|
| 36 |
const [passwordInput, setPasswordInput] = useState('');
|
| 37 |
const [loginError, setLoginError] = useState(false);
|
| 38 |
const [activeTab, setActiveTab] = useState('dashboard');
|
| 39 |
|
| 40 |
+
const [members, setMembers] = useState(() => {
|
| 41 |
+
const saved = localStorage.getItem(STORAGE_KEYS.MEMBERS);
|
| 42 |
+
return saved ? JSON.parse(saved) : [];
|
| 43 |
+
});
|
| 44 |
+
const [trips, setTrips] = useState(() => {
|
| 45 |
+
const saved = localStorage.getItem(STORAGE_KEYS.TRIPS);
|
| 46 |
+
return saved ? JSON.parse(saved) : [];
|
| 47 |
+
});
|
| 48 |
+
const [activities, setActivities] = useState(() => {
|
| 49 |
+
const saved = localStorage.getItem(STORAGE_KEYS.ACTIVITIES);
|
| 50 |
+
return saved ? JSON.parse(saved) : [];
|
| 51 |
+
});
|
| 52 |
+
const [tripOneRegistrations, setTripOneRegistrations] = useState(() => {
|
| 53 |
+
const saved = localStorage.getItem(STORAGE_KEYS.TRIP_ONE);
|
| 54 |
+
return saved ? JSON.parse(saved) : [];
|
| 55 |
+
});
|
| 56 |
|
| 57 |
+
useEffect(() => localStorage.setItem(STORAGE_KEYS.MEMBERS, JSON.stringify(members)), [members]);
|
| 58 |
+
useEffect(() => localStorage.setItem(STORAGE_KEYS.TRIPS, JSON.stringify(trips)), [trips]);
|
| 59 |
+
useEffect(() => localStorage.setItem(STORAGE_KEYS.ACTIVITIES, JSON.stringify(activities)), [activities]);
|
| 60 |
+
useEffect(() => localStorage.setItem(STORAGE_KEYS.TRIP_ONE, JSON.stringify(tripOneRegistrations)), [tripOneRegistrations]);
|
| 61 |
+
|
| 62 |
+
const [memberForm, setMemberForm] = useState({ name: '', amount: '', date: '' });
|
| 63 |
+
const [tripForm, setTripForm] = useState({ destination: '', cost: '', income: '', date: '' });
|
| 64 |
+
const [activityForm, setActivityForm] = useState({ title: '', expenses: '', date: '' });
|
| 65 |
const [tripOneForm, setTripOneForm] = useState({ name: '', fee: '', paid: false });
|
| 66 |
|
| 67 |
const CORRECT_PASSWORD = "CLUB@2976";
|
| 68 |
+
const WHATSAPP_LINK = "https://wa.me/message/P3DXRV6NTZ63L1";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
const handleLogin = (e) => {
|
| 71 |
e.preventDefault();
|
| 72 |
if (passwordInput === CORRECT_PASSWORD) {
|
| 73 |
+
setIsAdmin(true);
|
| 74 |
+
setShowLoginModal(false);
|
| 75 |
+
setLoginError(false);
|
| 76 |
+
setPasswordInput('');
|
| 77 |
+
} else {
|
| 78 |
+
setLoginError(true);
|
| 79 |
+
}
|
| 80 |
};
|
| 81 |
|
| 82 |
+
const totalMemberIncome = members.reduce((acc, curr) => acc + (parseFloat(curr.amount) || 0), 0);
|
| 83 |
+
const tripOneIncome = tripOneRegistrations.reduce((acc, curr) => curr.paid ? acc + (parseFloat(curr.fee) || 0) : acc, 0);
|
| 84 |
+
const totalTripBalance = trips.reduce((acc, curr) => acc + ((parseFloat(curr.income) || 0) - (parseFloat(curr.cost) || 0)), 0);
|
| 85 |
+
const totalActivityExpenses = activities.reduce((acc, curr) => acc + (parseFloat(curr.expenses) || 0), 0);
|
| 86 |
+
const netBalance = totalMemberIncome + tripOneIncome + totalTripBalance - totalActivityExpenses;
|
| 87 |
+
|
| 88 |
+
useEffect(() => { lucide.createIcons(); }, [activeTab, isAdmin, showLoginModal]);
|
| 89 |
+
|
| 90 |
+
const SidebarItem = ({ id, iconName, label, color = "blue", isExternal = false, href = "" }) => {
|
| 91 |
+
const baseClass = `w-full flex items-center space-x-reverse space-x-2 px-3 py-2 rounded-lg transition-all text-xs font-semibold`;
|
| 92 |
+
if (isExternal) {
|
| 93 |
+
return (
|
| 94 |
+
<a href={href} target="_blank" rel="noopener noreferrer" className={`${baseClass} text-emerald-600 hover:bg-emerald-50`}>
|
| 95 |
+
<i data-lucide={iconName} className="w-4 h-4"></i>
|
| 96 |
+
<span>{label}</span>
|
| 97 |
+
</a>
|
| 98 |
+
);
|
| 99 |
+
}
|
| 100 |
+
return (
|
| 101 |
+
<button onClick={() => setActiveTab(id)} className={`${baseClass} ${activeTab === id ? `bg-${color}-600 text-white shadow-md` : `text-slate-500 hover:bg-slate-100`}`}>
|
| 102 |
+
<i data-lucide={iconName} className="w-4 h-4"></i>
|
| 103 |
+
<span>{label}</span>
|
| 104 |
+
</button>
|
| 105 |
+
);
|
| 106 |
+
};
|
| 107 |
|
| 108 |
return (
|
| 109 |
+
<div className="min-h-screen bg-slate-50 flex flex-col md:flex-row-reverse" dir="rtl">
|
| 110 |
+
{/* Login Modal */}
|
| 111 |
{showLoginModal && (
|
| 112 |
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
|
| 113 |
+
<div className="bg-white rounded-3xl p-8 shadow-2xl animate-fade-in text-center max-w-xs w-full relative">
|
| 114 |
+
<button onClick={() => {setShowLoginModal(false); setLoginError(false);}} className="absolute top-4 left-4 text-slate-400 hover:text-slate-600">
|
| 115 |
+
<i data-lucide="x" className="w-5 h-5"></i>
|
| 116 |
+
</button>
|
| 117 |
+
<div className="bg-blue-600 w-12 h-12 rounded-xl flex items-center justify-center text-white mx-auto mb-4">
|
| 118 |
+
<i data-lucide="shield-check" className="w-6 h-6"></i>
|
| 119 |
+
</div>
|
| 120 |
+
<h2 className="text-lg font-extrabold text-slate-800 mb-1">دخول المكلف</h2>
|
| 121 |
+
<p className="text-[10px] text-slate-400 mb-4 font-bold">يرجى إدخال رمز الوصول للإدارة</p>
|
| 122 |
+
<form onSubmit={handleLogin} className="space-y-3">
|
| 123 |
+
<input type="password" placeholder="كلمة السر" value={passwordInput} onChange={(e) => setPasswordInput(e.target.value)}
|
| 124 |
+
className="w-full p-3 bg-slate-50 rounded-xl border border-slate-100 outline-none text-center font-bold text-sm" autoFocus />
|
| 125 |
+
<button type="submit" className="w-full bg-blue-600 text-white py-3 rounded-xl font-bold text-sm shadow-lg hover:bg-blue-700">تأكيد الدخول</button>
|
| 126 |
</form>
|
| 127 |
+
{loginError && <p className="text-rose-500 text-[10px] mt-2 font-bold">الرمز غير صحيح!</p>}
|
|
|
|
| 128 |
</div>
|
| 129 |
</div>
|
| 130 |
)}
|
| 131 |
|
| 132 |
+
{/* Sidebar */}
|
| 133 |
+
<aside className="w-full md:w-56 bg-white border-l border-slate-200 p-4 flex flex-col gap-1 shrink-0 z-10">
|
| 134 |
+
<div className="flex items-center gap-2 mb-6 px-2">
|
| 135 |
+
<div className="bg-blue-600 p-1.5 rounded-lg text-white shadow-sm">
|
| 136 |
+
<i data-lucide="wallet" className="w-4 h-4"></i>
|
| 137 |
+
</div>
|
| 138 |
+
<h1 className="text-base font-extrabold text-slate-800">المالية الذكية</h1>
|
| 139 |
</div>
|
| 140 |
+
<nav className="space-y-1">
|
| 141 |
+
<SidebarItem id="dashboard" iconName="layout-dashboard" label="لوحة التحكم" />
|
| 142 |
+
<SidebarItem id="members" iconName="users" label="المنخرطون" />
|
| 143 |
+
<SidebarItem id="trip_one" iconName="map" label="الرحلة 1" />
|
| 144 |
+
<SidebarItem id="trips" iconName="navigation" label="رحلات أخرى" color="purple" />
|
| 145 |
+
<SidebarItem id="activities" iconName="calendar" label="الأنشطة" color="rose" />
|
| 146 |
+
<SidebarItem isExternal={true} href={WHATSAPP_LINK} iconName="message-circle" label="تواصل معنا" />
|
| 147 |
</nav>
|
| 148 |
+
<div className="mt-auto pt-4 border-t border-slate-100 space-y-2">
|
| 149 |
{isAdmin ? (
|
| 150 |
+
<button onClick={() => setIsAdmin(false)} className="w-full text-rose-500 text-[11px] font-bold flex items-center gap-2 hover:bg-rose-50 p-2 rounded-lg">
|
| 151 |
+
<i data-lucide="log-out" className="w-3.5 h-3.5"></i> خروج من الإدارة
|
| 152 |
+
</button>
|
| 153 |
) : (
|
| 154 |
+
<button onClick={() => setShowLoginModal(true)} className="w-full text-blue-600 text-[11px] font-bold flex items-center gap-2 hover:bg-blue-50 p-2 rounded-lg">
|
| 155 |
+
<i data-lucide="user-check" className="w-3.5 h-3.5"></i> دخول المكلف
|
| 156 |
+
</button>
|
| 157 |
)}
|
| 158 |
+
<div className={`text-[9px] text-center font-bold px-2 py-1 rounded ${isAdmin ? 'bg-emerald-50 text-emerald-600' : 'bg-slate-100 text-slate-500'}`}>
|
| 159 |
+
{isAdmin ? 'وضع التعديل نشط' : 'وضع العرض فقط'}
|
| 160 |
+
</div>
|
| 161 |
</div>
|
| 162 |
</aside>
|
| 163 |
|
| 164 |
+
{/* Main Content */}
|
| 165 |
+
<main className="flex-1 p-4 md:p-6 overflow-y-auto animate-fade-in">
|
| 166 |
{activeTab === 'dashboard' && (
|
| 167 |
+
<div className="space-y-6">
|
| 168 |
+
<header className="flex justify-between items-center">
|
| 169 |
+
<div>
|
| 170 |
+
<h2 className="text-xl font-extrabold text-slate-800 leading-tight">ملخص النادي</h2>
|
| 171 |
+
<p className="text-[11px] text-slate-400 font-bold uppercase tracking-wider">البيانات المالية المحدثة</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
</div>
|
| 173 |
+
</header>
|
| 174 |
+
|
| 175 |
+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
| 176 |
+
{[
|
| 177 |
+
{ label: "انخراطات", val: totalMemberIncome, bg: "bg-emerald-50", text: "text-emerald-700" },
|
| 178 |
+
{ label: "الرحلة 1", val: tripOneIncome, bg: "bg-blue-50", text: "text-blue-700" },
|
| 179 |
+
{ label: "أرباح الرحلات", val: totalTripBalance, bg: "bg-purple-50", text: "text-purple-700" },
|
| 180 |
+
{ label: "المصاريف", val: totalActivityExpenses, bg: "bg-rose-50", text: "text-rose-700" }
|
| 181 |
+
].map((card, i) => (
|
| 182 |
+
<div key={i} className={`p-3 rounded-2xl ${card.bg} border border-white shadow-sm`}>
|
| 183 |
+
<p className={`text-[10px] font-bold mb-0.5 ${card.text}`}>{card.label}</p>
|
| 184 |
+
<h3 className={`text-lg font-extrabold ${card.text}`}>{card.val} <span className="text-[10px]">MAD</span></h3>
|
| 185 |
+
</div>
|
| 186 |
+
))}
|
| 187 |
</div>
|
| 188 |
+
|
| 189 |
+
<div className="bg-slate-900 text-white p-6 rounded-3xl shadow-xl relative overflow-hidden">
|
| 190 |
+
<div className="absolute top-0 right-0 w-32 h-32 bg-blue-500/10 rounded-full blur-2xl"></div>
|
| 191 |
+
<p className="text-slate-400 text-xs font-bold mb-1">صافي الرصيد المتوفر</p>
|
| 192 |
+
<h2 className="text-4xl font-extrabold tracking-tight">{netBalance} <span className="text-sm text-slate-500 font-bold">درهم مغربي</span></h2>
|
| 193 |
</div>
|
| 194 |
</div>
|
| 195 |
)}
|
| 196 |
|
| 197 |
{activeTab === 'members' && (
|
| 198 |
+
<div className="space-y-4">
|
| 199 |
+
<h2 className="text-lg font-extrabold text-slate-800">سجل المنخرطين</h2>
|
| 200 |
{isAdmin && (
|
| 201 |
+
<div className="bg-white p-4 rounded-2xl shadow-sm grid grid-cols-1 md:grid-cols-4 gap-2 border">
|
| 202 |
+
<input type="text" placeholder="الاسم" value={memberForm.name} onChange={(e)=>setMemberForm({...memberForm, name: e.target.value})} className="p-2 bg-slate-50 rounded-lg outline-none border border-slate-100 text-xs font-semibold" />
|
| 203 |
+
<input type="number" placeholder="المبلغ" value={memberForm.amount} onChange={(e)=>setMemberForm({...memberForm, amount: e.target.value})} className="p-2 bg-slate-50 rounded-lg outline-none border border-slate-100 text-xs font-semibold" />
|
| 204 |
+
<input type="date" value={memberForm.date} onChange={(e)=>setMemberForm({...memberForm, date: e.target.value})} className="p-2 bg-slate-50 rounded-lg outline-none border border-slate-100 text-xs font-semibold" />
|
| 205 |
+
<button onClick={()=>{if(!memberForm.name) return; setMembers([...members, {...memberForm, id: Date.now()}]); setMemberForm({name:'',amount:'',date:''})}} className="bg-blue-600 text-white p-2 rounded-lg font-bold text-xs">إضافة</button>
|
| 206 |
</div>
|
| 207 |
)}
|
| 208 |
+
<div className="bg-white rounded-2xl shadow-sm border overflow-hidden">
|
| 209 |
+
<table className="w-full text-right">
|
| 210 |
+
<thead className="bg-slate-50 border-b text-[11px] text-slate-400 uppercase font-extrabold">
|
| 211 |
+
<tr><th className="px-4 py-3">الاسم</th><th className="px-4 py-3 text-center">المبلغ</th>{isAdmin && <th className="px-4 py-3 text-center w-16">إجراء</th>}</tr>
|
| 212 |
+
</thead>
|
| 213 |
+
<tbody className="divide-y divide-slate-50 text-xs font-semibold">
|
| 214 |
+
{members.map(m => (
|
| 215 |
+
<tr key={m.id} className="hover:bg-slate-50">
|
| 216 |
+
<td className="px-4 py-2 text-slate-700">{m.name}</td>
|
| 217 |
+
<td className="px-4 py-2 text-emerald-600 text-center font-bold">{m.amount} MAD</td>
|
| 218 |
+
{isAdmin && <td className="px-4 py-2 text-center"><button onClick={()=>setMembers(members.filter(x=>x.id!==m.id))} className="text-rose-300 hover:text-rose-500 p-1"><i data-lucide="trash-2" className="w-3.5 h-3.5"></i></button></td>}
|
| 219 |
+
</tr>
|
| 220 |
+
))}
|
| 221 |
+
</tbody>
|
| 222 |
+
</table>
|
| 223 |
</div>
|
| 224 |
</div>
|
| 225 |
)}
|
| 226 |
|
| 227 |
{activeTab === 'trip_one' && (
|
| 228 |
+
<div className="space-y-4">
|
| 229 |
+
<h2 className="text-lg font-extrabold text-blue-800 text-center md:text-right">لائحة المشاركين في الرحلة 1</h2>
|
| 230 |
{isAdmin && (
|
| 231 |
+
<div className="bg-white p-4 rounded-2xl shadow-sm grid grid-cols-1 md:grid-cols-4 gap-2 border border-blue-50">
|
| 232 |
+
<input type="text" placeholder="الاسم الكامل" value={tripOneForm.name} onChange={(e)=>setTripOneForm({...tripOneForm, name: e.target.value})} className="p-2 bg-blue-50/50 rounded-lg outline-none text-xs font-semibold" />
|
| 233 |
+
<input type="number" placeholder="المبلغ" value={tripOneForm.fee} onChange={(e)=>setTripOneForm({...tripOneForm, fee: e.target.value})} className="p-2 bg-blue-50/50 rounded-lg outline-none text-xs font-semibold" />
|
| 234 |
+
<div className="flex items-center gap-2 px-1">
|
| 235 |
+
<input type="checkbox" checked={tripOneForm.paid} onChange={(e)=>setTripOneForm({...tripOneForm, paid: e.target.checked})} className="w-4 h-4 accent-blue-600" />
|
| 236 |
+
<span className="text-[11px] font-bold">مدفوع</span>
|
| 237 |
+
</div>
|
| 238 |
+
<button onClick={()=>{if(!tripOneForm.name) return; setTripOneRegistrations([...tripOneRegistrations, {...tripOneForm, id: Date.now()}]); setTripOneForm({name:'',fee:'',paid:false})}} className="bg-blue-700 text-white p-2 rounded-lg font-bold text-xs shadow-sm">تسجيل مشارك</button>
|
| 239 |
</div>
|
| 240 |
)}
|
| 241 |
+
<div className="bg-white rounded-2xl shadow-sm border overflow-hidden">
|
| 242 |
+
<table className="w-full text-right">
|
| 243 |
+
<thead className="bg-blue-700 text-white text-[11px] font-bold">
|
| 244 |
+
<tr><th className="px-4 py-2.5">المشارك</th><th className="px-4 py-2.5 text-center">الواجب</th><th className="px-4 py-2.5 text-center">الحالة</th>{isAdmin && <th className="px-4 py-2.5 text-center w-16">إجراء</th>}</tr>
|
| 245 |
+
</thead>
|
| 246 |
+
<tbody className="text-xs font-semibold divide-y divide-blue-50">
|
| 247 |
+
{tripOneRegistrations.map(reg => (
|
| 248 |
+
<tr key={reg.id} className="hover:bg-blue-50/30">
|
| 249 |
+
<td className="px-4 py-2">{reg.name}</td>
|
| 250 |
+
<td className="px-4 py-2 text-center font-bold">{reg.fee} MAD</td>
|
| 251 |
+
<td className="px-4 py-2 text-center">
|
| 252 |
+
<span onClick={() => isAdmin && setTripOneRegistrations(tripOneRegistrations.map(r => r.id === reg.id ? {...r, paid: !r.paid} : r))}
|
| 253 |
+
className={`px-2 py-0.5 rounded-full text-[9px] font-extrabold border ${isAdmin ? 'cursor-pointer' : ''} ${reg.paid ? 'bg-emerald-50 text-emerald-600 border-emerald-100' : 'bg-amber-50 text-amber-600 border-amber-100'}`}>
|
| 254 |
+
{reg.paid ? 'تم الدفع' : 'في الانتظار'}
|
| 255 |
+
</span>
|
| 256 |
+
</td>
|
| 257 |
+
{isAdmin && <td className="px-4 py-2 text-center"><button onClick={()=>setTripOneRegistrations(tripOneRegistrations.filter(x=>x.id!==reg.id))} className="text-blue-200 hover:text-rose-500"><i data-lucide="trash-2" className="w-3.5 h-3.5"></i></button></td>}
|
| 258 |
+
</tr>
|
| 259 |
+
))}
|
| 260 |
+
</tbody>
|
| 261 |
+
</table>
|
| 262 |
</div>
|
| 263 |
</div>
|
| 264 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
|
| 266 |
+
{activeTab === 'trips' && (
|
| 267 |
+
<div className="space-y-4">
|
| 268 |
+
<h2 className="text-lg font-extrabold text-purple-800">بيانات الرحلات</h2>
|
| 269 |
+
{isAdmin && (
|
| 270 |
+
<div className="bg-white p-4 rounded-2xl shadow-sm grid grid-cols-1 md:grid-cols-4 gap-2 border border-purple-50">
|
| 271 |
+
<input type="text" placeholder="الوجهة" value={tripForm.destination} onChange={(e)=>setTripForm({...tripForm, destination: e.target.value})} className="p-2 bg-purple-50/50 rounded-lg outline-none text-xs font-semibold" />
|
| 272 |
+
<input type="number" placeholder="المصاريف" value={tripForm.cost} onChange={(e)=>setTripForm({...tripForm, cost: e.target.value})} className="p-2 bg-purple-50/50 rounded-lg outline-none text-xs font-semibold" />
|
| 273 |
+
<input type="number" placeholder="المداخيل" value={tripForm.income} onChange={(e)=>setTripForm({...tripForm, income: e.target.value})} className="p-2 bg-purple-50/50 rounded-lg outline-none text-xs font-semibold" />
|
| 274 |
+
<button onClick={()=>{if(!tripForm.destination) return; setTrips([...trips, {...tripForm, id: Date.now()}]); setTripForm({destination:'',cost:'',income:'',date:''})}} className="bg-purple-600 text-white p-2 rounded-lg font-bold text-xs shadow-sm">حفظ الرحلة</button>
|
| 275 |
+
</div>
|
| 276 |
+
)}
|
| 277 |
+
<div className="bg-white rounded-2xl shadow-sm border overflow-hidden">
|
| 278 |
+
<table className="w-full text-right text-xs">
|
| 279 |
+
<thead className="bg-purple-50 text-purple-900 border-b text-[11px] font-bold">
|
| 280 |
+
<tr><th className="px-4 py-3">الوجهة</th><th className="px-4 py-3 text-center">مصاريف</th><th className="px-4 py-3 text-center">مداخيل</th><th className="px-4 py-3 text-center">الصافي</th>{isAdmin && <th className="px-4 py-3 text-center">حذف</th>}</tr>
|
| 281 |
+
</thead>
|
| 282 |
+
<tbody className="divide-y divide-purple-50 font-semibold">
|
| 283 |
+
{trips.map(t => (
|
| 284 |
+
<tr key={t.id} className="hover:bg-purple-50/30">
|
| 285 |
+
<td className="px-4 py-2">{t.destination}</td>
|
| 286 |
+
<td className="px-4 py-2 text-rose-500 text-center">{t.cost}</td>
|
| 287 |
+
<td className="px-4 py-2 text-emerald-600 text-center">{t.income}</td>
|
| 288 |
+
<td className="px-4 py-2 text-center font-extrabold text-slate-800">{(t.income - t.cost)} MAD</td>
|
| 289 |
+
{isAdmin && <td className="px-4 py-2 text-center"><button onClick={()=>setTrips(trips.filter(x=>x.id!==t.id))} className="text-purple-200 hover:text-rose-500"><i data-lucide="trash-2" className="w-3.5 h-3.5"></i></button></td>}
|
| 290 |
+
</tr>
|
| 291 |
+
))}
|
| 292 |
+
</tbody>
|
| 293 |
+
</table>
|
| 294 |
+
</div>
|
| 295 |
+
</div>
|
| 296 |
+
)}
|
| 297 |
+
|
| 298 |
+
{activeTab === 'activities' && (
|
| 299 |
+
<div className="space-y-4">
|
| 300 |
+
<h2 className="text-lg font-extrabold text-rose-800">سجل الأنشطة</h2>
|
| 301 |
+
{isAdmin && (
|
| 302 |
+
<div className="bg-white p-4 rounded-2xl shadow-sm grid grid-cols-1 md:grid-cols-3 gap-2 border border-rose-50">
|
| 303 |
+
<input type="text" placeholder="اسم النشاط" value={activityForm.title} onChange={(e)=>setActivityForm({...activityForm, title: e.target.value})} className="p-2 bg-rose-50/50 rounded-lg outline-none text-xs font-semibold" />
|
| 304 |
+
<input type="number" placeholder="الميزانية المستهلكة" value={activityForm.expenses} onChange={(e)=>setActivityForm({...activityForm, expenses: e.target.value})} className="p-2 bg-rose-50/50 rounded-lg outline-none text-xs font-semibold" />
|
| 305 |
+
<button onClick={()=>{if(!activityForm.title) return; setActivities([...activities, {...activityForm, id: Date.now()}]); setActivityForm({title:'',expenses:'',date:''})}} className="bg-rose-600 text-white p-2 rounded-lg font-bold text-xs shadow-sm">إضافة نشاط</button>
|
| 306 |
+
</div>
|
| 307 |
+
)}
|
| 308 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
|
| 309 |
+
{activities.map(a => (
|
| 310 |
+
<div key={a.id} className="p-3 bg-white border border-rose-50 rounded-xl flex justify-between items-center shadow-sm">
|
| 311 |
+
<div>
|
| 312 |
+
<h4 className="font-extrabold text-xs text-slate-800 leading-tight">{a.title}</h4>
|
| 313 |
+
<p className="text-rose-600 font-bold text-[11px]">{a.expenses} MAD</p>
|
| 314 |
+
</div>
|
| 315 |
+
{isAdmin && <button onClick={()=>setActivities(activities.filter(x=>x.id!==a.id))} className="text-slate-200 hover:text-rose-500"><i data-lucide="trash-2" className="w-3.5 h-3.5"></i></button>}
|
| 316 |
+
</div>
|
| 317 |
+
))}
|
| 318 |
+
{activities.length === 0 && <p className="text-slate-400 text-center col-span-full py-10 text-[11px] font-bold italic">لا توجد أنشطة مسجلة بعد</p>}
|
| 319 |
+
</div>
|
| 320 |
+
</div>
|
| 321 |
+
)}
|
| 322 |
</main>
|
| 323 |
</div>
|
| 324 |
);
|
|
|
|
| 330 |
|
| 331 |
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
| 332 |
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
| 333 |
+
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
| 334 |
</body>
|
| 335 |
</html>
|