@onahhas/hello-dev 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -11
- package/backend/Controllers/AccountController.cs +100 -0
- package/backend/Controllers/ActivityController.cs +44 -0
- package/backend/Controllers/AuthController.cs +127 -0
- package/backend/Controllers/LookupController.cs +46 -0
- package/backend/Controllers/TasksController.cs +652 -0
- package/backend/Controllers/UsersController.cs +181 -0
- package/backend/Data/AppDbContext.cs +93 -0
- package/backend/Data/DbSeeder.cs +122 -0
- package/backend/DevTasks.Api.csproj +13 -0
- package/backend/Dtos/ActivityDtos.cs +12 -0
- package/backend/Dtos/AuthDtos.cs +37 -0
- package/backend/Dtos/TaskDtos.cs +104 -0
- package/backend/Dtos/UserDtos.cs +29 -0
- package/backend/Enums/EditRequestStatus.cs +8 -0
- package/backend/Enums/TaskPriority.cs +8 -0
- package/backend/Enums/TaskState.cs +9 -0
- package/backend/Enums/TaskVisibility.cs +7 -0
- package/backend/Enums/UserRole.cs +7 -0
- package/backend/Extensions/ClaimsPrincipalExtensions.cs +23 -0
- package/backend/Models/ActivityLog.cs +12 -0
- package/backend/Models/AppUser.cs +17 -0
- package/backend/Models/TaskEditRequest.cs +31 -0
- package/backend/Models/TaskItem.cs +25 -0
- package/backend/Program.cs +138 -0
- package/backend/Properties/launchSettings.json +13 -0
- package/backend/Services/ActivityService.cs +28 -0
- package/backend/Services/PasswordHasher.cs +58 -0
- package/backend/Services/TokenService.cs +49 -0
- package/backend/appsettings.Development.json +10 -0
- package/backend/appsettings.json +24 -0
- package/frontend/index.html +12 -0
- package/frontend/package-lock.json +1769 -0
- package/frontend/package.json +23 -0
- package/frontend/src/App.tsx +40 -0
- package/frontend/src/api/http.ts +75 -0
- package/frontend/src/auth/AuthContext.tsx +101 -0
- package/frontend/src/components/EditRequestModal.tsx +139 -0
- package/frontend/src/components/EditRequestsPanel.tsx +94 -0
- package/frontend/src/components/Layout.tsx +76 -0
- package/frontend/src/components/PageHeader.tsx +21 -0
- package/frontend/src/components/ProtectedRoute.tsx +14 -0
- package/frontend/src/components/StatCard.tsx +15 -0
- package/frontend/src/components/TaskCard.tsx +83 -0
- package/frontend/src/components/TaskDetailsModal.tsx +45 -0
- package/frontend/src/components/TaskFilters.tsx +67 -0
- package/frontend/src/components/TaskModal.tsx +159 -0
- package/frontend/src/components/TaskTable.tsx +68 -0
- package/frontend/src/components/UserModal.tsx +124 -0
- package/frontend/src/main.tsx +19 -0
- package/frontend/src/pages/ActivityPage.tsx +37 -0
- package/frontend/src/pages/BoardPage.tsx +75 -0
- package/frontend/src/pages/CalendarPage.tsx +101 -0
- package/frontend/src/pages/DashboardPage.tsx +131 -0
- package/frontend/src/pages/LoginPage.tsx +69 -0
- package/frontend/src/pages/ProfilePage.tsx +111 -0
- package/frontend/src/pages/PublicTasksPage.tsx +99 -0
- package/frontend/src/pages/RegisterPage.tsx +80 -0
- package/frontend/src/pages/TasksPage.tsx +135 -0
- package/frontend/src/pages/UsersPage.tsx +86 -0
- package/frontend/src/styles.css +596 -0
- package/frontend/src/theme.tsx +49 -0
- package/frontend/src/types.ts +78 -0
- package/frontend/src/utils/date.ts +30 -0
- package/frontend/src/utils/labels.ts +3 -0
- package/frontend/src/vite-env.d.ts +1 -0
- package/frontend/tsconfig.json +21 -0
- package/frontend/vite.config.ts +15 -0
- package/package.json +22 -9
- package/index.js +0 -7
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { api, toQuery } from '../api/http';
|
|
3
|
+
import { EditRequestsPanel } from '../components/EditRequestsPanel';
|
|
4
|
+
import { PageHeader } from '../components/PageHeader';
|
|
5
|
+
import { TaskCard } from '../components/TaskCard';
|
|
6
|
+
import { TaskDetailsModal } from '../components/TaskDetailsModal';
|
|
7
|
+
import { TaskFilters, type TaskFiltersValue } from '../components/TaskFilters';
|
|
8
|
+
import { TaskModal } from '../components/TaskModal';
|
|
9
|
+
import { TaskTable } from '../components/TaskTable';
|
|
10
|
+
import type { TaskItem, User } from '../types';
|
|
11
|
+
|
|
12
|
+
const defaultFilters: TaskFiltersValue = {
|
|
13
|
+
q: '',
|
|
14
|
+
status: '',
|
|
15
|
+
priority: '',
|
|
16
|
+
visibility: ''
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function TasksPage() {
|
|
20
|
+
const [tasks, setTasks] = useState<TaskItem[]>([]);
|
|
21
|
+
const [users, setUsers] = useState<User[]>([]);
|
|
22
|
+
const [filters, setFilters] = useState(defaultFilters);
|
|
23
|
+
const [editingTask, setEditingTask] = useState<TaskItem | null>(null);
|
|
24
|
+
const [viewingTask, setViewingTask] = useState<TaskItem | null>(null);
|
|
25
|
+
const [modalOpen, setModalOpen] = useState(false);
|
|
26
|
+
const [viewMode, setViewMode] = useState<'table' | 'cards'>('table');
|
|
27
|
+
const [refreshKey, setRefreshKey] = useState(0);
|
|
28
|
+
const [error, setError] = useState('');
|
|
29
|
+
|
|
30
|
+
const query = useMemo(() => toQuery(filters), [filters]);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
void loadTasks();
|
|
34
|
+
}, [query]);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
void loadUsers();
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
async function loadTasks() {
|
|
41
|
+
try {
|
|
42
|
+
setTasks(await api<TaskItem[]>(`/api/tasks/my${query}`));
|
|
43
|
+
} catch (err) {
|
|
44
|
+
setError(err instanceof Error ? err.message : 'Could not load tasks.');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function loadUsers() {
|
|
49
|
+
try {
|
|
50
|
+
setUsers(await api<User[]>('/api/lookup/users'));
|
|
51
|
+
} catch {
|
|
52
|
+
setUsers([]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function deleteTask(task: TaskItem) {
|
|
57
|
+
if (!confirm(`Delete task #${task.id}: ${task.title}?`)) return;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
await api<void>(`/api/tasks/${task.id}`, { method: 'DELETE' });
|
|
61
|
+
await loadTasks();
|
|
62
|
+
} catch (err) {
|
|
63
|
+
alert(err instanceof Error ? err.message : 'Could not delete task.');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function openCreate() {
|
|
68
|
+
setEditingTask(null);
|
|
69
|
+
setModalOpen(true);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function openEdit(task: TaskItem) {
|
|
73
|
+
setEditingTask(task);
|
|
74
|
+
setModalOpen(true);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function reloadEverything() {
|
|
78
|
+
await loadTasks();
|
|
79
|
+
setRefreshKey((value) => value + 1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<section>
|
|
84
|
+
<PageHeader
|
|
85
|
+
eyebrow="Tasks"
|
|
86
|
+
title="My Tasks"
|
|
87
|
+
description="Table-first task management with optional card view and edit request approval."
|
|
88
|
+
actions={(
|
|
89
|
+
<div className="buttonGroup">
|
|
90
|
+
<button className="ghostBtn" type="button" onClick={() => setViewMode(viewMode === 'table' ? 'cards' : 'table')}>
|
|
91
|
+
{viewMode === 'table' ? 'Card view' : 'Table view'}
|
|
92
|
+
</button>
|
|
93
|
+
<button className="primaryBtn" type="button" onClick={openCreate}>Add task</button>
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
/>
|
|
97
|
+
|
|
98
|
+
<EditRequestsPanel refreshKey={refreshKey} onChanged={reloadEverything} />
|
|
99
|
+
|
|
100
|
+
<TaskFilters value={filters} onChange={setFilters} />
|
|
101
|
+
{error && <div className="errorBox">{error}</div>}
|
|
102
|
+
|
|
103
|
+
{viewMode === 'table' ? (
|
|
104
|
+
<>
|
|
105
|
+
<TaskTable tasks={tasks} onView={setViewingTask} onEdit={openEdit} onDelete={deleteTask} />
|
|
106
|
+
{tasks.length === 0 && <div className="emptyState">No tasks match your filters.</div>}
|
|
107
|
+
</>
|
|
108
|
+
) : (
|
|
109
|
+
<div className="taskList">
|
|
110
|
+
{tasks.map((task) => (
|
|
111
|
+
<TaskCard
|
|
112
|
+
key={task.id}
|
|
113
|
+
task={task}
|
|
114
|
+
onView={setViewingTask}
|
|
115
|
+
onEdit={openEdit}
|
|
116
|
+
onDelete={deleteTask}
|
|
117
|
+
/>
|
|
118
|
+
))}
|
|
119
|
+
{tasks.length === 0 && <div className="emptyState">No tasks match your filters.</div>}
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
|
|
123
|
+
{modalOpen && (
|
|
124
|
+
<TaskModal
|
|
125
|
+
task={editingTask}
|
|
126
|
+
users={users}
|
|
127
|
+
onClose={() => setModalOpen(false)}
|
|
128
|
+
onSaved={reloadEverything}
|
|
129
|
+
/>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
{viewingTask && <TaskDetailsModal task={viewingTask} onClose={() => setViewingTask(null)} />}
|
|
133
|
+
</section>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { api } from '../api/http';
|
|
3
|
+
import { PageHeader } from '../components/PageHeader';
|
|
4
|
+
import { UserModal } from '../components/UserModal';
|
|
5
|
+
import type { User } from '../types';
|
|
6
|
+
import { formatDate } from '../utils/date';
|
|
7
|
+
|
|
8
|
+
export function UsersPage() {
|
|
9
|
+
const [users, setUsers] = useState<User[]>([]);
|
|
10
|
+
const [editingUser, setEditingUser] = useState<User | null>(null);
|
|
11
|
+
const [modalOpen, setModalOpen] = useState(false);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
void load();
|
|
15
|
+
}, []);
|
|
16
|
+
|
|
17
|
+
async function load() {
|
|
18
|
+
setUsers(await api<User[]>('/api/users'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function deactivate(user: User) {
|
|
22
|
+
if (!confirm(`Deactivate ${user.fullName}?`)) return;
|
|
23
|
+
|
|
24
|
+
await api<void>(`/api/users/${user.id}`, { method: 'DELETE' });
|
|
25
|
+
await load();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function openCreate() {
|
|
29
|
+
setEditingUser(null);
|
|
30
|
+
setModalOpen(true);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function openEdit(user: User) {
|
|
34
|
+
setEditingUser(user);
|
|
35
|
+
setModalOpen(true);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<section>
|
|
40
|
+
<PageHeader
|
|
41
|
+
eyebrow="Admin"
|
|
42
|
+
title="Users"
|
|
43
|
+
description="Add users, change roles, and deactivate accounts."
|
|
44
|
+
actions={<button className="primaryBtn" type="button" onClick={openCreate}>Add user</button>}
|
|
45
|
+
/>
|
|
46
|
+
|
|
47
|
+
<section className="tableCard">
|
|
48
|
+
<table>
|
|
49
|
+
<thead>
|
|
50
|
+
<tr>
|
|
51
|
+
<th>Name</th>
|
|
52
|
+
<th>Email</th>
|
|
53
|
+
<th>Role</th>
|
|
54
|
+
<th>Status</th>
|
|
55
|
+
<th>Created</th>
|
|
56
|
+
<th></th>
|
|
57
|
+
</tr>
|
|
58
|
+
</thead>
|
|
59
|
+
<tbody>
|
|
60
|
+
{users.map((user) => (
|
|
61
|
+
<tr key={user.id}>
|
|
62
|
+
<td>{user.fullName}</td>
|
|
63
|
+
<td>{user.email}</td>
|
|
64
|
+
<td>{user.role}</td>
|
|
65
|
+
<td><span className={`statusDot ${user.isActive ? 'active' : 'inactive'}`}>{user.isActive ? 'Active' : 'Inactive'}</span></td>
|
|
66
|
+
<td>{formatDate(user.createdAt)}</td>
|
|
67
|
+
<td className="rowActions">
|
|
68
|
+
<button className="ghostBtn" onClick={() => openEdit(user)}>Edit</button>
|
|
69
|
+
{user.isActive && <button className="dangerBtn" onClick={() => void deactivate(user)}>Deactivate</button>}
|
|
70
|
+
</td>
|
|
71
|
+
</tr>
|
|
72
|
+
))}
|
|
73
|
+
</tbody>
|
|
74
|
+
</table>
|
|
75
|
+
</section>
|
|
76
|
+
|
|
77
|
+
{modalOpen && (
|
|
78
|
+
<UserModal
|
|
79
|
+
user={editingUser}
|
|
80
|
+
onClose={() => setModalOpen(false)}
|
|
81
|
+
onSaved={load}
|
|
82
|
+
/>
|
|
83
|
+
)}
|
|
84
|
+
</section>
|
|
85
|
+
);
|
|
86
|
+
}
|