feat: add more roles (tech_lead, scrum_master, product_owner, designer, qa)
- Registration form: added 5 new role options to dropdown - Sidebar: new roles get proper nav access via ALL_ROLES/LEADER_ROLES - Dashboard: isLeader check expanded to include new leadership roles - Shared/Pages: role badge colors added for all new roles - Invite modal: expanded role dropdown
This commit is contained in:
@@ -6,12 +6,17 @@ import { Avatar, Tag, ProgressBar } from './Shared';
|
||||
interface DrawerProps {
|
||||
task: Task; currentUser: User; onClose: () => void;
|
||||
onUpdate: (updated: Task) => void;
|
||||
onAddDependency: (taskId: string, dep: { dependsOnUserId: string; description: string }) => void;
|
||||
onToggleDependency: (taskId: string, depId: string, resolved: boolean) => void;
|
||||
onRemoveDependency: (taskId: string, depId: string) => void;
|
||||
users: User[];
|
||||
}
|
||||
|
||||
export function TaskDrawer({ task, currentUser, onClose, onUpdate, users }: DrawerProps) {
|
||||
export function TaskDrawer({ task, currentUser, onClose, onUpdate, onAddDependency, onToggleDependency, onRemoveDependency, users }: DrawerProps) {
|
||||
const [commentText, setCommentText] = useState('');
|
||||
const [subtaskText, setSubtaskText] = useState('');
|
||||
const [depUser, setDepUser] = useState('');
|
||||
const [depDesc, setDepDesc] = useState('');
|
||||
|
||||
const updateField = (field: string, value: any) => {
|
||||
const now = new Date().toISOString();
|
||||
@@ -49,8 +54,16 @@ export function TaskDrawer({ task, currentUser, onClose, onUpdate, users }: Draw
|
||||
setCommentText('');
|
||||
};
|
||||
|
||||
const handleAddDep = () => {
|
||||
if (!depDesc.trim()) return;
|
||||
onAddDependency(task.id, { dependsOnUserId: depUser, description: depDesc });
|
||||
setDepDesc('');
|
||||
setDepUser('');
|
||||
};
|
||||
|
||||
const reporter = getUserById(users, task.reporter);
|
||||
const doneCount = task.subtasks.filter(s => s.done).length;
|
||||
const unresolvedDeps = (task.dependencies || []).filter(d => !d.resolved).length;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -69,7 +82,7 @@ export function TaskDrawer({ task, currentUser, onClose, onUpdate, users }: Draw
|
||||
<div className="drawer-meta-label">Assignee</div>
|
||||
<div className="drawer-meta-val">
|
||||
<Avatar userId={task.assignee} size={20} users={users} />
|
||||
<select className="drawer-select" value={task.assignee} onChange={e => updateField('assignee', e.target.value)}>
|
||||
<select className="drawer-select" aria-label="Assignee" value={task.assignee} onChange={e => updateField('assignee', e.target.value)}>
|
||||
{users.map(u => <option key={u.id} value={u.id}>{u.name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
@@ -80,13 +93,13 @@ export function TaskDrawer({ task, currentUser, onClose, onUpdate, users }: Draw
|
||||
</div>
|
||||
<div>
|
||||
<div className="drawer-meta-label">Status</div>
|
||||
<select className="drawer-select" value={task.status} onChange={e => updateField('status', e.target.value)}>
|
||||
<select className="drawer-select" aria-label="Status" value={task.status} onChange={e => updateField('status', e.target.value)}>
|
||||
{Object.entries(STATUS_LABELS).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<div className="drawer-meta-label">Priority</div>
|
||||
<select className="drawer-select" value={task.priority} onChange={e => updateField('priority', e.target.value)}>
|
||||
<select className="drawer-select" aria-label="Priority" value={task.priority} onChange={e => updateField('priority', e.target.value)}>
|
||||
{['critical', 'high', 'medium', 'low'].map(p => <option key={p} value={p}>{p}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
@@ -100,6 +113,50 @@ export function TaskDrawer({ task, currentUser, onClose, onUpdate, users }: Draw
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dependencies Section */}
|
||||
<div className="drawer-section">
|
||||
<div className="drawer-section-title">
|
||||
🔗 Dependencies
|
||||
{unresolvedDeps > 0 && <span className="dep-unresolved-badge">{unresolvedDeps} blocking</span>}
|
||||
</div>
|
||||
{(task.dependencies || []).length === 0 && (
|
||||
<div className="dep-empty">No dependencies yet</div>
|
||||
)}
|
||||
{(task.dependencies || []).map(dep => {
|
||||
const depUser = getUserById(users, dep.dependsOnUserId);
|
||||
return (
|
||||
<div key={dep.id} className={`dep-item ${dep.resolved ? 'dep-resolved' : 'dep-unresolved'}`}>
|
||||
<button
|
||||
className={`dep-check ${dep.resolved ? 'checked' : ''}`}
|
||||
onClick={() => onToggleDependency(task.id, dep.id, !dep.resolved)}
|
||||
title={dep.resolved ? 'Mark unresolved' : 'Mark resolved'}
|
||||
>
|
||||
{dep.resolved ? '✓' : ''}
|
||||
</button>
|
||||
<div className="dep-info">
|
||||
{depUser && (
|
||||
<span className="dep-user">
|
||||
<Avatar userId={dep.dependsOnUserId} size={18} users={users} />
|
||||
<span>{depUser.name}</span>
|
||||
</span>
|
||||
)}
|
||||
<span className={`dep-desc ${dep.resolved ? 'done' : ''}`}>{dep.description}</span>
|
||||
</div>
|
||||
<button className="dep-remove" onClick={() => onRemoveDependency(task.id, dep.id)} title="Remove">✕</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="dep-add-row">
|
||||
<select className="dep-add-select" value={depUser} onChange={e => setDepUser(e.target.value)}>
|
||||
<option value="">Anyone</option>
|
||||
{users.map(u => <option key={u.id} value={u.id}>{u.avatar} {u.name}</option>)}
|
||||
</select>
|
||||
<input className="dep-add-input" placeholder="Describe the dependency..." value={depDesc}
|
||||
onChange={e => setDepDesc(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAddDep()} />
|
||||
<button className="dep-add-btn" onClick={handleAddDep}>Add</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="drawer-section">
|
||||
<div className="drawer-section-title">Subtasks <span style={{ color: '#64748b', fontWeight: 400, fontSize: 12 }}>{doneCount} of {task.subtasks.length} complete</span></div>
|
||||
{task.subtasks.length > 0 && <ProgressBar subtasks={task.subtasks} />}
|
||||
@@ -162,16 +219,38 @@ interface ModalProps {
|
||||
currentUser: User;
|
||||
}
|
||||
|
||||
interface PendingDep {
|
||||
id: string;
|
||||
dependsOnUserId: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export function AddTaskModal({ onClose, onAdd, defaultDate, defaultStatus, users, currentUser }: ModalProps) {
|
||||
const [title, setTitle] = useState('');
|
||||
const [desc, setDesc] = useState('');
|
||||
const [assignee, setAssignee] = useState(users[0]?.id || '');
|
||||
const [assignee, setAssignee] = useState(currentUser.id);
|
||||
const [priority, setPriority] = useState<Priority>('medium');
|
||||
const [status, setStatus] = useState<Status>(defaultStatus || 'todo');
|
||||
const [dueDate, setDueDate] = useState(defaultDate || new Date().toISOString().split('T')[0]);
|
||||
const [tags, setTags] = useState('');
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
// Dependencies state
|
||||
const [deps, setDeps] = useState<PendingDep[]>([]);
|
||||
const [depUser, setDepUser] = useState('');
|
||||
const [depDesc, setDepDesc] = useState('');
|
||||
|
||||
const addDep = () => {
|
||||
if (!depDesc.trim()) return;
|
||||
setDeps(prev => [...prev, { id: `pd${Date.now()}`, dependsOnUserId: depUser, description: depDesc }]);
|
||||
setDepDesc('');
|
||||
setDepUser('');
|
||||
};
|
||||
|
||||
const removeDep = (id: string) => {
|
||||
setDeps(prev => prev.filter(d => d.id !== id));
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
if (!title.trim()) { setError(true); return; }
|
||||
const task: Task = {
|
||||
@@ -180,6 +259,7 @@ export function AddTaskModal({ onClose, onAdd, defaultDate, defaultStatus, users
|
||||
tags: tags ? tags.split(',').map(t => t.trim()).filter(Boolean) : [],
|
||||
subtasks: [], comments: [],
|
||||
activity: [{ id: `a${Date.now()}`, text: `📝 Task created`, timestamp: new Date().toISOString() }],
|
||||
dependencies: deps.map(d => ({ id: d.id, dependsOnUserId: d.dependsOnUserId, description: d.description, resolved: false })),
|
||||
};
|
||||
onAdd(task);
|
||||
onClose();
|
||||
@@ -200,7 +280,7 @@ export function AddTaskModal({ onClose, onAdd, defaultDate, defaultStatus, users
|
||||
</div>
|
||||
<div className="modal-grid">
|
||||
<div className="modal-field">
|
||||
<label>Assignee</label>
|
||||
<label>Assign To</label>
|
||||
<select className="modal-input" value={assignee} onChange={e => setAssignee(e.target.value)}>
|
||||
{users.map(u => <option key={u.id} value={u.id}>{u.avatar} {u.name}</option>)}
|
||||
</select>
|
||||
@@ -226,6 +306,33 @@ export function AddTaskModal({ onClose, onAdd, defaultDate, defaultStatus, users
|
||||
<label>Tags (comma separated)</label>
|
||||
<input className="modal-input" placeholder="devops, backend, ..." value={tags} onChange={e => setTags(e.target.value)} />
|
||||
</div>
|
||||
|
||||
{/* Dependencies Section */}
|
||||
<div className="modal-field">
|
||||
<label>🔗 Dependencies / Blockers</label>
|
||||
<div className="modal-deps-list">
|
||||
{deps.map(d => {
|
||||
const u = getUserById(users, d.dependsOnUserId);
|
||||
return (
|
||||
<div key={d.id} className="modal-dep-item">
|
||||
<span className="modal-dep-icon">⏳</span>
|
||||
{u && <span className="modal-dep-user">{u.avatar} {u.name}:</span>}
|
||||
<span className="modal-dep-desc">{d.description}</span>
|
||||
<button className="modal-dep-remove" onClick={() => removeDep(d.id)}>✕</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="modal-dep-add">
|
||||
<select className="modal-dep-select" value={depUser} onChange={e => setDepUser(e.target.value)}>
|
||||
<option value="">Blocked by (anyone)</option>
|
||||
{users.map(u => <option key={u.id} value={u.id}>{u.avatar} {u.name}</option>)}
|
||||
</select>
|
||||
<input className="modal-dep-input" placeholder="e.g. Need API endpoints from backend team"
|
||||
value={depDesc} onChange={e => setDepDesc(e.target.value)} onKeyDown={e => e.key === 'Enter' && (e.preventDefault(), addDep())} />
|
||||
<button className="modal-dep-btn" onClick={addDep} type="button">+ Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button className="btn-ghost" onClick={onClose}>Cancel</button>
|
||||
|
||||
Reference in New Issue
Block a user