-
- Overview
- Documents
- Tasks
- Members
-
+
+
+ Overview
+ Documents
+ Tasks
+ Members
+
-
+
+
+
+
+
diff --git a/src/main.ts b/src/main.ts
index cc76f5f..b46e4e9 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -3,7 +3,7 @@ import { createPinia } from 'pinia'
import PrimeVue from 'primevue/config'
import ToastService from 'primevue/toastservice'
import ConfirmationService from 'primevue/confirmationservice'
-import Aura from '@primevue/themes/aura'
+import KrakenPreset from './theme'
import 'primeicons/primeicons.css'
@@ -17,7 +17,7 @@ app.use(createPinia())
app.use(router)
app.use(PrimeVue, {
theme: {
- preset: Aura,
+ preset: KrakenPreset,
options: { darkModeSelector: '.app-dark' },
},
})
diff --git a/src/router/index.ts b/src/router/index.ts
index aba299c..ed273ca 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -5,6 +5,7 @@ declare module 'vue-router' {
interface RouteMeta {
public?: boolean
screenKey?: string
+ title?: string
}
}
@@ -22,10 +23,9 @@ const router = createRouter({
component: () => import('../layouts/MainLayout.vue'),
children: [
{ path: '', redirect: '/projects' },
- { path: 'dashboard', name: 'dashboard', component: () => import('../views/DashboardView.vue'), meta: { screenKey: 'dashboard' } },
- { path: 'projects', name: 'projects', component: () => import('../views/projects/ProjectsListView.vue'), meta: { screenKey: 'projects' } },
- { path: 'accounts', name: 'accounts', component: () => import('../views/AccountsView.vue'), meta: { screenKey: 'accounts' } },
- { path: 'roles', name: 'roles', component: () => import('../views/RolesView.vue'), meta: { screenKey: 'roles' } },
+ { path: 'dashboard', name: 'dashboard', component: () => import('../views/dashboard/DashboardView.vue'), meta: { screenKey: 'dashboard', title: 'Dashboard' } },
+ { path: 'projects', name: 'projects', component: () => import('../views/projects/ProjectsListView.vue'), meta: { screenKey: 'projects', title: 'Projects' } },
+ { path: 'users', name: 'users', component: () => import('../views/users/UsersView.vue'), meta: { screenKey: 'users', title: 'Users' } },
{
path: 'projects/:id',
component: () => import('../layouts/ProjectLayout.vue'),
@@ -38,7 +38,8 @@ const router = createRouter({
{ path: 'members', name: 'members', component: () => import('../views/projects/MembersView.vue') },
],
},
- { path: 'settings', name: 'settings', component: () => import('../views/SettingsView.vue') },
+ { path: 'settings', name: 'settings', component: () => import('../views/settings/SettingsView.vue'), meta: { title: 'Settings' } },
+ { path: 'profile', name: 'profile', component: () => import('../views/profile/ProfileView.vue'), meta: { title: 'Profile' } },
],
},
{ path: '/:pathMatch(.*)*', redirect: '/projects' },
@@ -51,13 +52,15 @@ router.beforeEach(async (to) => {
return { name: 'login', query: { redirect: to.fullPath } }
}
if (to.name === 'login' && auth.isAuthenticated) {
- return { path: '/projects' }
+ return { path: '/dashboard' }
}
if (auth.isAuthenticated) {
await auth.ensureMenu()
}
if (to.meta.screenKey && !auth.canView(to.meta.screenKey)) {
- return { path: '/projects' }
+ if (to.path !== '/dashboard') {
+ return { path: '/dashboard' }
+ }
}
})
diff --git a/src/services/backend.ts b/src/services/backend.ts
index e5d1bb8..1fbe812 100644
--- a/src/services/backend.ts
+++ b/src/services/backend.ts
@@ -1,5 +1,19 @@
import { api } from './api'
-import type { LoginResponse, Project, ProjectMember, MemberRole, Account, MenuItem, Role, SaveRoleRequest } from '../types'
+import type {
+ LoginResponse,
+ Project,
+ ProjectMember,
+ MemberRole,
+ UserListItem,
+ MenuItem,
+ Role,
+ SaveRoleRequest,
+ MasterDataItem,
+ SaveMasterDataRequest,
+ PagedResult,
+ UserRoleDetail,
+ User,
+} from '../types'
export async function login(username: string, password: string): Promise
{
const { data } = await api.post('/api/auth/login', { username, password })
@@ -7,36 +21,65 @@ export async function login(username: string, password: string): Promise {
- const { data } = await api.get('/api/menu')
+ const { data } = await api.get('/api/settings/permission/menu')
return data
}
-export async function getRoles(): Promise {
- const { data } = await api.get('/api/roles')
+export async function getRoles(page = 1, pageSize = 20): Promise> {
+ const { data } = await api.get>('/api/settings/permission', { params: { page, pageSize } })
return data
}
export async function getRole(id: string): Promise {
- const { data } = await api.get(`/api/roles/${id}`)
+ const { data } = await api.get(`/api/settings/permission/${id}`)
return data
}
export async function createRole(payload: SaveRoleRequest): Promise {
- const { data } = await api.post('/api/roles', payload)
+ const { data } = await api.post('/api/settings/permission', payload)
return data
}
export async function updateRole(id: string, payload: SaveRoleRequest): Promise {
- const { data } = await api.put(`/api/roles/${id}`, payload)
+ const { data } = await api.put(`/api/settings/permission/${id}`, payload)
return data
}
export async function deleteRole(id: string): Promise {
- await api.delete(`/api/roles/${id}`)
+ await api.delete(`/api/settings/permission/${id}`)
}
-export async function getProjects(): Promise {
- const { data } = await api.get('/api/projects')
+export async function getMasterDataList(group?: string, page = 1, pageSize = 20): Promise> {
+ const { data } = await api.get>('/api/masterdata', { params: { group, page, pageSize } })
+ return data
+}
+
+export async function getMasterDataByGroup(group: string): Promise {
+ const { data } = await api.get(`/api/masterdata/groups/${group}`)
+ return data
+}
+
+export async function getMasterDataOptions(group: string): Promise<{ label: string; value: string }[]> {
+ const items = await getMasterDataByGroup(group)
+ return items.map((i) => ({ label: i.label, value: i.value }))
+}
+
+export async function createMasterData(payload: SaveMasterDataRequest): Promise {
+ const { data } = await api.post('/api/masterdata', payload)
+ return data
+}
+
+export async function updateMasterData(id: string, payload: SaveMasterDataRequest): Promise {
+ const { data } = await api.put(`/api/masterdata/${id}`, payload)
+ return data
+}
+
+export async function deleteMasterData(id: string): Promise {
+ await api.delete(`/api/masterdata/${id}`)
+}
+
+export async function getProjects(page = 1, pageSize = 20): Promise> {
+ const { data } = await api.get>('/api/projects', { params: { page, pageSize } })
return data
}
@@ -72,8 +115,8 @@ export async function deleteProject(id: string): Promise {
await api.delete(`/api/projects/${id}`)
}
-export async function getMembers(projectId: string): Promise {
- const { data } = await api.get(`/api/projects/${projectId}/members`)
+export async function getMembers(projectId: string, page = 1, pageSize = 20): Promise> {
+ const { data } = await api.get>(`/api/projects/${projectId}/members`, { params: { page, pageSize } })
return data
}
@@ -90,38 +133,75 @@ export async function removeMember(projectId: string, userId: string): Promise {
+ const { data } = await api.put(`/api/projects/${projectId}/members/${userId}/document-permissions`, payload)
return data
}
-export async function getAccounts(q?: string): Promise {
- const { data } = await api.get('/api/accounts', { params: { q } })
+export async function getProfile(): Promise {
+ const { data } = await api.get('/api/auth/me')
return data
}
-export async function createAccount(payload: {
+export async function getUsers(q?: string): Promise {
+ const { data } = await api.get('/api/users/list', { params: { q } })
+ return data
+}
+
+export async function getUsersPaged(q?: string, page = 1, pageSize = 20): Promise> {
+ const { data } = await api.get>('/api/users', { params: { q, page, pageSize } })
+ return data
+}
+
+export async function createUser(payload: {
username: string
displayName: string
password: string
- roleId: string
-}): Promise {
- const { data } = await api.post('/api/accounts', payload)
+ roleId?: string
+}): Promise {
+ const { data } = await api.post('/api/users', payload)
return data
}
-export async function updateAccount(
+export async function updateUser(
id: string,
- payload: { displayName: string; roleId: string; isActive: boolean },
-): Promise {
- const { data } = await api.put(`/api/accounts/${id}`, payload)
+ payload: { displayName: string; roleId?: string; isActive: boolean },
+): Promise {
+ const { data } = await api.put(`/api/users/${id}`, payload)
return data
}
-export async function deleteAccount(id: string): Promise {
- await api.delete(`/api/accounts/${id}`)
+export async function deleteUser(id: string): Promise {
+ await api.delete(`/api/users/${id}`)
}
-export async function resetAccountPassword(id: string, newPassword: string): Promise {
- await api.post(`/api/accounts/${id}/reset-password`, { newPassword })
-}
\ No newline at end of file
+export async function resetUserPassword(id: string, newPassword: string): Promise {
+ await api.post(`/api/users/${id}/reset-password`, { newPassword })
+}
+
+export async function getUserRoles(userId: string): Promise {
+ const { data } = await api.get(`/api/users/${userId}/roles`)
+ return data
+}
+
+export async function assignUserRole(userId: string, roleId: string): Promise {
+ await api.post(`/api/users/${userId}/roles`, { roleId })
+}
+
+export async function unassignUserRole(userId: string, roleId: string): Promise {
+ await api.delete(`/api/users/${userId}/roles/${roleId}`)
+}
+
+export async function getUserPermissions(userId: string): Promise {
+ const { data } = await api.get(`/api/users/${userId}/permissions`)
+ return data
+}
diff --git a/src/services/modules.ts b/src/services/modules.ts
index fd127ca..472413c 100644
--- a/src/services/modules.ts
+++ b/src/services/modules.ts
@@ -1,5 +1,5 @@
import { api } from './api'
-import type { DocumentItem, DocumentNode, DocumentType, Task, TaskPriority, TaskStatus } from '../types'
+import type { DocumentItem, DocumentNode, DocumentType, PagedResult, Task, TaskPriority, TaskStatus } from '../types'
export async function getDocumentTree(projectId: string): Promise {
const { data } = await api.get(`/api/projects/${projectId}/documents`)
@@ -44,8 +44,10 @@ export async function searchDocuments(q: string): Promise {
export async function getTasks(
projectId: string,
filters?: { status?: string; priority?: string; assigneeId?: string },
-): Promise {
- const { data } = await api.get(`/api/projects/${projectId}/tasks`, { params: filters })
+ page = 1,
+ pageSize = 20,
+): Promise> {
+ const { data } = await api.get>(`/api/projects/${projectId}/tasks`, { params: { ...filters, page, pageSize } })
return data
}
@@ -91,4 +93,4 @@ export async function deleteTask(id: string): Promise {
export async function searchTasks(q: string): Promise {
const { data } = await api.get('/api/tasks/search', { params: { q } })
return data
-}
\ No newline at end of file
+}
diff --git a/src/stores/auth.ts b/src/stores/auth.ts
index 38c5ff3..c54d5a7 100644
--- a/src/stores/auth.ts
+++ b/src/stores/auth.ts
@@ -1,5 +1,5 @@
import type { MenuItem, User } from '../types'
-import { login as apiLogin, getMenu } from '../services/backend'
+import { login as apiLogin, getMenu, getProfile } from '../services/backend'
function loadUser(): User | null {
try {
@@ -41,6 +41,12 @@ export const useAuthStore = defineStore('auth', {
this.menu = await getMenu()
this.menuLoaded = true
},
+ async fetchProfile() {
+ if (this.token) {
+ this.user = await getProfile()
+ localStorage.setItem('mws_user', JSON.stringify(this.user))
+ }
+ },
async ensureMenu() {
if (!this.menuLoaded) await this.loadMenu()
},
diff --git a/src/style.css b/src/style.css
index 0cef4f8..681c3f4 100644
--- a/src/style.css
+++ b/src/style.css
@@ -2,29 +2,166 @@
@custom-variant dark (&:where(.app-dark, .app-dark *));
+@theme {
+ /* Display = heavy geometric grotesque (DESIGN.md substitute for ABC Ginto Nord) */
+ --font-sans: 'Inter', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ --font-display: 'Hanken Grotesk', 'Inter', Helvetica, Arial, sans-serif;
+
+ /* Blurple — brand primary, remaps every indigo-* utility */
+ --color-indigo-50: #eef0fe;
+ --color-indigo-100: #dfe3fd;
+ --color-indigo-200: #c3cafb;
+ --color-indigo-300: #9ba6f8;
+ --color-indigo-400: #7b88f5;
+ --color-indigo-500: #5865f2;
+ --color-indigo-600: #4551e0;
+ --color-indigo-700: #3742b8;
+ --color-indigo-800: #29328c;
+ --color-indigo-900: #1e2353;
+ --color-indigo-950: #0a0d3a;
+
+ /* Legacy purple-* aliases → Blurple, so untouched views follow the brand */
+ --color-purple-50: #eef0fe;
+ --color-purple-100: #dfe3fd;
+ --color-purple-200: #c3cafb;
+ --color-purple-300: #9ba6f8;
+ --color-purple-400: #7b88f5;
+ --color-purple-500: #5865f2;
+ --color-purple-600: #4551e0;
+ --color-purple-700: #3742b8;
+ --color-purple-800: #29328c;
+ --color-purple-900: #1e2353;
+ --color-purple-950: #0a0d3a;
+
+ /* Magenta — the playful counterweight (badges, folder marks, focus art) */
+ --color-fuchsia-400: #f26bcb;
+ --color-fuchsia-500: #ec48bd;
+ --color-fuchsia-600: #d32ba1;
+
+ /* Electric green — highest-intent only */
+ --color-green-50: #e8fdf1;
+ --color-green-100: #c7f9dd;
+ --color-green-200: #8df3bb;
+ --color-green-300: #56ef9c;
+ --color-green-400: #35ed7e;
+ --color-green-500: #1cc963;
+ --color-green-600: #14a151;
+ --color-green-700: #0f7c3f;
+ --color-green-800: #0b562d;
+ --color-green-900: #073a1f;
+ --color-green-950: #042313;
+
+ /* Cool indigo-tinted neutrals — remaps every slate-* utility */
+ --color-slate-50: #f6f7fb;
+ --color-slate-100: #eef0f7;
+ --color-slate-200: #dde0ed;
+ --color-slate-300: #c1c6dd;
+ --color-slate-400: #8e95b5;
+ --color-slate-500: #666d92;
+ --color-slate-600: #4a5177;
+ --color-slate-700: #2d3358;
+ --color-slate-800: #1e2353;
+ --color-slate-900: #141840;
+ --color-slate-950: #0a0d3a;
+
+ --shadow-subtle: rgba(69, 42, 124, 0.1) 0px 3px 34px;
+ --shadow-micro: rgba(20, 24, 64, 0.06) 0px 1px 3px;
+}
+
+:root {
+ --font-family: var(--font-sans);
+ --ink: #141840;
+ --ink-muted: #666d92;
+ --primary: #5865f2;
+ --magenta: #ec48bd;
+ --canvas: #f6f7fb;
+ --panel: #ffffff;
+ --hairline: #dde0ed;
+ --primary-soft: rgba(88, 101, 242, 0.12);
+}
+
+.app-dark {
+ --ink: #ffffff;
+ --ink-muted: #8e95b5;
+ --canvas: #0a0d3a;
+ --panel: #141840;
+ --hairline: #2d3358;
+ --primary-soft: rgba(123, 136, 245, 0.2);
+}
+
@layer base {
body {
- font-family: var(--font-family, Inter, 'Segoe UI', Roboto, Arial, sans-serif);
- background: #f5f7fa;
- color: #1e293b;
- }
-
- .app-dark body {
- background: #0f172a;
- color: #e2e8f0;
+ font-family: var(--font-family);
+ background: var(--canvas);
+ color: var(--ink);
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
}
#app {
- height: 100vh;
+ height: 100dvh;
}
a {
- color: #3b82f6;
+ color: var(--primary);
text-decoration: none;
}
+ :focus-visible {
+ outline: 2px solid var(--primary);
+ outline-offset: 2px;
+ }
+
+ /* CKEditor: fill its container, no fixed height */
+ .ck.ck-editor {
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ height: 100%;
+ }
+
+ .ck.ck-editor__main {
+ display: flex;
+ min-height: 0;
+ flex: 1;
+ overflow: auto;
+ }
+
.ck-editor__editable {
- min-height: 400px;
+ min-height: 100%;
+ }
+
+ .ck.ck-editor__editable_inline {
+ width: 100% !important;
+ padding: 20px !important;
+ border: 1px solid var(--hairline) !important;
+ border-radius: 0 0 12px 12px !important;
+ }
+
+ .ck.ck-toolbar {
+ border-radius: 0 !important;
+ }
+
+ .ck.ck-editor__editable_inline.ck-focused {
+ box-shadow: none !important;
+ }
+
+ /* No max-width here: CKEditor puts this class on the live editable too — capping
+ it would re-center and narrow the edit area. Read-view width lives on .document-content. */
+ .ck-content {
+ font-size: 16px;
+ line-height: 1.65;
+ }
+
+ .ck-content h2 {
+ font-family: var(--font-display);
+ font-weight: 700;
+ }
+
+ .ck-content h3,
+ .ck-content h4 {
+ font-family: var(--font-display);
+ font-weight: 600;
}
.ck-content ol,
@@ -37,26 +174,26 @@
}
.app-dark .ck.ck-editor {
- --ck-color-base-background: #0f172a;
- --ck-color-base-border: #334155;
- --ck-color-base-text: #e2e8f0;
- --ck-color-text: #e2e8f0;
- --ck-color-focus-border: #3b82f6;
- --ck-color-toolbar-background: #1e293b;
- --ck-color-toolbar-border: #334155;
- --ck-color-dropdown-panel-background: #1e293b;
- --ck-color-panel-background: #1e293b;
- --ck-color-panel-border: #334155;
- --ck-color-button-default-hover-background: #334155;
- --ck-color-button-default-active-background: #334155;
- --ck-color-button-on-background: #334155;
- --ck-color-input-background: #0f172a;
- --ck-color-input-border: #334155;
- --ck-color-input-text: #e2e8f0;
- --ck-color-tooltip-background: #334155;
- --ck-color-tooltip-text: #e2e8f0;
- --ck-color-table-border: #475569;
- --ck-color-link-default: #60a5fa;
+ --ck-color-base-background: #0a0d3a;
+ --ck-color-base-border: #2d3358;
+ --ck-color-base-text: #ffffff;
+ --ck-color-text: #ffffff;
+ --ck-color-focus-border: #5865f2;
+ --ck-color-toolbar-background: #141840;
+ --ck-color-toolbar-border: #2d3358;
+ --ck-color-dropdown-panel-background: #141840;
+ --ck-color-panel-background: #141840;
+ --ck-color-panel-border: #2d3358;
+ --ck-color-button-default-hover-background: #2d3358;
+ --ck-color-button-default-active-background: #2d3358;
+ --ck-color-button-on-background: #2d3358;
+ --ck-color-input-background: #0a0d3a;
+ --ck-color-input-border: #2d3358;
+ --ck-color-input-text: #ffffff;
+ --ck-color-tooltip-background: #2d3358;
+ --ck-color-tooltip-text: #ffffff;
+ --ck-color-table-border: #4a5177;
+ --ck-color-link-default: #9ba6f8;
}
}
@@ -66,6 +203,58 @@
}
.field > label {
- @apply mb-1.5 block text-sm font-medium;
+ @apply mb-1.5 block text-[13px] font-semibold uppercase tracking-[0.06em];
+ color: var(--ink-muted);
+ }
+
+ .page-title {
+ font-family: var(--font-display);
+ font-size: clamp(28px, 3vw, 38px);
+ font-weight: 800;
+ line-height: 1.05;
+ letter-spacing: -0.02em;
+ color: var(--ink);
+ }
+
+ .page-subtitle {
+ font-size: 15px;
+ line-height: 1.5;
+ color: var(--ink-muted);
+ }
+
+ .muted-note {
+ font-size: 13px;
+ line-height: 1.45;
+ color: var(--ink-muted);
+ }
+
+ /* Eyebrow: small caps label above a title */
+ .eyebrow {
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.18em;
+ text-transform: uppercase;
+ color: var(--ink-muted);
+ }
+
+ /* Surface card used outside PrimeVue */
+ .panel {
+ background: var(--panel);
+ border: 1px solid var(--hairline);
+ border-radius: 16px;
+ }
+
+ .search-input.p-inputtext,
+ .search-input.p-select {
+ border-radius: 12px !important;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ transition-duration: 0.01ms !important;
}
}
diff --git a/src/theme.ts b/src/theme.ts
new file mode 100644
index 0000000..f724652
--- /dev/null
+++ b/src/theme.ts
@@ -0,0 +1,219 @@
+import { definePreset } from '@primevue/themes'
+import Aura from '@primevue/themes/aura'
+
+// Blurple — DESIGN.md brand primary (#5865f2)
+const blurple = {
+ 50: '#eef0fe',
+ 100: '#dfe3fd',
+ 200: '#c3cafb',
+ 300: '#9ba6f8',
+ 400: '#7b88f5',
+ 500: '#5865f2',
+ 600: '#4551e0',
+ 700: '#3742b8',
+ 800: '#29328c',
+ 900: '#1e2353',
+ 950: '#0a0d3a',
+}
+
+const green = {
+ 50: '#e8fdf1',
+ 100: '#c7f9dd',
+ 200: '#8df3bb',
+ 300: '#56ef9c',
+ 400: '#35ed7e',
+ 500: '#1cc963',
+ 600: '#14a151',
+ 700: '#0f7c3f',
+ 800: '#0b562d',
+ 900: '#073a1f',
+ 950: '#042313',
+}
+
+const lightSurface = {
+ 0: '#ffffff',
+ 50: '#f6f7fb',
+ 100: '#eef0f7',
+ 200: '#dde0ed',
+ 300: '#c1c6dd',
+ 400: '#8e95b5',
+ 500: '#666d92',
+ 600: '#4a5177',
+ 700: '#2d3358',
+ 800: '#1e2353',
+ 900: '#141840',
+ 950: '#0a0d3a',
+}
+
+const darkSurface = {
+ 0: '#ffffff',
+ 50: '#f6f7fb',
+ 100: '#dde0ed',
+ 200: '#c1c6dd',
+ 300: '#8e95b5',
+ 400: '#666d92',
+ 500: '#4a5177',
+ 600: '#2d3358',
+ 700: '#242a4d',
+ 800: '#1e2353',
+ 900: '#141840',
+ 950: '#0a0d3a',
+}
+
+export default definePreset(Aura, {
+ primitive: {
+ borderRadius: {
+ none: '0',
+ xs: '6px',
+ sm: '10px',
+ md: '12px',
+ lg: '16px',
+ xl: '20px',
+ },
+ blurple,
+ green,
+ },
+ semantic: {
+ primary: blurple,
+ success: green,
+ focusRing: {
+ width: '2px',
+ style: 'solid',
+ color: '{primary.500}',
+ offset: '2px',
+ shadow: 'none',
+ },
+ colorScheme: {
+ light: {
+ surface: lightSurface,
+ primary: {
+ color: '{primary.500}',
+ contrastColor: '#ffffff',
+ hoverColor: '{primary.600}',
+ activeColor: '{primary.700}',
+ },
+ text: {
+ color: '{surface.900}',
+ hoverColor: '{surface.950}',
+ mutedColor: '{surface.500}',
+ hoverMutedColor: '{surface.600}',
+ },
+ formField: {
+ borderColor: '{surface.200}',
+ hoverBorderColor: '{surface.300}',
+ focusBorderColor: '{primary.500}',
+ color: '{surface.900}',
+ },
+ content: {
+ background: '#ffffff',
+ borderColor: '{surface.200}',
+ },
+ overlay: {
+ modal: {
+ background: '#ffffff',
+ borderColor: '{surface.200}',
+ color: '{text.color}',
+ shadow: 'rgba(69, 42, 124, 0.14) 0px 12px 48px',
+ },
+ },
+ },
+ dark: {
+ surface: darkSurface,
+ text: {
+ color: '#ffffff',
+ hoverColor: '#ffffff',
+ mutedColor: '{surface.300}',
+ hoverMutedColor: '{surface.200}',
+ },
+ primary: {
+ color: '{primary.400}',
+ contrastColor: '#0a0d3a',
+ hoverColor: '{primary.300}',
+ activeColor: '{primary.200}',
+ },
+ content: {
+ background: '#141840',
+ borderColor: '{surface.600}',
+ },
+ overlay: {
+ modal: {
+ background: '#141840',
+ borderColor: '{surface.600}',
+ color: '#ffffff',
+ shadow: 'rgba(0, 0, 0, 0.5) 0px 12px 48px',
+ },
+ },
+ },
+ },
+ },
+ components: {
+ button: {
+ root: {
+ borderRadius: '12px',
+ paddingX: '1.05rem',
+ label: { fontWeight: '600' },
+ },
+ },
+ card: {
+ root: {
+ background: '{content.background}',
+ borderRadius: '16px',
+ border: '1px solid {content.border.color}',
+ shadow: 'none',
+ },
+ body: { gap: '0.75rem' },
+ title: { fontSize: '0.95rem', fontWeight: '700' },
+ },
+ dialog: {
+ root: { borderRadius: '16px' },
+ header: { padding: '1.25rem 1.5rem 0.75rem' },
+ content: { padding: '0 1.5rem 0.5rem' },
+ footer: { padding: '0.75rem 1.5rem 1.25rem' },
+ title: { fontWeight: '700', fontSize: '1.05rem' },
+ },
+ datatable: {
+ header: {
+ background: '{content.background}',
+ borderColor: '{content.border.color}',
+ color: '{text.muted.color}',
+ },
+ headerCell: {
+ background: 'transparent',
+ borderColor: '{content.border.color}',
+ color: '{text.muted.color}',
+ fontWeight: '600',
+ padding: '0.7rem 1rem',
+ },
+ bodyCell: { padding: '0.8rem 1rem' },
+ row: { borderColor: '{content.border.color}' },
+ },
+ treetable: {
+ headerCell: {
+ background: 'transparent',
+ borderColor: '{content.border.color}',
+ color: '{text.muted.color}',
+ fontWeight: '600',
+ },
+ bodyCell: { padding: '0.55rem 0.85rem' },
+ row: { borderColor: '{content.border.color}' },
+ },
+ tag: {
+ root: { borderRadius: '999px', fontWeight: '600', padding: '0.2rem 0.6rem' },
+ },
+ inputtext: {
+ root: { borderRadius: '12px' },
+ },
+ select: {
+ root: { borderRadius: '12px' },
+ },
+ textarea: {
+ root: { borderRadius: '12px' },
+ },
+ toast: {
+ root: { borderRadius: '16px' },
+ },
+ menu: {
+ root: { borderRadius: '14px' },
+ },
+ },
+})
diff --git a/src/types/index.ts b/src/types/index.ts
index 87ea154..3a105bf 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -1,3 +1,10 @@
+export interface PagedResult {
+ items: T[]
+ totalCount: number
+ page: number
+ pageSize: number
+}
+
export interface User {
id: string
username: string
@@ -6,7 +13,7 @@ export interface User {
roleName: string
}
-export interface Account {
+export interface UserListItem {
id: string
username: string
displayName: string
@@ -51,6 +58,23 @@ export interface SaveRoleRequest {
permissions: PermissionEntry[]
}
+export interface MasterDataItem {
+ id: string
+ group: string
+ label: string
+ value: string
+ sortOrder: number
+ isActive: boolean
+}
+
+export interface SaveMasterDataRequest {
+ group: string
+ label: string
+ value: string
+ sortOrder: number
+ isActive: boolean
+}
+
export type ProjectStatus = 'Active' | 'Archived'
export type MemberRole = 'Owner' | 'Member'
export type DocumentType = 'Folder' | 'Document'
@@ -62,7 +86,11 @@ export interface Project {
name: string
description: string | null
status: ProjectStatus
+ createdBy: string
+ createdByName: string
createdAt: string
+ updatedBy: string | null
+ updatedByName: string | null
updatedAt: string
}
@@ -71,6 +99,10 @@ export interface ProjectMember {
username: string
displayName: string
role: MemberRole
+ canViewDocuments: boolean
+ canCreateDocuments: boolean
+ canEditDocuments: boolean
+ canDeleteDocuments: boolean
}
export interface ProjectOverview {
@@ -100,7 +132,10 @@ export interface DocumentNode {
parentId: string | null
title: string
type: DocumentType
+ createdAt: string
+ createdBy: string
updatedAt: string
+ updatedBy: string | null
children: DocumentNode[]
}
@@ -131,6 +166,14 @@ export interface Task {
updatedAt: string
}
+export interface UserRoleDetail {
+ userId: string
+ username: string
+ displayName: string
+ assignedRoles: Role[]
+ unassignedRoles: Role[]
+}
+
export interface TaskCounts {
[status: string]: number
-}
\ No newline at end of file
+}
diff --git a/src/views/DashboardView.vue b/src/views/DashboardView.vue
deleted file mode 100644
index 7c97ad4..0000000
--- a/src/views/DashboardView.vue
+++ /dev/null
@@ -1,100 +0,0 @@
-
-
-
-
Dashboard
-
- Welcome back, {{ auth.user?.displayName ?? auth.user?.username }}.
-
-
-
-
-
-
- Projects
- {{ projects.length }}
-
-
-
-
- Active
- {{ activeCount }}
-
-
-
-
- Archived
- {{ archivedCount }}
-
-
-
-
-
- Recent Projects
-
-
-
-
-
-
- {{ data.name }}
-
-
-
-
-
-
-
-
-
-
- {{ formatDate(data.updatedAt) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/views/RolesView.vue b/src/views/RolesView.vue
deleted file mode 100644
index e1c6c42..0000000
--- a/src/views/RolesView.vue
+++ /dev/null
@@ -1,164 +0,0 @@
-
-
-
-
Roles
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Name
-
-
-
-
Permissions
-
-
-
-
- Screen
- View
- Create
- Edit
- Delete
-
-
-
-
- {{ screenLabel(row.screen) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/views/SettingsView.vue b/src/views/SettingsView.vue
deleted file mode 100644
index 533cc7d..0000000
--- a/src/views/SettingsView.vue
+++ /dev/null
@@ -1,64 +0,0 @@
-
-
-
-
Settings
-
-
-
- Profile
-
-
-
-
-
{{ auth.user?.displayName }}
-
@{{ auth.user?.username }}
-
-
-
-
-
-
- Appearance
-
-
-
-
Dark mode
-
Switch between light and dark theme
-
-
-
-
-
-
-
- Account
-
-
-
-
-
-
-
-
diff --git a/src/views/auth/LoginView.vue b/src/views/auth/LoginView.vue
index 44ce809..d654c81 100644
--- a/src/views/auth/LoginView.vue
+++ b/src/views/auth/LoginView.vue
@@ -1,41 +1,63 @@
-
+
-
-
-
-
- MWS — My Workspace
+
+
+
+
+
+
+
+ Write documents, track work, and keep your team in sync — without switching tools.
+
+
+
+ Documents
+ Tasks
+ Members
+
+
+
+
+
+
+
My Workspace
+
Sign in
+
Use the account your workspace owner set up for you.
-
-
+
-
-
+
+
@@ -57,7 +79,7 @@ const error = ref('')
async function submit() {
error.value = ''
if (!username.value || !password.value) {
- error.value = 'Username and password are required'
+ error.value = 'Enter your username and password'
return
}
loading.value = true
@@ -72,3 +94,13 @@ async function submit() {
}
}
+
+
diff --git a/src/views/dashboard/DashboardView.vue b/src/views/dashboard/DashboardView.vue
new file mode 100644
index 0000000..ebc497f
--- /dev/null
+++ b/src/views/dashboard/DashboardView.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
Recent projects
+
+
+
+
+
+
No projects yet
+
Create a project to start collecting documents and tasks.
+
+
+
+
+
+
+ {{ data.name }}
+
+
+
+
+
+
+
+
+
+
+ {{ formatDate(data.updatedAt) }}
+
+
+
+
+
+
+
+
diff --git a/src/views/documents/DocumentsView.vue b/src/views/documents/DocumentsView.vue
index 6f50d84..274a4b5 100644
--- a/src/views/documents/DocumentsView.vue
+++ b/src/views/documents/DocumentsView.vue
@@ -1,60 +1,36 @@
-
-
-
-
-
-
-
-
-
-
-
+
-
+
+
Title
@@ -95,7 +71,7 @@
+
\ No newline at end of file
diff --git a/src/views/profile/ProfileView.vue b/src/views/profile/ProfileView.vue
new file mode 100644
index 0000000..3910b83
--- /dev/null
+++ b/src/views/profile/ProfileView.vue
@@ -0,0 +1,94 @@
+
+
+
+
Profile
+
+
+
+
+
+
+
+ {{ auth.user?.displayName ?? auth.user?.username }}
+
+
@{{ auth.user?.username }}
+
+
+
+
+
+
+
+
+
diff --git a/src/views/projects/MembersView.vue b/src/views/projects/MembersView.vue
index a837cf6..634f57d 100644
--- a/src/views/projects/MembersView.vue
+++ b/src/views/projects/MembersView.vue
@@ -1,39 +1,82 @@
-
-
-
+
+
-
-
+
+
+
+ style="background: var(--primary); color: #fff" />
{{ data.displayName }}
-
@{{ data.username }}
+
@{{ data.username }}
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
-
+
+
@@ -53,14 +96,40 @@
+
+
+
+ Permissions for {{ permTarget?.displayName }}
+ @{{ permTarget?.username }}
+
+
+
+
+
+
{{ opt.label }}
+
{{ opt.hint }}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/settings/masterdata/MasterDataView.vue b/src/views/settings/masterdata/MasterDataView.vue
new file mode 100644
index 0000000..72f8c21
--- /dev/null
+++ b/src/views/settings/masterdata/MasterDataView.vue
@@ -0,0 +1,197 @@
+
+
+
+
Master Data
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Group
+
+
+
+ Label
+
+
+
+ Value
+
+
+
+ Sort order
+
+
+
+
+ Active
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/settings/permissions/PermissionsView.vue b/src/views/settings/permissions/PermissionsView.vue
new file mode 100644
index 0000000..f0536ec
--- /dev/null
+++ b/src/views/settings/permissions/PermissionsView.vue
@@ -0,0 +1,191 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No roles found
+
+
+
+
+
+
+
+ Select a role from the left list to view/edit permissions
+
+
+
+
Screen Permissions ({{ selectedRole?.name }})
+
+
+
+
+
+
+
+ Screen
+ View
+ Create
+ Edit
+ Delete
+
+
+
+
+ {{ screenLabel(item.screen) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/settings/roles/RolesView.vue b/src/views/settings/roles/RolesView.vue
new file mode 100644
index 0000000..8429654
--- /dev/null
+++ b/src/views/settings/roles/RolesView.vue
@@ -0,0 +1,214 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No users found
+
+
+
+
+
+
+
{{ u.displayName }}
+
@{{ u.username }}
+
+
+
+
+
+
+
+
+
+
+ Select a user from the left list to manage roles
+
+
+
+
+
+ Unassigned Roles
+
+
+
+ All available roles assigned
+
+
+
+
+
+
+
+ Assigned Roles
+
+
+
+ No roles assigned yet
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/tasks/TaskDetailDialog.vue b/src/views/tasks/TaskDetailDialog.vue
index 78196ce..ba21f93 100644
--- a/src/views/tasks/TaskDetailDialog.vue
+++ b/src/views/tasks/TaskDetailDialog.vue
@@ -66,6 +66,7 @@
+
+
diff --git a/src/views/tasks/TasksListView.vue b/src/views/tasks/TasksListView.vue
index bd9f13b..762a32d 100644
--- a/src/views/tasks/TasksListView.vue
+++ b/src/views/tasks/TasksListView.vue
@@ -1,5 +1,5 @@
-
+
-
-
+
+
+
{{ data.title }}
- {{ data.description }}
+ {{ data.description }}
@@ -70,10 +85,11 @@
- {{ formatDate(data.updatedAt) }}
+ {{ formatDate(data.updatedAt) }}
-
+
+
import TaskDetailDialog from './TaskDetailDialog.vue'
import { getTasks } from '../../services/modules'
-import { getMembers } from '../../services/backend'
+import { getMembers, getMasterDataOptions } from '../../services/backend'
import { errorMessage } from '../../services/api'
import { useAuthStore } from '../../stores/auth'
+import AppDataTable from '../../components/AppDataTable.vue'
import type { Task, TaskPriority, TaskStatus } from '../../types'
+import type { DataTablePageEvent } from 'primevue/datatable'
const route = useRoute()
const toast = useToast()
@@ -106,6 +124,11 @@ const tasks = ref([])
const loading = ref(false)
const members = ref<{ userId: string; displayName: string }[]>([])
+const page = ref(1)
+const pageSize = ref(20)
+const totalCount = ref(0)
+const first = computed(() => (page.value - 1) * pageSize.value)
+
const filterStatus = ref(null)
const filterPriority = ref(null)
const filterAssignee = ref(null)
@@ -115,36 +138,24 @@ const editingTask = ref(null)
const boardMode = computed(() => route.name === 'tasks-board')
-const statusOptions = [
- { label: 'Todo', value: 'Todo' },
- { label: 'In Progress', value: 'InProgress' },
- { label: 'Done', value: 'Done' },
- { label: 'Cancelled', value: 'Cancelled' },
-]
-
-const priorityOptions = [
- { label: 'Low', value: 'Low' },
- { label: 'Medium', value: 'Medium' },
- { label: 'High', value: 'High' },
-]
+const statusOptions = ref<{ label: string; value: string }[]>([])
+const priorityOptions = ref<{ label: string; value: string }[]>([])
const assigneeOptions = computed(() =>
members.value.map((m) => ({ label: m.displayName, value: m.userId })),
)
-const filteredTasks = computed(() => {
- return tasks.value.filter((t) => {
- if (filterStatus.value && t.status !== filterStatus.value) return false
- if (filterPriority.value && t.priority !== filterPriority.value) return false
- if (filterAssignee.value && t.assigneeId !== filterAssignee.value) return false
- return true
- })
-})
-
async function load() {
loading.value = true
try {
- tasks.value = await getTasks(projectId)
+ const filters = {
+ status: filterStatus.value || undefined,
+ priority: filterPriority.value || undefined,
+ assigneeId: filterAssignee.value || undefined,
+ }
+ const res = await getTasks(projectId, filters, page.value, pageSize.value)
+ tasks.value = res.items
+ totalCount.value = res.totalCount
} catch (e) {
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
} finally {
@@ -152,9 +163,16 @@ async function load() {
}
}
+function onPageChange(event: DataTablePageEvent) {
+ page.value = event.page + 1
+ pageSize.value = event.rows
+ void load()
+}
+
async function loadMembers() {
try {
- members.value = (await getMembers(projectId)).map((m) => ({
+ const res = await getMembers(projectId, 1, 100)
+ members.value = res.items.map((m) => ({
userId: m.userId,
displayName: m.displayName,
}))
@@ -163,9 +181,9 @@ async function loadMembers() {
}
}
-watch(filterStatus, load)
-watch(filterPriority, load)
-watch(filterAssignee, load)
+watch(filterStatus, () => { page.value = 1; void load() })
+watch(filterPriority, () => { page.value = 1; void load() })
+watch(filterAssignee, () => { page.value = 1; void load() })
function openCreate() {
editingTask.value = null
@@ -208,6 +226,12 @@ function formatDate(v: string) {
}
onMounted(async () => {
+ const [status, priority] = await Promise.all([
+ getMasterDataOptions('task_status'),
+ getMasterDataOptions('task_priority'),
+ ])
+ statusOptions.value = status
+ priorityOptions.value = priority
await loadMembers()
await load()
})
diff --git a/src/views/AccountsView.vue b/src/views/users/UsersView.vue
similarity index 54%
rename from src/views/AccountsView.vue
rename to src/views/users/UsersView.vue
index 9429733..01e632d 100644
--- a/src/views/AccountsView.vue
+++ b/src/views/users/UsersView.vue
@@ -1,48 +1,65 @@
-
-
-
Accounts
-
+
+
+
Users
+
-
-
+
+
+
+
+
-
-
+
+
+
+ style="background: var(--primary); color: #fff" />
{{ data.displayName }}
-
@{{ data.username }}
+
@{{ data.username }}
-
+
-
+
-
+
-
-
-
-
-
- {{ formatDate(data.createdAt) }}
+ {{ formatDate(data.createdAt) }}
-
-
+
+
-
+
+
-
+
Username
@@ -67,25 +85,17 @@
Password
-
- Role
-
-
-
+
Display name
-
- Role
-
-
Active
@@ -110,39 +120,46 @@