PM_Console/frontend/src/pages/MeetingList.jsx

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

مخزن invisible runes header

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

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

import { useState, useEffect } from 'react';
import api from '../services/api';
import Modal from '../components/Modal';
import ConfirmDialog from '../components/ConfirmDialog';
import EmptyState from '../components/EmptyState';
import { TableSkeleton } from '../components/LoadingSkeleton';
import PersianDateInput from '../components/PersianDateInput';
import toast from 'react-hot-toast';
import { Calendar, Edit3, Trash2, X, ArrowLeft } from 'lucide-react';
import { formatJalaliDate, todayIsoDate } from '../utils/date';
import { Link } from 'react-router-dom';
export default function MeetingList() {
const [meetings, setMeetings] = useState([]);
const [loading, setLoading] = useState(true);
const [projects, setProjects] = useState([]);
const [users, setUsers] = useState([]);
const [filters, setFilters] = useState({ project_id: '', meeting_type: '' });
const [showModal, setShowModal] = useState(false);
const [editing, setEditing] = useState(null);
const [deleting, setDeleting] = useState(null);
const [saving, setSaving] = useState(false);
const [selected, setSelected] = useState(null);
const [meta, setMeta] = useState({});
const [page, setPage] = useState(1);
const [form, setForm] = useState({ title: '', date: '', start_time: '', end_time: '', meeting_type: 'online', location: '', meeting_link: '', project_id: '', agenda: '', notes: '', decisions: '' });
const [actionForm, setActionForm] = useState({ title: '', assigned_to: '', due_date: '' });
const fetchMeetings = () => {
setLoading(true);
const params = { page, per_page: 15 };
if (filters.project_id) params.project_id = filters.project_id;
if (filters.meeting_type) params.meeting_type = filters.meeting_type;
api.get('/meetings', { params }).then(({ data }) => {
setMeetings(data.data || []);
setMeta(data.meta || {});
}).catch(() => toast.error('خطا')).finally(() => setLoading(false));
};
useEffect(() => { fetchMeetings(); }, [page, filters]);
useEffect(() => {
api.get('/projects', { params: { per_page: 100 } }).then(({ data }) => setProjects(data.data || [])).catch(() => {});
api.get('/users', { params: { per_page: 100 } }).then(({ data }) => setUsers(data.data || [])).catch(() => {});
}, []);
const openCreate = () => {
setEditing(null);
setForm({ title: '', date: todayIsoDate(), start_time: '', end_time: '', meeting_type: 'online', location: '', meeting_link: '', project_id: '', agenda: '', notes: '', decisions: '' });
setShowModal(true);
};
const openEdit = (m) => {
setEditing(m);
setForm({
title: m.title, date: m.date?.slice(0, 10) || '', start_time: m.start_time || '', end_time: m.end_time || '',
meeting_type: m.meeting_type, location: m.location || '', meeting_link: m.meeting_link || '',
project_id: m.project_id || '', agenda: m.agenda || '', notes: m.notes || '', decisions: m.decisions || '',
});
setShowModal(true);
};
const handleSave = async (e) => {
e.preventDefault();
if (!form.title || !form.date) { toast.error('عنوان و تاریخ الزامی است'); return; }
setSaving(true);
try {
if (editing) { await api.put(`/meetings/${editing.id}`, { ...form, _method: 'PUT' }); toast.success('به‌روزرسانی شد'); }
else { await api.post('/meetings', form); toast.success('ایجاد شد'); }
setShowModal(false); fetchMeetings();
} catch (err) { toast.error(err.response?.data?.message || 'خطا'); } finally { setSaving(false); }
};
const handleDelete = async () => {
try { await api.delete(`/meetings/${deleting.id}`); toast.success('حذف شد'); setDeleting(null); fetchMeetings(); }
catch { toast.error('خطا'); }
};
const viewDetail = async (m) => {
try {
const { data } = await api.get(`/meetings/${m.id}`);
setSelected(data.data);
} catch { toast.error('خطا'); }
};
const addParticipant = async (userId) => {
try {
await api.post(`/meetings/${selected.id}/participants`, { user_id: userId });
toast.success('شرکت‌کننده اضافه شد');
viewDetail(selected);
} catch { toast.error('خطا'); }
};
const removeParticipant = async (userId) => {
try {
await api.delete(`/meetings/${selected.id}/participants/${userId}`);
toast.success('شرکت‌کننده حذف شد');
viewDetail(selected);
} catch { toast.error('خطا'); }
};
const addActionItem = async () => {
if (!actionForm.title.trim()) { toast.error('عنوان الزامی است'); return; }
try {
await api.post(`/meetings/${selected.id}/action-items`, actionForm);
toast.success('ایتم اقدام اضافه شد');
setActionForm({ title: '', assigned_to: '', due_date: '' });
viewDetail(selected);
} catch { toast.error('خطا'); }
};
const toggleActionItem = async (item) => {
try {
await api.put(`/meetings/${selected.id}/action-items/${item.id}`, { is_completed: !item.is_completed });
viewDetail(selected);
} catch { toast.error('خطا'); }
};
const convertToTask = async (item) => {
try {
await api.post(`/meetings/${selected.id}/action-items/${item.id}/convert-to-task`, { project_id: selected.project_id || 1 });
toast.success('به تسک تبدیل شد');
viewDetail(selected);
} catch { toast.error('خطا'); }
};
return (
<div className="page-container">
<div className="page-header">
<button className="btn btn-primary" onClick={openCreate}>+ جلسه جدید</button>
</div>
<div className="filter-bar">
<select className="form-select" value={filters.project_id} onChange={e => setFilters({...filters, project_id: e.target.value})}>
<option value="">همه پروژهها</option>
{projects.map(p => <option key={p.id} value={p.id}>{p.title}</option>)}
</select>
<select className="form-select" value={filters.meeting_type} onChange={e => setFilters({...filters, meeting_type: e.target.value})}>
<option value="">همه انواع</option>
<option value="online">آنلاین</option>
<option value="in_person">حضوری</option>
<option value="hybrid">ترکیبی</option>
</select>
</div>
{loading ? <TableSkeleton rows={6} cols={5} /> : meetings.length === 0 ? (
<EmptyState icon={Calendar} title="جلسه‌ای وجود ندارد" description="هنوز هیچ جلسه‌ای ثبت نشده" action={<button className="btn btn-primary" onClick={openCreate}>ثبت جلسه جدید</button>} />
) : (
<div className="card" style={{ padding: 0, overflow: 'hidden' }}>
<div className="table-container">
<table>
<thead><tr><th>عنوان</th><th>پروژه</th><th>نوع</th><th>تاریخ</th><th>ساعت</th><th>شرکتکنندگان</th><th>عملیات</th></tr></thead>
<tbody>
{meetings.map(m => (
<tr key={m.id} onClick={() => viewDetail(m)} style={{ cursor: 'pointer' }}>
<td style={{ fontWeight: 500 }}>{m.title}</td>
<td>{m.project?.title || '—'}</td>
<td><span className="badge badge-info">{m.meeting_type === 'online' ? 'آنلاین' : m.meeting_type === 'in_person' ? 'حضوری' : 'ترکیبی'}</span></td>
<td>{formatJalaliDate(m.date)}</td>
<td>{m.start_time || '—'} {m.end_time ? `- ${m.end_time}` : ''}</td>
<td>{m.participants_count ?? m.participants?.length ?? 0}</td>
<td>
<div style={{ display: 'flex', gap: '0.25rem' }} onClick={e => e.stopPropagation()}>
<button className="btn btn-sm btn-secondary" onClick={() => openEdit(m)}><Edit3 size={14} /></button>
<button className="btn btn-sm btn-danger" onClick={() => setDeleting(m)}><Trash2 size={14} /></button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{meta?.last_page > 1 && (
<div style={{ display: 'flex', justifyContent: 'center', gap: '0.5rem', padding: '1rem', borderTop: '1px solid var(--gray-100)' }}>
{Array.from({ length: meta.last_page }, (_, i) => i + 1).map(p => (
<button key={p} className={`btn btn-sm ${page === p ? 'btn-primary' : 'btn-secondary'}`} onClick={() => setPage(p)}>{p}</button>
))}
</div>
)}
</div>
)}
{showModal && (
<Modal title={editing ? 'ویرایش جلسه' : 'جلسه جدید'} onClose={() => setShowModal(false)} size="lg" footer={
<><button className="btn btn-secondary" onClick={() => setShowModal(false)}>انصراف</button><button className="btn btn-primary" onClick={handleSave} disabled={saving}>{saving ? '...' : 'ذخیره'}</button></>
}>
<form onSubmit={handleSave} className="meeting-form-compact">
<div className="grid grid-2" style={{ gap: '0.75rem' }}>
<div className="form-group"><label className="form-label">عنوان *</label><input className="form-input" value={form.title} onChange={e => setForm({...form, title: e.target.value})} /></div>
<div className="form-group"><label className="form-label">پروژه</label>
<select className="form-select" value={form.project_id} onChange={e => setForm({...form, project_id: e.target.value})}>
<option value="">انتخاب کنید</option>
{projects.map(p => <option key={p.id} value={p.id}>{p.title}</option>)}
</select>
</div>
<div className="form-group"><label className="form-label">تاریخ *</label><PersianDateInput value={form.date} onChange={value => setForm({...form, date: value})} /></div>
<div className="form-group"><label className="form-label">نوع جلسه</label>
<select className="form-select" value={form.meeting_type} onChange={e => setForm({...form, meeting_type: e.target.value})}>
<option value="online">آنلاین</option>
<option value="in_person">حضوری</option>
<option value="hybrid">ترکیبی</option>
</select>
</div>
<div className="form-group"><label className="form-label">ساعت شروع</label><input className="form-input" type="time" value={form.start_time} onChange={e => setForm({...form, start_time: e.target.value})} /></div>
<div className="form-group"><label className="form-label">ساعت پایان</label><input className="form-input" type="time" value={form.end_time} onChange={e => setForm({...form, end_time: e.target.value})} /></div>
<div className="form-group"><label className="form-label">مکان</label><input className="form-input" value={form.location} onChange={e => setForm({...form, location: e.target.value})} /></div>
<div className="form-group"><label className="form-label">لینک جلسه</label><input className="form-input" value={form.meeting_link} onChange={e => setForm({...form, meeting_link: e.target.value})} /></div>
</div>
<div className="grid grid-3" style={{ gap: '0.75rem' }}>
<div className="form-group"><label className="form-label">دستور جلسه</label><textarea className="form-textarea" value={form.agenda} onChange={e => setForm({...form, agenda: e.target.value})} /></div>
<div className="form-group"><label className="form-label">یادداشتها</label><textarea className="form-textarea" value={form.notes} onChange={e => setForm({...form, notes: e.target.value})} /></div>
<div className="form-group"><label className="form-label">تصمیمات</label><textarea className="form-textarea" value={form.decisions} onChange={e => setForm({...form, decisions: e.target.value})} /></div>
</div>
</form>
</Modal>
)}
{deleting && <ConfirmDialog title="حذف جلسه" message={`آیا از حذف "${deleting.title}" اطمینان دارید؟`} onConfirm={handleDelete} onCancel={() => setDeleting(null)} danger />}
{selected && (
<>
<div className="drawer-overlay" onClick={() => setSelected(null)} />
<div className="drawer">
<div className="drawer-header">
<h3 className="drawer-title">{selected.title}</h3>
<button className="modal-close" onClick={() => setSelected(null)}>&times;</button>
</div>
<div className="drawer-body">
<Link className="btn btn-primary" style={{ width: '100%', marginBottom: '1rem' }} to={`/meetings/${selected.id}/workspace`} onClick={() => setSelected(null)}>
ورود به فضای کاری جلسه <ArrowLeft size={16} />
</Link>
<div className="card" style={{ padding: '1rem', marginBottom: '1rem' }}>
<div style={{ display: 'grid', gap: '0.5rem', fontSize: '0.875rem' }}>
<div><strong>پروژه:</strong> {selected.project?.title || ''}</div>
<div><strong>تاریخ:</strong> {formatJalaliDate(selected.date)} {selected.start_time && `(${selected.start_time}${selected.end_time ? `-${selected.end_time}` : ''})`}</div>
<div><strong>نوع:</strong> {selected.meeting_type === 'online' ? 'آنلاین' : selected.meeting_type === 'in_person' ? 'حضوری' : 'ترکیبی'}</div>
{selected.location && <div><strong>مکان:</strong> {selected.location}</div>}
{selected.meeting_link && <div><strong>لینک:</strong> <a href={selected.meeting_link} target="_blank" rel="noreferrer">{selected.meeting_link}</a></div>}
</div>
</div>
{selected.agenda && <div className="card" style={{ padding: '1rem', marginBottom: '1rem' }}><h4 style={{ fontSize: '0.875rem', fontWeight: 600, marginBottom: '0.5rem' }}>دستور جلسه</h4><p style={{ fontSize: '0.875rem', color: 'var(--gray-600)' }}>{selected.agenda}</p></div>}
{selected.notes && <div className="card" style={{ padding: '1rem', marginBottom: '1rem' }}><h4 style={{ fontSize: '0.875rem', fontWeight: 600, marginBottom: '0.5rem' }}>یادداشتها</h4><p style={{ fontSize: '0.875rem', color: 'var(--gray-600)' }}>{selected.notes}</p></div>}
{selected.decisions && <div className="card" style={{ padding: '1rem', marginBottom: '1rem' }}><h4 style={{ fontSize: '0.875rem', fontWeight: 600, marginBottom: '0.5rem' }}>تصمیمات</h4><p style={{ fontSize: '0.875rem', color: 'var(--gray-600)' }}>{selected.decisions}</p></div>}
<h4 style={{ fontSize: '0.9375rem', fontWeight: 600, marginBottom: '0.75rem', marginTop: '1rem' }}>شرکتکنندگان ({selected.participants?.length || 0})</h4>
<div style={{ marginBottom: '1rem' }}>
{(selected.participants || []).map(p => (
<div key={p.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '0.375rem 0', borderBottom: '1px solid var(--gray-50)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<div className="avatar avatar-sm" style={{ width: 24, height: 24, fontSize: '0.625rem' }}>{p.name?.[0] || '?'}</div>
<span style={{ fontSize: '0.875rem' }}>{p.name}</span>
</div>
<button className="btn btn-sm btn-danger" style={{ padding: '0.125rem 0.375rem' }} onClick={() => removeParticipant(p.id)}><X size={14} /></button>
</div>
))}
</div>
<div style={{ marginBottom: '1rem', display: 'flex', gap: '0.5rem' }}>
<select className="form-select" id="add-participant" style={{ flex: 1 }}
onChange={e => { if (e.target.value) { addParticipant(e.target.value); e.target.value = ''; } }}>
<option value="">افزودن شرکتکننده...</option>
{users.filter(u => !(selected.participants || []).find(p => p.id === u.id)).map(u => (
<option key={u.id} value={u.id}>{u.name}</option>
))}
</select>
</div>
<h4 style={{ fontSize: '0.9375rem', fontWeight: 600, marginBottom: '0.75rem', marginTop: '1rem' }}>ایتمهای اقدام</h4>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 180px auto', gap: '0.5rem', marginBottom: '1rem', alignItems: 'start' }}>
<input className="form-input" value={actionForm.title} onChange={e => setActionForm({...actionForm, title: e.target.value})} placeholder="عنوان..." />
<PersianDateInput value={actionForm.due_date} onChange={value => setActionForm({...actionForm, due_date: value})} placeholder="ددلاین شمسی" />
<button className="btn btn-primary btn-sm" onClick={addActionItem}>افزودن</button>
</div>
{(selected.action_items || []).length === 0 ? (
<div style={{ textAlign: 'center', padding: '1rem', color: 'var(--gray-400)', fontSize: '0.875rem' }}>ایتم اقدامی وجود ندارد</div>
) : (
(selected.action_items || []).map(item => (
<div key={item.id} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.5rem 0', borderBottom: '1px solid var(--gray-50)' }}>
<input type="checkbox" checked={!!item.is_completed} onChange={() => toggleActionItem(item)} style={{ width: 18, height: 18 }} />
<div style={{ flex: 1, fontSize: '0.875rem', textDecoration: item.is_completed ? 'line-through' : 'none', color: item.is_completed ? 'var(--gray-400)' : 'var(--gray-700)' }}>{item.title}</div>
<span style={{ fontSize: '0.75rem', color: 'var(--gray-400)' }}>{item.assignee?.name || ''}</span>
<button className="btn btn-sm btn-success" style={{ padding: '0.125rem 0.375rem' }} onClick={() => convertToTask(item)} title="تبدیل به تسک"><ArrowLeft size={14} /></button>
</div>
))
)}
</div>
</div>
</>
)}
</div>
);
}