import React, { useState, useEffect } from 'react';
import { initializeApp } from 'firebase/app';
import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from 'firebase/auth';
import { getFirestore, doc, getDoc, addDoc, setDoc, updateDoc, deleteDoc, onSnapshot, collection, query, where, getDocs } from 'firebase/firestore';
// Main App component
const App = () => {
const [employees, setEmployees] = useState([]);
const [newEmployeeName, setNewEmployeeName] = useState('');
const [currentMonth, setCurrentMonth] = useState(new Date().getMonth());
const [currentYear, setCurrentYear] = useState(new Date().getFullYear());
const [showAttendanceModal, setShowAttendanceModal] = useState(false);
const [selectedEmployeeForAttendance, setSelectedEmployeeForAttendance] = useState(null);
const [showDetailsModal, setShowDetailsModal] = useState(false);
const [selectedEmployeeForDetails, setSelectedEmployeeForDetails] = useState(null);
// Firestore states
const [db, setDb] = useState(null);
const [auth, setAuth] = useState(null);
const [userId, setUserId] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Initialize Firebase and set up authentication listener
useEffect(() => {
try {
const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';
const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {};
const app = initializeApp(firebaseConfig);
const firestore = getFirestore(app);
const firebaseAuth = getAuth(app);
setDb(firestore);
setAuth(firebaseAuth);
const unsubscribe = onAuthStateChanged(firebaseAuth, async (user) => {
if (user) {
setUserId(user.uid);
} else {
// Sign in anonymously if no token or user
try {
if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) {
await signInWithCustomToken(firebaseAuth, __initial_auth_token);
} else {
await signInAnonymously(firebaseAuth);
}
} catch (authError) {
console.error("Firebase Auth Error:", authError);
setError("فشل المصادقة مع Firebase: " + authError.message);
setLoading(false);
}
}
});
return () => unsubscribe(); // Cleanup auth listener
} catch (e) {
console.error("Firebase Initialization Error:", e);
setError("خطأ في تهيئة Firebase: " + e.message);
setLoading(false);
}
}, []);
// Fetch employees data from Firestore when db or userId changes
useEffect(() => {
if (!db || !userId) {
if (!db && !auth && !loading) { // Only set loading to true if not already initializing
setLoading(true);
}
return;
}
const employeesCollectionRef = collection(db, `artifacts/${__app_id}/users/${userId}/employees`);
const unsubscribe = onSnapshot(employeesCollectionRef, (snapshot) => {
const employeesData = snapshot.docs.map(doc => ({
id: doc.id, // Firestore document ID is used as employee ID
...doc.data(),
}));
setEmployees(employeesData);
setLoading(false);
setError(null); // Clear any previous errors
}, (err) => {
console.error("Failed to fetch employees:", err);
setError("فشل في تحميل بيانات الموظفين: " + err.message);
setLoading(false);
});
return () => unsubscribe(); // Cleanup Firestore listener
}, [db, userId, __app_id]); // Depend on db, userId, and __app_id
// Function to handle changes in input fields in the main table (monthlySalary, bonus, deduction)
const handleTableInputChange = async (id, field, value) => {
if (!db || !userId) {
console.error("Firestore not initialized or user not logged in.");
setError("قاعدة البيانات غير جاهزة أو المستخدم غير مسجل الدخول.");
return;
}
const employeeDocRef = doc(db, `artifacts/${__app_id}/users/${userId}/employees`, id);
try {
await updateDoc(employeeDocRef, {
[field]: parseFloat(value) || 0 // Parse numeric values
});
} catch (e) {
console.error(`Error updating employee ${field}:`, e);
setError(`فشل تحديث حقل ${field} للموظف: ` + e.message);
}
};
// Function to handle changes in employee details modal inputs
const handleDetailsModalInputChange = async (field, value) => {
if (!db || !userId || !selectedEmployeeForDetails) {
console.error("Firestore not initialized, user not logged in, or no employee selected.");
setError("قاعدة البيانات غير جاهزة أو لا يوجد موظف محدد.");
return;
}
let processedValue = value;
if (['monthlySalary', 'allowedLeaveDays'].includes(field)) {
processedValue = parseFloat(value) || 0;
} else if (field === 'peopleOfDetermination') {
processedValue = value; // Checkbox value is boolean
}
const employeeDocRef = doc(db, `artifacts/${__app_id}/users/${userId}/employees`, selectedEmployeeForDetails.id);
try {
await updateDoc(employeeDocRef, {
[field]: processedValue
});
// Update the local state of the modal's selected employee for immediate UI feedback
setSelectedEmployeeForDetails(prevSelected => ({
...prevSelected,
[field]: processedValue,
}));
} catch (e) {
console.error(`Error updating employee details ${field}:`, e);
setError(`فشل تحديث تفاصيل الموظف ${field}: ` + e.message);
}
};
// Function to add a new employee to the list
const addEmployee = async () => {
if (!db || !userId) {
console.error("Firestore not initialized or user not logged in.");
setError("قاعدة البيانات غير جاهزة أو المستخدم غير مسجل الدخول.");
return;
}
if (newEmployeeName.trim() === '') {
console.error('الرجاء إدخال اسم الموظف الجديد.');
return;
}
try {
const docRef = await addDoc(collection(db, `artifacts/${__app_id}/users/${userId}/employees`), {
name: newEmployeeName.trim(),
monthlySalary: 5000,
allowedLeaveDays: 4,
absentDates: [],
bonus: 0,
deduction: 0,
nationalId: '',
nationality: '',
maritalStatus: 'أعزب',
peopleOfDetermination: false,
jobTitle: '',
});
setNewEmployeeName('');
} catch (e) {
console.error("Error adding document: ", e);
setError("فشل إضافة موظف جديد: " + e.message);
}
};
// Function to delete an employee from the list
const deleteEmployee = async (idToDelete) => {
if (!db || !userId) {
console.error("Firestore not initialized or user not logged in.");
setError("قاعدة البيانات غير جاهزة أو المستخدم غير مسجل الدخول.");
return;
}
try {
await deleteDoc(doc(db, `artifacts/${__app_id}/users/${userId}/employees`, idToDelete));
} catch (e) {
console.error("Error deleting document: ", e);
setError("فشل حذف الموظف: " + e.message);
}
};
// Function to open the attendance calendar modal for a specific employee
const openAttendanceCalendarModal = (employee) => {
setSelectedEmployeeForAttendance(employee);
setShowAttendanceModal(true);
};
// Function to close the attendance calendar modal
const closeAttendanceCalendarModal = () => {
setShowAttendanceModal(false);
setSelectedEmployeeForAttendance(null);
};
// Function to open the employee details modal
const openEmployeeDetailsModal = (employee) => {
setSelectedEmployeeForDetails(employee);
setShowDetailsModal(true);
};
// Function to close the employee details modal
const closeEmployeeDetailsModal = () => {
setShowDetailsModal(false);
setSelectedEmployeeForDetails(null);
};
// Function to toggle an absent date for the selected employee in calendar modal
const toggleAbsentDate = async (date) => {
if (!db || !userId || !selectedEmployeeForAttendance) {
console.error("Firestore not initialized, user not logged in, or no employee selected.");
setError("قاعدة البيانات غير جاهزة أو لا يوجد موظف محدد.");
return;
}
const dateString = `${currentYear}-${(currentMonth + 1).toString().padStart(2, '0')}-${date.toString().padStart(2, '0')}`;
const isCurrentlyAbsent = selectedEmployeeForAttendance.absentDates.includes(dateString);
const newAbsentDates = isCurrentlyAbsent
? selectedEmployeeForAttendance.absentDates.filter(d => d !== dateString)
: [...selectedEmployeeForAttendance.absentDates, dateString];
const employeeDocRef = doc(db, `artifacts/${__app_id}/users/${userId}/employees`, selectedEmployeeForAttendance.id);
try {
await updateDoc(employeeDocRef, {
absentDates: newAbsentDates
});
// Update the local state of the modal's selected employee for immediate UI feedback
setSelectedEmployeeForAttendance(prevSelected => ({
...prevSelected,
absentDates: newAbsentDates,
}));
} catch (e) {
console.error("Error toggling absent date: ", e);
setError("فشل تحديث أيام الغياب: " + e.message);
}
};
// Function to navigate calendar month
const changeMonth = (delta) => {
let newMonth = currentMonth + delta;
let newYear = currentYear;
if (newMonth > 11) {
newMonth = 0;
newYear++;
} else if (newMonth < 0) {
newMonth = 11;
newYear--;
}
setCurrentMonth(newMonth);
setCurrentYear(newYear);
};
// Utility to get the number of days in a given month and year
const getDaysInMonth = (year, month) => {
return new Date(year, month + 1, 0).getDate();
};
// Utility to get the first day of the week for a given month and year (0=Sunday, 1=Monday...)
const getFirstDayOfMonth = (year, month) => {
return new Date(year, month, 1).getDay();
};
// Function to calculate salary details for a single employee
const calculateEmployeeSalary = (employee) => {
const { monthlySalary, allowedLeaveDays, bonus, deduction } = employee;
// Get total days in the currently selected month
const totalDaysInCurrentMonth = getDaysInMonth(currentYear, currentMonth);
// Filter absent dates relevant to the current month and year
const absentDaysInCurrentMonth = (employee.absentDates || []).filter(dateString => {
const [year, month, day] = dateString.split('-').map(Number);
return year === currentYear && (month - 1) === currentMonth; // month is 1-indexed in string
}).length;
// Calculate absent days
const absentDays = absentDaysInCurrentMonth;
// Calculate present days (total days in month - absent days)
const daysPresent = totalDaysInCurrentMonth - absentDays;
// Define the deduction percentage per excess absent day
const deductionPercentagePerExcessDay = 0.07; // 7% from total monthly salary
// Calculate excess absent days (beyond allowed leave)
const excessAbsentDays = Math.max(0, absentDays - allowedLeaveDays);
// Calculate deduction for excess absent days based on a percentage of monthly salary
const absenceDeduction = excessAbsentDays * (monthlySalary * deductionPercentagePerExcessDay);
// Calculate net salary
const netSalary = monthlySalary - absenceDeduction + bonus - deduction;
return {
absentDays: absentDays,
excessAbsentDays: excessAbsentDays,
absenceDeduction: absenceDeduction,
netSalary: netSalary,
daysPresent: daysPresent, // Also return daysPresent for display
};
};
// Month names for display
const monthNames = [
'يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو',
'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'
];
// Days of the week for calendar header
const dayNames = ['أحد', 'اثنين', 'ثلاثاء', 'أربعاء', 'خميس', 'جمعة', 'سبت'];
// Render the main application UI
return (
{loading ? (
) : (
)}
{/* Attendance Calendar Modal */}
{showAttendanceModal && selectedEmployeeForAttendance && (
إغلاق
)}
{/* Employee Details Modal */}
{showDetailsModal && selectedEmployeeForDetails && (
{/* People of Determination */}
إغلاق
)}
);
};
export default App;
جدول الحضور والرواتب
{error && (
خطأ!
{error}
)}
{userId && (
معرف المستخدم (للتخزين): {userId}
)} {/* Section for adding a new employee */}
setNewEmployeeName(e.target.value)}
className="flex-grow p-3 border border-blue-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 text-right"
dir="rtl"
disabled={loading}
/>
{/* Calendar Navigation */}
جاري تحميل البيانات...
| اسم الموظف | الراتب الشهري (ر.ق) | أيام الحضور (في {monthNames[currentMonth]}) | أيام الغياب (في {monthNames[currentMonth]}) | أيام الغياب الزائدة | خصم الغياب (ر.ق) | مكافأة (ر.ق) | خصم إضافي (ر.ق) | الراتب الصافي (ر.ق) | إجراءات |
|---|---|---|---|---|---|---|---|---|---|
| لا يوجد موظفون مضافون حتى الآن. ابدأ بإضافة موظف جديد! | |||||||||
| openEmployeeDetailsModal(employee)}
title="اضغط لعرض/تعديل تفاصيل الموظف"
>
{employee.name}
{employee.jobTitle && (
{employee.jobTitle}
)}
| handleTableInputChange(employee.id, 'monthlySalary', e.target.value)} className="w-24 p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 transition duration-150 ease-in-out text-center" disabled={loading} /> | openAttendanceCalendarModal(employee)} title="اضغط لتحديد أيام الغياب" > {daysPresent} | {absentDays} | {excessAbsentDays} | {absenceDeduction.toFixed(2)} | handleTableInputChange(employee.id, 'bonus', e.target.value)} className="w-24 p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 transition duration-150 ease-in-out text-center" disabled={loading} /> | handleTableInputChange(employee.id, 'deduction', e.target.value)} className="w-24 p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 transition duration-150 ease-in-out text-center" disabled={loading} /> | {netSalary.toFixed(2)} |
|
تحديد أيام غياب {selectedEmployeeForAttendance.name}
للشهر: {monthNames[currentMonth]} {currentYear}
{/* Calendar grid */}
{dayNames.map(day => (
);
})}
{day}
))}
{/* Render empty cells for days before the 1st of the month */}
{[...Array(getFirstDayOfMonth(currentYear, currentMonth))].map((_, i) => (
))}
{/* Render actual days of the month */}
{[...Array(getDaysInMonth(currentYear, currentMonth))].map((_, i) => {
const day = i + 1;
const dateString = `${currentYear}-${(currentMonth + 1).toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
const isAbsent = (selectedEmployeeForAttendance.absentDates || []).includes(dateString);
return (
toggleAbsentDate(day)}
>
{day}
تفاصيل الموظف: {selectedEmployeeForDetails.name}
{/* Employee Name - now editable in modal */}
handleDetailsModalInputChange('name', e.target.value)}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline focus:ring-blue-500 focus:border-blue-500"
dir="rtl"
/>
{/* Allowed Leave Days - Moved to details modal */}
handleDetailsModalInputChange('allowedLeaveDays', e.target.value)}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline focus:ring-blue-500 focus:border-blue-500"
dir="rtl"
/>
{/* Job Title */}
handleDetailsModalInputChange('jobTitle', e.target.value)}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline focus:ring-blue-500 focus:border-blue-500"
dir="rtl"
/>
{/* National ID */}
handleDetailsModalInputChange('nationalId', e.target.value)}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline focus:ring-blue-500 focus:border-blue-500"
dir="rtl"
/>
{/* Nationality */}
handleDetailsModalInputChange('nationality', e.target.value)}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline focus:ring-blue-500 focus:border-blue-500"
dir="rtl"
/>
{/* Marital Status */}
handleDetailsModalInputChange('peopleOfDetermination', e.target.checked)}
className="form-checkbox h-5 w-5 text-blue-600 rounded focus:ring-blue-500 transition duration-150 ease-in-out"
/>