📅
LeaveSync load ho raha hai...
📅
LeaveSync
Team Leave Management System
Default Admin: admin@company.com / admin123
// ── STATE ──
let CU = null; // current user object
let USERS = []; // in-memory users cache
let LEAVES = []; // in-memory leaves cache
let cals = {}; // fullcalendar instances
let rf = 'all'; // request filter
let unsubListeners = []; // firestore realtime unsubscribe fns
const COLS = ['#7c3aed','#2563eb','#059669','#d97706','#dc2626','#0891b2','#9333ea','#c2410c','#0284c7','#65a30d'];
const ADMIN_DEFAULT = { id:'admin_001', name:'Admin', email:'admin@company.com', password:'admin123', role:'admin', color:'#7c3aed' };
// ── HELPERS ──
const uid = () => '_' + Math.random().toString(36).substr(2, 9) + Date.now();
const ini = n => (n||'?').split(' ').map(c=>c[0]).join('').toUpperCase().slice(0, 2);
const tod = () => new Date().toISOString().split('T')[0];
const fmD = d => d ? new Date(d+'T00:00:00').toLocaleDateString('en-IN', {weekday:'short',day:'numeric',month:'long',year:'numeric'}) : '---';
const fmS = d => d ? new Date(d).toLocaleDateString('en-IN', {day:'numeric',month:'short',year:'numeric'}) : '---';
const gCol = id => { const u=USERS.find(x=>x.id===id); return u?.color||'#7c3aed'; };
const sbdg = s => { const m={pending:'⏳ Pending',approved:'✅ Approved',rejected:'❌ Rejected'}; return `${m[s]||s}`; };
const raf = fn => requestAnimationFrame(() => setTimeout(fn, 30));
const hide = id => { const el=document.getElementById(id); if(el) el.style.display='none'; };
const show = (id,d='flex') => { const el=document.getElementById(id); if(el) el.style.display=d; };
// ── TOAST ──
function toast(msg, type='info') {
const w = document.getElementById('twrap');
const el = document.createElement('div');
el.className = 'toast ' + type;
el.innerHTML = msg;
w.appendChild(el);
setTimeout(() => {
el.style.transition = '.3s';
el.style.opacity = '0';
el.style.transform = 'translateX(110%)';
setTimeout(() => el.remove(), 300);
}, 3500);
}
// ── MODAL ──
function openM(title, html) {
document.getElementById('mti').textContent = title;
document.getElementById('mb').innerHTML = html;
document.getElementById('mow').classList.add('show');
}
function closeM() { document.getElementById('mow').classList.remove('show'); }
function moClick(e) { if (e.target === document.getElementById('mow')) closeM(); }
// ── LOADING ──
function showLoading() { show('loading-screen','flex'); }
function hideLoading() { hide('loading-screen'); }
// ════════════════════════════════════
// FIRESTORE OPERATIONS
// ════════════════════════════════════
const FS = {
// Get all documents from a collection
getAll: async (col) => {
const snap = await db.collection(col).get();
return snap.docs.map(d => ({ id: d.id, ...d.data() }));
},
// Get document by id
getById: async (col, id) => {
const doc = await db.collection(col).doc(id).get();
return doc.exists ? { id: doc.id, ...doc.data() } : null;
},
// Query with where clause
where: async (col, field, op, val) => {
const snap = await db.collection(col).where(field, op, val).get();
return snap.docs.map(d => ({ id: d.id, ...d.data() }));
},
// Set document with specific id
set: async (col, id, data) => {
return db.collection(col).doc(id).set(data);
},
// Add document (auto id)
add: async (col, data) => {
const ref = await db.collection(col).add(data);
return { id: ref.id, ...data };
},
// Update specific fields
update: async (col, id, data) => {
return db.collection(col).doc(id).update(data);
},
// Delete document
del: async (col, id) => {
return db.collection(col).doc(id).delete();
},
// Real-time listener
listen: (col, callback, query) => {
let ref = db.collection(col);
if (query) ref = ref.where(query.field, query.op, query.val);
return ref.onSnapshot(snap => {
const docs = snap.docs.map(d => ({ id: d.id, ...d.data() }));
callback(docs);
});
}
};
// ════════════════════════════════════
// BOOT / INIT
// ════════════════════════════════════
async function boot() {
if (!CONFIG_SET) {
hideLoading();
// Show setup warning on login page
document.getElementById('lerr').style.display = 'block';
document.getElementById('lerr').innerHTML = `
⚠️ Firebase config set nahi hai!
index.html mein firebaseConfig apna project config se update karo.
Firebase Console kholo →`;
document.getElementById('login-btn').disabled = true;
show('lp','flex');
return;
}
try {
// Load all data first
await Promise.all([loadUsers(), loadLeaves()]);
// Seed admin if not exists
const adminExists = USERS.find(u => u.role === 'admin');
if (!adminExists) {
await FS.set('users', ADMIN_DEFAULT.id, ADMIN_DEFAULT);
USERS.push(ADMIN_DEFAULT);
}
hideLoading();
// Check for saved session
const saved = localStorage.getItem('lms_session');
if (saved) {
const parsed = JSON.parse(saved);
const fresh = USERS.find(u => u.id === parsed.id);
if (fresh) { CU = fresh; mountApp(); return; }
else { localStorage.removeItem('lms_session'); }
}
show('lp','flex');
} catch(err) {
hideLoading();
show('lp','flex');
document.getElementById('lerr').style.display = 'block';
document.getElementById('lerr').textContent = '⚠ Firebase connect nahi ho pa raha. Config check karo. Error: ' + err.message;
}
}
async function loadUsers() {
USERS = await FS.getAll('users');
}
async function loadLeaves() {
LEAVES = await FS.getAll('leaves');
}
// ── REAL-TIME LISTENERS ──
function startListeners() {
// Stop old listeners
unsubListeners.forEach(fn => fn());
unsubListeners = [];
// Listen to users
const u1 = FS.listen('users', docs => {
USERS = docs;
refreshCurrentPage();
});
// Listen to leaves
const u2 = FS.listen('leaves', docs => {
LEAVES = docs;
refreshCurrentPage();
updatePendingBadge();
});
unsubListeners = [u1, u2];
}
function stopListeners() {
unsubListeners.forEach(fn => fn());
unsubListeners = [];
}
// Refresh whatever page is currently active
function refreshCurrentPage() {
if (!CU) return;
if (CU.role === 'admin') {
const active = document.querySelector('.pg.active');
if (!active) return;
const id = active.id;
if (id === 'pg-ds') rDash();
if (id === 'pg-rq') rReqs(rf);
if (id === 'pg-mb') rMembs();
if (id === 'pg-ac') refreshAdminCal();
updatePendingBadge();
} else {
const active = document.querySelector('.pg.active');
if (!active) return;
const id = active.id;
if (id === 'pg-um') { rUSt(); rULL(); }
if (id === 'pg-ur') { rUPend(); refreshUserReqCal(); }
if (id === 'pg-uc') refreshUserTeamCal();
}
}
// ════════════════════════════════════
// AUTH
// ════════════════════════════════════
async function doLogin() {
const em = document.getElementById('lei').value.trim().toLowerCase();
const pw = document.getElementById('lpi').value;
const btn = document.getElementById('login-btn');
const err = document.getElementById('lerr');
if (!em || !pw) { err.style.display='block'; err.textContent='⚠ Email aur password dono bharo!'; return; }
btn.disabled = true;
btn.textContent = 'Logging in...';
err.style.display = 'none';
try {
// Refresh users from Firestore
await loadUsers();
const user = USERS.find(u => u.email === em && u.password === pw);
if (!user) {
err.style.display = 'block';
err.innerHTML = '❌ Galat email ya password. Dobara try karo.';
document.getElementById('lpi').value = '';
btn.disabled = false;
btn.textContent = 'Login Karo →';
return;
}
CU = user;
localStorage.setItem('lms_session', JSON.stringify({ id: user.id }));
btn.disabled = false;
btn.textContent = 'Login Karo →';
mountApp();
} catch(e) {
err.style.display = 'block';
err.textContent = '⚠ Login error: ' + e.message;
btn.disabled = false;
btn.textContent = 'Login Karo →';
}
}
function doLogout() {
CU = null;
localStorage.removeItem('lms_session');
stopListeners();
Object.values(cals).forEach(c => { try { c.destroy(); } catch {} });
cals = {};
hide('app');
show('lp','flex');
document.getElementById('lei').value = '';
document.getElementById('lpi').value = '';
}
// ════════════════════════════════════
// MOUNT APP
// ════════════════════════════════════
function mountApp() {
hide('lp');
show('app','block');
document.getElementById('sbname').textContent = CU.name;
document.getElementById('sbemail').textContent = CU.email;
const av = document.getElementById('sbav');
av.textContent = ini(CU.name);
av.style.background = CU.color || '#7c3aed';
const pill = document.getElementById('rpill');
pill.innerHTML = CU.role === 'admin' ? '⚡ Admin' : '👤 Member';
pill.className = 'rpill ' + CU.role;
startListeners();
if (CU.role === 'admin') buildAdmin();
else buildUser();
}
// ── STAT CARD ──
function stCard(color, icon, val, lbl) {
return ``;
}
// ════════════════════════════════════
// ADMIN DASHBOARD
// ════════════════════════════════════
function buildAdmin() {
document.getElementById('sbn').innerHTML = `
Menu
📊Dashboard
📋Leave Requests
0
📅Team Calendar
👥Team Members
`;
document.getElementById('mc').innerHTML = `
📊 Admin Dashboard
Real-time sync — sabhi devices par update hoga
📋 Leave Requests
Sabhi requests approve ya reject karo
| Member | Chutti Date | Reason | Request Date | Status | Action |
📅 Team Calendar
Kaun kab chutti par hai — ek nazar mein
👥 Team Members
Members add aur manage karo
`;
rDash();
}
function updatePendingBadge() {
const pd = LEAVES.filter(l => l.status === 'pending');
const b = document.getElementById('pbd');
if (b) { b.textContent = pd.length; b.style.display = pd.length ? 'inline' : 'none'; }
}
function aP(p) {
document.querySelectorAll('.pg').forEach(x => x.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(x => x.classList.remove('active'));
document.getElementById('pg-'+p).classList.add('active');
document.getElementById('n-'+p).classList.add('active');
if (p==='ds') rDash();
if (p==='rq') rReqs(rf);
if (p==='ac') iACal();
if (p==='mb') rMembs();
}
// ADMIN: Dashboard render
function rDash() {
const tm = USERS.filter(u => u.role === 'user');
const pd = LEAVES.filter(l => l.status === 'pending');
const ap = LEAVES.filter(l => l.status === 'approved');
const rj = LEAVES.filter(l => l.status === 'rejected');
updatePendingBadge();
const ast = document.getElementById('ast'); if (!ast) return;
ast.innerHTML =
stCard('#7c3aed','👥', tm.length, 'Team Members') +
stCard('#f59e0b','⏳', pd.length, 'Pending Requests') +
stCard('#10b981','✅', ap.length, 'Approved Leaves') +
stCard('#ef4444','❌', rj.length, 'Rejected');
const apl = document.getElementById('apl'); if (!apl) return;
if (!pd.length) {
apl.innerHTML = '🎉
Koi pending request nahi hai!
';
return;
}
const sorted = [...pd].sort((a,b) => new Date(a.date) - new Date(b.date));
apl.innerHTML = sorted.slice(0,6).map(l => `
${ini(l.uname)}
${l.uname}
📅 ${fmD(l.date)}${l.reason ? ' · ' + l.reason : ''}
`).join('');
}
// ADMIN: Filter
function aFlt(f, btn) {
rf = f;
document.querySelectorAll('.ftab').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
rReqs(f);
}
// ADMIN: Requests table
function rReqs(f) {
let ls = [...LEAVES];
if (f !== 'all') ls = ls.filter(l => l.status === f);
ls.sort((a,b) => new Date(b.createdAt) - new Date(a.createdAt));
const tb = document.getElementById('rtb'); if (!tb) return;
if (!ls.length) {
tb.innerHTML = ' |
';
return;
}
tb.innerHTML = ls.map(l => `
${ini(l.uname)} ${l.uname} |
${fmD(l.date)} |
${l.reason||'---'} |
${fmS(l.createdAt)} |
${sbdg(l.status)} |
${l.status==='pending'
? ``
: '---'} |
`).join('');
}
// ADMIN: Update leave status (Firestore)
async function setSt(id, st) {
try {
await FS.update('leaves', id, { status: st, updatedAt: new Date().toISOString() });
// LEAVES cache updated via realtime listener
toast(st==='approved' ? '✅ Leave approve kar di!' : '❌ Leave reject kar di!', st==='approved' ? 'success' : 'error');
} catch(e) {
toast('⚠ Error: ' + e.message, 'error');
}
}
// ADMIN: Calendar events
function apEvs() {
return LEAVES
.filter(l => l.status === 'approved')
.map(l => ({ id:l.id, title:l.uname, date:l.date, backgroundColor:gCol(l.uid), borderColor:'transparent' }));
}
function refreshAdminCal() {
if (cals.ac) {
cals.ac.removeAllEvents();
cals.ac.addEventSource(apEvs());
bLgnd('alg');
}
}
function bLgnd(cid) {
const el = document.getElementById(cid); if (!el) return;
const ms = USERS.filter(u => u.role === 'user');
el.innerHTML = ms.map(m => `
`).join('') || 'Koi member nahi';
}
function iACal() {
bLgnd('alg');
if (cals.ac) return;
raf(() => {
const el = document.getElementById('acal'); if (!el) return;
cals.ac = new FullCalendar.Calendar(el, {
initialView: 'dayGridMonth',
headerToolbar: { left:'prev,next today', center:'title', right:'dayGridMonth,dayGridWeek' },
height: 580, events: apEvs(),
eventClick(info) { toast('😆 '+info.event.title+' — '+fmD(info.event.startStr), 'info'); }
});
cals.ac.render();
});
}
// ADMIN: Members list
function rMembs() {
const ms = USERS.filter(u => u.role === 'user');
const el = document.getElementById('mbl'); if (!el) return;
if (!ms.length) {
el.innerHTML = '👥
Koi member nahi. "+ Member Add Karo" se add karo.
';
return;
}
el.innerHTML = ms.map(m => {
const ac = LEAVES.filter(l => l.uid === m.id && l.status === 'approved').length;
const pc = LEAVES.filter(l => l.uid === m.id && l.status === 'pending').length;
return `
✅ ${ac} approved
${pc ? `⏳ ${pc} pending` : ''}
`;
}).join('');
}
// ADMIN: Open add member modal
function oAddM() {
openM('👤 Naya Member Add Karo', `
💡 Yeh credentials member ko share karo — woh isse login karenge
`);
}
// ADMIN: Add member (Firestore)
async function addM() {
const n = document.getElementById('nm').value.trim();
const e = document.getElementById('ne').value.trim().toLowerCase();
const p = document.getElementById('np').value;
const btn = document.getElementById('add-btn');
if (!n||!e||!p) { toast('⚠ Sabhi fields bharo!','error'); return; }
btn.disabled = true;
btn.textContent = 'Adding...';
try {
// Check email uniqueness
const existing = USERS.find(u => u.email === e);
if (existing) { toast('⚠ Yeh email pehle se registered hai!','error'); btn.disabled=false; btn.textContent='✅ Add Karo'; return; }
const colorUsers = USERS.filter(u => u.role === 'user');
const ci = colorUsers.length % COLS.length;
const newUser = {
name: n, email: e, password: p,
role: 'user', color: COLS[ci],
createdAt: new Date().toISOString()
};
await FS.add('users', newUser);
// USERS will update via realtime listener
closeM();
toast('✅ ' + n + ' ko team mein add kar diya!', 'success');
} catch(err) {
toast('⚠ Error: ' + err.message, 'error');
btn.disabled = false;
btn.textContent = '✅ Add Karo';
}
}
// ADMIN: Remove member (Firestore)
async function rmM(id, name) {
if (!confirm('Kya aap "'+name+'" ko remove karna chahte hain?\nUnke sabhi leave records bhi delete ho jayenge.')) return;
try {
// Delete user
await FS.del('users', id);
// Delete all their leaves
const userLeaves = LEAVES.filter(l => l.uid === id);
await Promise.all(userLeaves.map(l => FS.del('leaves', l.id)));
// Cache updated via listeners
toast('🗑 '+name+' ko remove kar diya!', 'info');
} catch(err) {
toast('⚠ Error: ' + err.message, 'error');
}
}
// ════════════════════════════════════
// USER DASHBOARD
// ════════════════════════════════════
function buildUser() {
document.getElementById('sbn').innerHTML = `
Menu
🏖Meri Chutti
📝Chutti Mangao
📅Team Calendar
`;
document.getElementById('mc').innerHTML = `
🏖 Meri Chutti
Live — admin approve karte hi update hoga
📝 Chutti Mangao
Calendar mein kisi bhi future date par click karke request karo
📌 Aage ki kisi bhi date par click karo — chutti request karo!
📅 Team Calendar
Dekho team mein kaun kab chutti par hai
`;
rUSt(); rULL();
}
function uP(p) {
document.querySelectorAll('.pg').forEach(x => x.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(x => x.classList.remove('active'));
document.getElementById('pg-'+p).classList.add('active');
document.getElementById('n-'+p).classList.add('active');
if (p==='um') { rUSt(); rULL(); }
if (p==='ur') iURC();
if (p==='uc') iUTC();
}
// USER: Stats
function rUSt() {
const my = LEAVES.filter(l => l.uid === CU.id);
const el = document.getElementById('ust'); if (!el) return;
el.innerHTML =
stCard('#7c3aed','📋', my.length, 'Total Requests') +
stCard('#f59e0b','⏳', my.filter(l=>l.status==='pending').length, 'Pending') +
stCard('#10b981','✅', my.filter(l=>l.status==='approved').length, 'Approved') +
stCard('#ef4444','❌',my.filter(l=>l.status==='rejected').length, 'Rejected');
}
// USER: Leave history
function rULL() {
const my = LEAVES.filter(l => l.uid === CU.id).sort((a,b) => new Date(b.createdAt) - new Date(a.createdAt));
const el = document.getElementById('ull'); if (!el) return;
if (!my.length) {
el.innerHTML = '🌴
Abhi tak koi leave nahi ki.
"Chutti Mangao" se pehli request karo!
';
return;
}
el.innerHTML = my.map(l => `
📅 ${fmD(l.date)}
${l.reason || 'Koi reason nahi diya'}
${fmS(l.createdAt)} ko request ki
${sbdg(l.status)}
`).join('');
}
// USER: Init request calendar
function iURC() {
rUPend();
if (cals.ur) return;
raf(() => {
const el = document.getElementById('urc'); if (!el) return;
const myLeaves = LEAVES.filter(l => l.uid === CU.id);
const bg = myLeaves.map(l => ({
date: l.date, display: 'background',
backgroundColor: l.status==='approved' ? 'rgba(16,185,129,.25)' :
l.status==='rejected' ? 'rgba(239,68,68,.18)' :
'rgba(245,158,11,.22)'
}));
cals.ur = new FullCalendar.Calendar(el, {
initialView: 'dayGridMonth',
headerToolbar: { left:'prev,next today', center:'title', right:'' },
height: 420, events: bg,
dateClick(info) {
const d = info.dateStr;
if (d < tod()) { toast('⚠ Pichli date ke liye request nahi ho sakti!','error'); return; }
const ex = LEAVES.find(l => l.uid===CU.id && l.date===d);
if (ex) { toast('⚠ Is date ke liye pehle se request hai ('+ex.status+')','error'); return; }
oLM(d);
}
});
cals.ur.render();
});
}
function refreshUserReqCal() {
if (cals.ur) {
cals.ur.removeAllEvents();
const myLeaves = LEAVES.filter(l => l.uid === CU.id);
const bg = myLeaves.map(l => ({
date: l.date, display: 'background',
backgroundColor: l.status==='approved' ? 'rgba(16,185,129,.25)' :
l.status==='rejected' ? 'rgba(239,68,68,.18)' :
'rgba(245,158,11,.22)'
}));
cals.ur.addEventSource(bg);
}
rUPend();
}
// USER: Leave modal
function oLM(d) {
openM('📝 Chutti Ki Request', `
📅 Selected Date: ${fmD(d)}
`);
}
// USER: Submit leave (Firestore)
async function subLv(d) {
const reason = (document.getElementById('lvr')?.value || '').trim();
const btn = document.getElementById('sub-btn');
if (btn) { btn.disabled=true; btn.textContent='Submitting...'; }
try {
await FS.add('leaves', {
uid: CU.id, uname: CU.name,
date: d, reason,
status: 'pending',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
});
closeM();
toast('✅ Chutti ki request bhej di! Admin approve karega.', 'success');
// Cache updated via realtime listener
// Rebuild calendar to show new highlight
if (cals.ur) { cals.ur.destroy(); delete cals.ur; }
iURC();
rUSt(); rULL();
} catch(e) {
toast('⚠ Error: ' + e.message, 'error');
if (btn) { btn.disabled=false; btn.textContent='📤 Request Bhejo'; }
}
}
// USER: Pending list
function rUPend() {
const el = document.getElementById('upl'); if (!el) return;
const pd = LEAVES.filter(l => l.uid===CU.id && l.status==='pending')
.sort((a,b) => new Date(a.date) - new Date(b.date));
if (!pd.length) { el.innerHTML='✨
Koi pending request nahi hai
'; return; }
el.innerHTML = pd.map(l => `
📅 ${fmD(l.date)}
${l.reason || 'Koi reason nahi'}
⏳ Pending
`).join('');
}
// USER: Cancel leave (Firestore)
async function canLv(id) {
if (!confirm('Kya aap yeh leave request cancel karna chahte hain?')) return;
try {
await FS.del('leaves', id);
toast('🗑 Leave request cancel kar di!', 'info');
if (cals.ur) { cals.ur.destroy(); delete cals.ur; }
iURC(); rUSt(); rULL();
} catch(e) {
toast('⚠ Error: ' + e.message, 'error');
}
}
// USER: Team calendar
function iUTC() {
bLgnd('ulg');
if (cals.uc) return;
raf(() => {
const el = document.getElementById('utc'); if (!el) return;
cals.uc = new FullCalendar.Calendar(el, {
initialView: 'dayGridMonth',
headerToolbar: { left:'prev,next today', center:'title', right:'dayGridMonth,dayGridWeek' },
height: 580, events: apEvs(),
eventClick(info) { toast('😆 '+info.event.title+' — '+fmD(info.event.startStr), 'info'); }
});
cals.uc.render();
});
}
function refreshUserTeamCal() {
if (cals.uc) {
cals.uc.removeAllEvents();
cals.uc.addEventSource(apEvs());
bLgnd('ulg');
}
}
// ── KEYBOARD ──
document.addEventListener('keydown', e => {
if (e.key==='Enter' && document.getElementById('lp').style.display!=='none') doLogin();
});
// ── BOOT ──
boot();