PM_Console/frontend/src/pages/MeetingWorkspace.jsx

194 خطوط
13 KiB
JavaScript
خام سرزنش تاریخچه

مخزن invisible runes header

راهنمای مخزن invisible runes توضیحات

راهنمای مخزن ambiguous runes توضیحات

import { useCallback, useEffect, useMemo, useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import { ArrowRight, Ban, CheckCircle2, ClipboardList, FileText, Play, Plus, Save, ShieldAlert, Users } from 'lucide-react';
import toast from 'react-hot-toast';
import api from '../services/api';
import Modal from '../components/Modal';
import { formatJalaliDate } from '../utils/date';
const stages = [['before', 'قبل از جلسه'], ['during', 'حین جلسه'], ['after', 'بعد از جلسه']];
export default function MeetingWorkspace() {
const { id } = useParams();
const [meeting, setMeeting] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [stage, setStage] = useState('before');
const [saving, setSaving] = useState(false);
const [form, setForm] = useState({ objective: '', agenda: '', notes: '', summary: '' });
const [quickForm, setQuickForm] = useState(null);
const load = useCallback(async () => {
setLoading(true);
try {
const { data } = await api.get(`/meetings/${id}/workspace`);
setMeeting(data.data);
setForm({ objective: data.data.objective || '', agenda: data.data.agenda || '', notes: data.data.notes || '', summary: data.data.summary || '' });
setStage(data.data.status === 'completed' ? 'after' : data.data.status === 'in_progress' ? 'during' : 'before');
setError('');
} catch {
setError('دریافت فضای کاری جلسه ناموفق بود.');
} finally {
setLoading(false);
}
}, [id]);
useEffect(() => { load(); }, [load]);
const saveNotes = async () => {
setSaving(true);
try {
await api.patch(`/meetings/${id}`, form);
toast.success('اطلاعات جلسه ذخیره شد');
await load();
} catch {
toast.error('ذخیره جلسه ناموفق بود');
} finally {
setSaving(false);
}
};
const startMeeting = async () => {
try {
await api.post(`/meetings/${id}/start`);
toast.success('جلسه شروع شد');
await load();
} catch (err) {
toast.error(err.response?.data?.message || 'شروع جلسه ناموفق بود');
}
};
const completeMeeting = async () => {
try {
await api.post(`/meetings/${id}/complete`, { summary: form.summary });
toast.success('جلسه تکمیل شد');
await load();
} catch (err) {
toast.error(err.response?.data?.message || 'تکمیل جلسه ناموفق بود');
}
};
if (loading) return <div className="page-container workspace-page"><div className="workspace-loading">{Array.from({ length: 7 }).map((_, index) => <div className="skeleton" key={index} />)}</div></div>;
if (error || !meeting) return <div className="page-container workspace-page"><div className="monitor-error"><ShieldAlert size={34} /><h2>{error}</h2><button className="btn btn-primary" onClick={load}>تلاش دوباره</button></div></div>;
return (
<div className="page-container workspace-page meeting-workspace">
<header className="workspace-hero">
<div className="workspace-breadcrumb"><Link to="/meetings"><ArrowRight size={16} /> جلسات</Link><span>/</span><span>{meeting.title}</span></div>
<div className="workspace-title-row"><h1>{meeting.title}</h1><span className={`meeting-status status-${meeting.status}`}>{meeting.status}</span></div>
<p>{meeting.objective || 'هدف جلسه هنوز ثبت نشده است.'}</p>
<div className="workspace-context"><span>{formatJalaliDate(meeting.date)}</span><span>{meeting.start_time?.slice(0, 5) || 'بدون ساعت'} تا {meeting.end_time?.slice(0, 5) || 'نامشخص'}</span><span>{meeting.project?.title || 'جلسه عمومی'}</span>{meeting.sprint?.title && <Link to={`/sprints/${meeting.sprint_id}/workspace`}>{meeting.sprint.title}</Link>}</div>
</header>
<nav className="meeting-stage-tabs">
{stages.map(([key, label]) => <button type="button" key={key} className={stage === key ? 'active' : ''} onClick={() => setStage(key)}>{label}</button>)}
</nav>
<div className="meeting-workspace-layout">
<main>
{stage === 'before' && (
<div className="meeting-stage-content">
<section className="workspace-card">
<header><div><ClipboardList size={18} /><h2>آمادهسازی جلسه</h2></div></header>
<label className="form-group"><span className="form-label">هدف جلسه</span><textarea className="form-textarea" value={form.objective} onChange={(event) => setForm({ ...form, objective: event.target.value })} /></label>
<label className="form-group"><span className="form-label">دستور جلسه</span><textarea className="form-textarea meeting-agenda-input" value={form.agenda} onChange={(event) => setForm({ ...form, agenda: event.target.value })} /></label>
</section>
<section className="workspace-card"><header><div><Users size={18} /><h2>شرکتکنندگان</h2></div><span>{meeting.participants?.length || 0} نفر</span></header>
<div className="participant-grid">{(meeting.participants || []).map((person) => <div key={person.id}><span className="avatar avatar-sm">{person.name?.[0]}</span><strong>{person.name}</strong></div>)}</div>
{(meeting.participants || []).length === 0 && <div className="workspace-empty">شرکتکنندهای اضافه نشده است.</div>}
</section>
</div>
)}
{stage === 'during' && (
<div className="meeting-stage-content">
<section className="workspace-card live-notes-card">
<header><div><FileText size={18} /><h2>یادداشت زنده</h2></div><span className="live-indicator"><i /> جلسه در جریان</span></header>
<textarea className="form-textarea" value={form.notes} onChange={(event) => setForm({ ...form, notes: event.target.value })} placeholder="نکات، گفتگوها و جمع‌بندی‌های مهم را ثبت کنید..." />
</section>
<div className="meeting-capture-grid">
<button type="button" onClick={() => setQuickForm({ type: 'decision', title: '', description: '' })}><CheckCircle2 size={22} /><strong>ثبت تصمیم</strong><span>نتیجه تصمیمگیری را ثبت کنید</span></button>
<button type="button" onClick={() => setQuickForm({ type: 'action', title: '', owner_id: '', due_date: '', priority: 'medium' })}><Plus size={22} /><strong>ثبت اقدام</strong><span>Owner و سررسید مشخص کنید</span></button>
<button type="button" onClick={() => setQuickForm({ type: 'blocker', title: '', description: '', severity: 'medium' })}><Ban size={22} /><strong>ثبت مانع</strong><span>مانع را به Sprint بازگردانید</span></button>
</div>
</div>
)}
{stage === 'after' && (
<div className="meeting-stage-content">
<section className="workspace-card">
<header><div><FileText size={18} /><h2>خلاصه و صورتجلسه</h2></div></header>
<textarea className="form-textarea meeting-summary-input" value={form.summary} onChange={(event) => setForm({ ...form, summary: event.target.value })} placeholder="خلاصه ساختاریافته جلسه..." />
</section>
<ResultSection title="تصمیم‌ها" items={meeting.structured_decisions} />
<ResultSection title="اقدام‌ها" items={meeting.structured_action_items} />
<ResultSection title="موانع" items={meeting.blockers} />
</div>
)}
</main>
<aside className="meeting-context-panel">
<section className="workspace-card"><h3>اطلاعات جلسه</h3>
<dl><div><dt>نوع</dt><dd>{meeting.type?.name || meeting.meeting_type}</dd></div><div><dt>Owner</dt><dd>{meeting.owner?.name || meeting.creator?.name || ''}</dd></div><div><dt>Facilitator</dt><dd>{meeting.facilitator?.name || ''}</dd></div><div><dt>مکان</dt><dd>{meeting.location || ''}</dd></div></dl>
</section>
<section className="workspace-card"><h3>خروجی فعلی</h3><dl><div><dt>تصمیم</dt><dd>{meeting.structured_decisions?.length || 0}</dd></div><div><dt>اقدام</dt><dd>{meeting.structured_action_items?.length || meeting.action_items?.length || 0}</dd></div><div><dt>مانع</dt><dd>{meeting.blockers?.length || 0}</dd></div><div><dt>پیوست</dt><dd>{meeting.files?.length || 0}</dd></div></dl></section>
</aside>
</div>
<div className="meeting-sticky-controls">
<button type="button" className="btn btn-outline" onClick={saveNotes} disabled={saving}><Save size={16} /> {saving ? 'در حال ذخیره...' : 'ذخیره'}</button>
{meeting.status !== 'in_progress' && meeting.status !== 'completed' && <button type="button" className="btn btn-primary" onClick={startMeeting}><Play size={16} /> شروع جلسه</button>}
{meeting.status === 'in_progress' && <button type="button" className="btn btn-success" onClick={completeMeeting}><CheckCircle2 size={16} /> پایان جلسه</button>}
</div>
{quickForm && <QuickCapture meetingId={meeting.id} form={quickForm} setForm={setQuickForm} onClose={() => setQuickForm(null)} onSaved={load} />}
</div>
);
}
function ResultSection({ title, items = [] }) {
return <section className="workspace-card"><header><div><CheckCircle2 size={18} /><h2>{title}</h2></div><span>{items?.length || 0}</span></header>{!items?.length ? <div className="workspace-empty">موردی ثبت نشده است.</div> : items.map((item) => <div className="workspace-data-row" key={item.id}><div><strong>{item.title}</strong><small>{item.description || item.status}</small></div><span className="badge badge-primary">{item.status || item.severity}</span></div>)}</section>;
}
function QuickCapture({ meetingId, form, setForm, onClose, onSaved }) {
const [users, setUsers] = useState([]);
const [saving, setSaving] = useState(false);
useEffect(() => {
if (form.type === 'action') api.get('/users', { params: { per_page: 100 } }).then(({ data }) => setUsers(data.data || [])).catch(() => {});
}, [form.type]);
const config = useMemo(() => ({
decision: { title: 'ثبت تصمیم', endpoint: 'decisions' },
action: { title: 'ثبت اقدام', endpoint: 'structured-action-items' },
blocker: { title: 'ثبت مانع', endpoint: 'blockers' },
})[form.type], [form.type]);
const save = async () => {
if (!form.title?.trim()) { toast.error('عنوان الزامی است'); return; }
if (form.type === 'action' && (!form.owner_id || !form.due_date)) { toast.error('Owner و سررسید الزامی است'); return; }
setSaving(true);
try {
const payload = { ...form }; delete payload.type;
await api.post(`/meetings/${meetingId}/${config.endpoint}`, payload);
toast.success('ثبت شد');
onClose();
await onSaved();
} catch (err) {
toast.error(err.response?.data?.message || 'ثبت مورد ناموفق بود');
} finally {
setSaving(false);
}
};
return (
<Modal title={config.title} onClose={onClose} footer={<><button className="btn btn-secondary" onClick={onClose}>انصراف</button><button className="btn btn-primary" onClick={save} disabled={saving}>{saving ? 'در حال ثبت...' : 'ثبت'}</button></>}>
<div className="compact-modal-form">
<label className="form-group"><span className="form-label">عنوان</span><input className="form-input" value={form.title} onChange={(event) => setForm({ ...form, title: event.target.value })} /></label>
{form.type !== 'action' && <label className="form-group"><span className="form-label">توضیحات</span><textarea className="form-textarea" value={form.description || ''} onChange={(event) => setForm({ ...form, description: event.target.value })} /></label>}
{form.type === 'action' && <><label className="form-group"><span className="form-label">Owner</span><select className="form-select" value={form.owner_id} onChange={(event) => setForm({ ...form, owner_id: event.target.value })}><option value="">انتخاب</option>{users.map((user) => <option key={user.id} value={user.id}>{user.name}</option>)}</select></label><label className="form-group"><span className="form-label">سررسید</span><input type="date" className="form-input" value={form.due_date} onChange={(event) => setForm({ ...form, due_date: event.target.value })} /></label></>}
{form.type === 'blocker' && <label className="form-group"><span className="form-label">شدت</span><select className="form-select" value={form.severity} onChange={(event) => setForm({ ...form, severity: event.target.value })}><option value="low">کم</option><option value="medium">متوسط</option><option value="high">زیاد</option><option value="critical">بحرانی</option></select></label>}
</div>
</Modal>
);
}