Ilo/design/screen-students.jsx
2026-05-02 20:01:30 +03:30

287 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Students / CRM screen.
const STATUS_META = {
active: { fa: 'فعال', cls: 'ok' },
completed: { fa: 'فارغ‌التحصیل', cls: 'info' },
'at-risk': { fa: 'خطر ریزش', cls: 'warn' },
};
function StudentsScreen() {
const { STUDENTS, COURSES } = window.DATA;
const [tab, setTab] = React.useState('all');
const [selected, setSelected] = React.useState(new Set());
const [detail, setDetail] = React.useState(STUDENTS[0]);
const [search, setSearch] = React.useState('');
const filtered = STUDENTS.filter(s => {
if (tab !== 'all' && s.status !== tab) return false;
if (search && !s.name.includes(search) && !s.course.includes(search)) return false;
return true;
});
const toggleSel = (id) => {
const next = new Set(selected);
next.has(id) ? next.delete(id) : next.add(id);
setSelected(next);
};
const segmentCounts = {
all: STUDENTS.length,
active: STUDENTS.filter(s => s.status === 'active').length,
completed: STUDENTS.filter(s => s.status === 'completed').length,
'at-risk': STUDENTS.filter(s => s.status === 'at-risk').length,
};
return (
<div className="page-inner">
{/* header */}
<div className="page-h">
<div>
<h1>دانشجویان</h1>
<p className="lede">مدیریت ارتباط با {STUDENTS.length} دانشجو در ۲ دوره</p>
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button className="btn"><Icon name="upload" className="ic-sm" /> ورود از فایل</button>
<button className="btn"><Icon name="send" className="ic-sm" /> پیام انبوه</button>
<button className="btn primary"><Icon name="plus" className="ic-sm" /> افزودن دانشجو</button>
</div>
</div>
{/* stat strip */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 'var(--gap-md)' }}>
{[
{ l: 'کل دانشجویان', v: STUDENTS.length, sub: '+۴۸ این ماه', good: true },
{ l: 'فعال این هفته', v: STUDENTS.filter(s => s.status === 'active').length, sub: '۸۲٪ نرخ فعالیت', good: true },
{ l: 'تکمیل‌کنندگان', v: STUDENTS.filter(s => s.status === 'completed').length, sub: 'نرخ ۴۲٪', good: true },
{ l: 'در معرض ریزش', v: STUDENTS.filter(s => s.status === 'at-risk').length, sub: 'نیاز به پیگیری', good: false },
].map((s, i) => (
<div key={i} className="card card-pad">
<div className="muted" style={{ fontSize: 12, marginBottom: 8 }}>{s.l}</div>
<div className="num" style={{ fontSize: 24, fontWeight: 700, letterSpacing: '-0.01em' }}>
{String(s.v).replace(/\d/g, d => '۰۱۲۳۴۵۶۷۸۹'[d])}
</div>
<div style={{ marginTop: 8, fontSize: 11.5 }}>
<span className={`pill ${s.good ? 'ok' : 'warn'}`}>{s.sub}</span>
</div>
</div>
))}
</div>
{/* main: list + detail */}
<div style={{ display: 'grid', gridTemplateColumns: detail ? '1.6fr 1fr' : '1fr', gap: 'var(--gap-lg)', alignItems: 'flex-start' }}>
<div className="card">
{/* filters */}
<div style={{ padding: '14px 20px 0 20px' }}>
<div className="tabs" style={{ marginInline: -20, paddingInline: 20 }}>
{[
['all', 'همه'],
['active', 'فعال'],
['completed', 'تکمیل‌شده'],
['at-risk', 'در معرض ریزش'],
].map(([k, v]) => (
<button key={k} className="tab" aria-selected={tab === k} onClick={() => setTab(k)}>
{v}
<span className="num" style={{
marginInlineStart: 6, fontSize: 11,
background: tab === k ? 'var(--accent-soft)' : 'var(--line-soft)',
color: tab === k ? 'var(--accent-ink)' : 'var(--ink-3)',
padding: '1px 6px', borderRadius: 'var(--r-pill)'
}}>
{String(segmentCounts[k]).replace(/\d/g, d => '۰۱۲۳۴۵۶۷۸۹'[d])}
</span>
</button>
))}
</div>
<div style={{ display: 'flex', gap: 10, padding: '14px 0 14px 0', alignItems: 'center' }}>
<div className="search" style={{ flex: 1, width: 'auto', maxWidth: 360 }}>
<Icon name="search" className="ic-sm" />
<input placeholder="جستجوی نام یا دوره…" value={search} onChange={e => setSearch(e.target.value)} />
</div>
<button className="btn sm"><Icon name="filter" className="ic-sm" /> همه دورهها</button>
<button className="btn sm"><Icon name="sort" className="ic-sm" /> مرتبسازی</button>
<div style={{ flex: 1 }} />
{selected.size > 0 && (
<span className="pill accent num">
{String(selected.size).replace(/\d/g, d => '۰۱۲۳۴۵۶۷۸۹'[d])} انتخاب شده
</span>
)}
</div>
</div>
{/* table */}
<div style={{ overflow: 'auto' }}>
<table className="tbl">
<thead>
<tr>
<th style={{ width: 36 }}>
<input type="checkbox" className="chk"
checked={selected.size === filtered.length && filtered.length > 0}
onChange={() => {
if (selected.size === filtered.length) setSelected(new Set());
else setSelected(new Set(filtered.map(s => s.id)));
}} />
</th>
<th>دانشجو</th>
<th>دوره</th>
<th>پیشرفت</th>
<th>وضعیت</th>
<th>آخرین فعالیت</th>
<th style={{ width: 40 }}></th>
</tr>
</thead>
<tbody>
{filtered.map(s => (
<tr key={s.id} onClick={() => setDetail(s)}
style={{ background: detail?.id === s.id ? 'var(--accent-soft)' : undefined }}>
<td onClick={e => e.stopPropagation()}>
<input type="checkbox" className="chk"
checked={selected.has(s.id)}
onChange={() => toggleSel(s.id)} />
</td>
<td>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div className="av" style={{ background: s.color, fontSize: 11 }}>
{s.initials}
</div>
<div>
<div style={{ fontWeight: 600 }}>{s.name}</div>
<div className="muted" style={{ fontSize: 11.5 }}>{s.tag}</div>
</div>
</div>
</td>
<td><span className="muted" style={{ fontSize: 12.5 }}>{s.course}</span></td>
<td>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div className="prog" style={{ width: 80 }}>
<i style={{ width: s.progress + '%', background: s.progress < 30 ? 'oklch(0.78 0.14 75)' : (s.progress >= 100 ? 'oklch(0.62 0.14 155)' : 'var(--accent)') }} />
</div>
<span className="num" style={{ fontSize: 12, fontWeight: 600, minWidth: 32 }}>
{String(s.progress).replace(/\d/g, d => '۰۱۲۳۴۵۶۷۸۹'[d])}٪
</span>
</div>
</td>
<td><span className={`pill ${STATUS_META[s.status].cls}`}>{STATUS_META[s.status].fa}</span></td>
<td><span className="muted" style={{ fontSize: 12.5 }}>{s.lastActive}</span></td>
<td onClick={e => e.stopPropagation()}>
<button className="btn ghost sm"><Icon name="more" className="ic-sm" /></button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* footer */}
<div style={{ padding: '14px 20px', borderTop: '1px solid var(--line-soft)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span className="muted" style={{ fontSize: 12 }}>
نمایش {filtered.length} از {STUDENTS.length} دانشجو
</span>
<div style={{ display: 'flex', gap: 4 }}>
<button className="btn sm"><Icon name="chevron" className="ic-sm" /></button>
<button className="btn sm" style={{ background: 'var(--accent-soft)', color: 'var(--accent-ink)', borderColor: 'transparent' }}>۱</button>
<button className="btn sm">۲</button>
<button className="btn sm">۳</button>
<button className="btn sm"><Icon name="chevron" className="ic-sm flipx" /></button>
</div>
</div>
</div>
{/* detail panel */}
{detail && (
<div className="col">
<div className="card card-pad">
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
<div className="av av-lg" style={{ background: detail.color, width: 56, height: 56, fontSize: 16 }}>
{detail.initials}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 16, fontWeight: 700 }}>{detail.name}</div>
<div className="muted num" style={{ fontSize: 12, marginTop: 2 }}>
عضو از {detail.joined}
</div>
</div>
<button className="btn ghost sm"><Icon name="more" className="ic-sm" /></button>
</div>
<div style={{ display: 'flex', gap: 6, marginTop: 14 }}>
<button className="btn sm" style={{ flex: 1 }}><Icon name="mail" className="ic-sm" /> پیام</button>
<button className="btn sm" style={{ flex: 1 }}><Icon name="phone" className="ic-sm" /> تماس</button>
<button className="btn sm"><Icon name="star" className="ic-sm" /></button>
</div>
</div>
<div className="card">
<div className="card-h">
<h3>پیشرفت در دوره</h3>
</div>
<div className="card-pad col">
<div>
<div className="between" style={{ marginBottom: 8 }}>
<span style={{ fontSize: 13, fontWeight: 500 }}>{detail.course}</span>
<span className="num" style={{ fontSize: 13, fontWeight: 600 }}>
{String(detail.progress).replace(/\d/g, d => '۰۱۲۳۴۵۶۷۸۹'[d])}٪
</span>
</div>
<div className="prog">
<i style={{ width: detail.progress + '%' }} />
</div>
<div className="muted num" style={{ fontSize: 11.5, marginTop: 8, display: 'flex', justifyContent: 'space-between' }}>
<span>۲۴ از ۳۸ درس</span>
<span>۸ ساعت تماشا</span>
</div>
</div>
<div className="divider" />
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 12 }}>
{[
{ l: 'مبلغ پرداختی', v: detail.spent + ' ت', cls: 'num' },
{ l: 'آزمون‌های موفق', v: '۳ از ۴', cls: 'num' },
{ l: 'تمرین ارسالی', v: '۵', cls: 'num' },
{ l: 'امتیاز رضایت', v: '⭐ ۴٫۸', cls: '' },
].map((s, i) => (
<div key={i} style={{ padding: 10, background: 'var(--surface-2)', borderRadius: 'var(--r-sm)' }}>
<div className="muted" style={{ fontSize: 11 }}>{s.l}</div>
<div className={s.cls} style={{ fontSize: 14, fontWeight: 600, marginTop: 2 }}>{s.v}</div>
</div>
))}
</div>
</div>
</div>
<div className="card">
<div className="card-h">
<h3>تایملاین</h3>
</div>
<div style={{ padding: '6px 0 14px' }}>
{[
{ ic: 'check', t: 'درس «تئوری رنگ کاربردی» را تمام کرد', when: '۲ ساعت پیش', color: 'oklch(0.62 0.14 155)' },
{ ic: 'chat', t: 'پرسشی در گفت‌وگوی دوره مطرح کرد', when: 'دیروز', color: 'oklch(0.70 0.12 240)' },
{ ic: 'quiz', t: 'به آزمون فصل اول نمره ۹۲ گرفت', when: '۳ روز پیش', color: 'oklch(0.72 0.12 280)' },
{ ic: 'star', t: 'دوره را خریداری کرد', when: detail.joined, color: 'oklch(0.74 0.13 50)' },
].map((x, i) => (
<div key={i} style={{ display: 'flex', gap: 12, padding: '8px 20px', alignItems: 'flex-start' }}>
<div style={{
width: 24, height: 24, borderRadius: '50%',
background: 'color-mix(in oklch, ' + x.color + ' 22%, transparent)',
color: x.color,
display: 'grid', placeItems: 'center', flexShrink: 0,
}}>
<Icon name={x.ic} className="ic-sm" style={{ width: 12, height: 12 }} />
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 13 }}>{x.t}</div>
<div className="muted num" style={{ fontSize: 11, marginTop: 1 }}>{x.when}</div>
</div>
</div>
))}
</div>
</div>
</div>
)}
</div>
</div>
);
}
window.StudentsScreen = StudentsScreen;