diff --git a/package.json b/package.json index fc626967b..723141a0a 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@fortawesome/fontawesome-free": "^6.1.1", "@gtm-support/vue-gtm": "^2.0.0", "@vuestic/tailwind": "^0.1.3", + "@vueuse/core": "^10.6.1", "axios": "^1.6.1", "chart.js": "^3.8.0", "epic-spinners": "^2.0.0", diff --git a/src/components/sidebar/NavigationRoutes.ts b/src/components/sidebar/NavigationRoutes.ts index 0f3bc7276..2a142e9d3 100644 --- a/src/components/sidebar/NavigationRoutes.ts +++ b/src/components/sidebar/NavigationRoutes.ts @@ -229,6 +229,14 @@ export default { name: 'pricing-plans', displayName: 'menu.pricing-plans', }, + { + name: 'users', + displayName: 'menu.users', + }, + { + name: 'projects', + displayName: 'menu.projects', + }, { name: 'settings', displayName: 'menu.settings', diff --git a/src/data/pages/projects-db.json b/src/data/pages/projects-db.json new file mode 100644 index 000000000..48f0f1331 --- /dev/null +++ b/src/data/pages/projects-db.json @@ -0,0 +1,82 @@ +[ + { + "id": 0, + "project_name": "A Vuestic", + "project_owner": 13, + "team": [13, 5, 28, 14, 17, 28, 23, 11, 16, 19, 12, 28, 11], + "status": "in progress", + "creation_date": "20 Nov 2023" + }, + { + "id": 1, + "project_name": "Mood board", + "project_owner": 28, + "team": [28, 10, 12, 28, 14, 27, 5, 4, 8, 23, 19, 18, 24, 11, 18, 12, 28], + "status": "important", + "creation_date": "16 Oct 2023" + }, + { + "id": 2, + "project_name": "J Jenkins", + "project_owner": 3, + "team": [3, 21, 7, 19, 4, 4, 7, 24], + "status": "important", + "creation_date": "1 Oct 2023" + }, + { + "id": 3, + "project_name": "S Springfield media", + "project_owner": 17, + "team": [17, 25, 21, 9, 18, 12, 15, 0, 7, 2, 7], + "status": "important", + "creation_date": "19 Sept 2023" + }, + { + "id": 4, + "project_name": "G Galileo", + "project_owner": 7, + "team": [7, 1, 28, 19, 3], + "status": "completed", + "creation_date": "23 Sept 2023" + }, + { + "id": 5, + "project_name": "O Website Redesign", + "project_owner": 24, + "team": [24, 19, 1, 8, 9], + "status": "completed", + "creation_date": "9 Sept 2023" + }, + { + "id": 6, + "project_name": "U Website Redesign", + "project_owner": 15, + "team": [15, 16, 8, 6, 11, 21, 3, 20], + "status": "archived", + "creation_date": "17 Aug 2023" + }, + { + "id": 7, + "project_name": "L Complete Product Redesign", + "project_owner": 25, + "team": [25, 18, 24, 13, 5, 3, 4, 16, 25, 12, 18, 9, 22], + "status": "completed", + "creation_date": "11 Aug 2023" + }, + { + "id": 8, + "project_name": "Design Team Project", + "project_owner": 17, + "team": [17, 6, 21, 17, 7, 6, 14, 13, 27, 7, 20], + "status": "archived", + "creation_date": "9 Aug 2023" + }, + { + "id": 9, + "project_name": "R Regular logistics", + "project_owner": 3, + "team": [3, 26, 8, 15, 21, 23, 18, 11, 22, 6, 20, 9], + "status": "archived", + "creation_date": "2 Aug 2023" + } +] diff --git a/src/data/pages/projects.ts b/src/data/pages/projects.ts new file mode 100644 index 000000000..db31cb159 --- /dev/null +++ b/src/data/pages/projects.ts @@ -0,0 +1,82 @@ +import { sleep } from '../../services/utils' +import projectsDb from './projects-db.json' +import usersDb from './users-db.json' + +// Simulate API calls +export type Pagination = { + page: number + perPage: number + total: number +} + +export type Sorting = { + sortBy: keyof (typeof projectsDb)[number] | undefined + sortingOrder: 'asc' | 'desc' | null +} + +export const getProjects = async (options: Sorting & Pagination) => { + await sleep(1000) + + const projects = projectsDb.map((project) => ({ + ...project, + project_owner: usersDb.find((user) => user.id === project.project_owner)! as (typeof usersDb)[number], + team: usersDb.filter((user) => project.team.includes(user.id)) as (typeof usersDb)[number][], + })) + + if (options.sortBy && options.sortingOrder) { + projects.sort((a, b) => { + if (a[options.sortBy!] < b[options.sortBy!]) { + return options.sortingOrder === 'asc' ? -1 : 1 + } + if (a[options.sortBy!] > b[options.sortBy!]) { + return options.sortingOrder === 'asc' ? 1 : -1 + } + return 0 + }) + } + + return { + data: projects, + pagination: { + page: options.page, + perPage: options.perPage, + total: projectsDb.length, + }, + } +} + +export const addProject = async (project: Omit<(typeof projectsDb)[number], 'id' | 'creation_date'>) => { + await sleep(1000) + + const newProject = { + ...project, + id: projectsDb.length + 1, + creation_date: new Date().toLocaleDateString('gb', { day: 'numeric', month: 'short', year: 'numeric' }), + } + + projectsDb.push(newProject) + + return { + ...newProject, + project_owner: usersDb.find((user) => user.id === project.project_owner)! as (typeof usersDb)[number], + team: usersDb.filter((user) => project.team.includes(user.id)) as (typeof usersDb)[number][], + } +} + +export const updateProject = async (project: (typeof projectsDb)[number]) => { + await sleep(1000) + + const index = projectsDb.findIndex((p) => p.id === project.id) + projectsDb[index] = project + + return project +} + +export const removeProject = async (project: (typeof projectsDb)[number]) => { + await sleep(1000) + + const index = projectsDb.findIndex((p) => p.id === project.id) + projectsDb.splice(index, 1) + + return project +} diff --git a/src/data/pages/users-db.json b/src/data/pages/users-db.json new file mode 100644 index 000000000..1b31ed735 --- /dev/null +++ b/src/data/pages/users-db.json @@ -0,0 +1,292 @@ +[ + { + "id": 1, + "fullname": "Patrik Radkow", + "email": "magicpan@example.gg", + "username": "magicpan", + "role": "user", + "avatar": "", + "active": true, + "notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum." + }, + { + "id": 2, + "fullname": "Martin Hoff", + "email": "niceadmin@mail.com", + "username": "admin", + "role": "admin", + "avatar": "😍", + "active": true, + "notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum." + }, + { + "id": 3, + "fullname": "Liz Macintosh", + "email": "ebrown@gmail.com", + "username": "ebrown", + "role": "user", + "avatar": "https://randomuser.me/api/portraits/men/1.jpg", + "active": true, + "notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum." + }, + { + "id": 4, + "fullname": "M2", + "email": "mrm@gmail.com", + "username": "mrm", + "role": "owner", + "avatar": "", + "active": true, + "notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum." + }, + { + "id": 5, + "fullname": "Kevin Smith", + "email": "kevin@gmail.com", + "username": "kevin13", + "role": "user", + "avatar": "https://randomuser.me/api/portraits/men/2.jpg", + "active": true, + "notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum." + }, + { + "id": 6, + "fullname": "Martin Hoff", + "email": "martin@gmail.com", + "username": "martin3", + "role": "user", + "avatar": "https://randomuser.me/api/portraits/men/3.jpg", + "active": true, + "notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum." + }, + { + "id": 7, + "fullname": "John Doe", + "email": "john@mail.com", + "username": "john", + "role": "user", + "avatar": "", + "active": true, + "notes": "" + }, + { + "id": 8, + "fullname": "Maksim Nedo", + "email": "maksim@epic.com", + "username": "maksim", + "role": "admin", + "avatar": "https://avatars.githubusercontent.com/u/23530004?v=4", + "active": true, + "notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum." + }, + { + "id": 9, + "fullname": "Dmitry Kuzmenko", + "email": "dd@pp.com", + "username": "dd", + "role": "user", + "avatar": "", + "active": true, + "notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum." + }, + { + "id": 10, + "fullname": "Rayan Gosling", + "email": "rayan@u.ua", + "username": "rayan", + "role": "user", + "avatar": "", + "active": true, + "notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum." + }, + { + "id": 11, + "active": true, + "fullname": "Laura Smith", + "email": "laura@example.gg", + "username": "bbb", + "role": "user", + "avatar": "", + "notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum." + }, + { + "id": 12, + "active": true, + "fullname": "Ted Mosby", + "email": "tedmosby@mail.com", + "username": "gamer777", + "role": "user", + "avatar": "😭", + "notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum." + }, + { + "id": 13, + "active": true, + "fullname": "Forrest Schmidt Jr.", + "email": "Willard23@gmail.com", + "username": "Clementine72", + "role": "user", + "avatar": "https://randomuser.me/api/portraits/men/4.jpg", + "notes": "sed asperiores sed" + }, + { + "id": 14, + "active": true, + "fullname": "Emilio Bruen", + "email": "Amya51@hotmail.com", + "username": "Madalyn_Brekke55", + "role": "user", + "avatar": "https://randomuser.me/api/portraits/men/5.jpg", + "notes": "architecto amet deleniti" + }, + { + "id": 15, + "active": false, + "fullname": "Jenny Heathcote", + "email": "Granville_Lebsack38@yahoo.com", + "role": "user", + "username": "Vivienne98", + "avatar": "https://randomuser.me/api/portraits/men/6.jpg", + "notes": "provident ipsam recusandae" + }, + { + "id": 16, + "active": true, + "fullname": "Sonya Cummerata III", + "email": "Toni2@yahoo.com", + "role": "user", + "username": "Norwood79", + "avatar": "https://randomuser.me/api/portraits/men/7.jpg", + "notes": "aut quaerat totam" + }, + { + "id": 17, + "active": true, + "fullname": "Ruben Mitchell", + "email": "Lisette41@yahoo.com", + "role": "user", + "username": "Dariana_Schulist", + "avatar": "https://randomuser.me/api/portraits/men/8.jpg", + "notes": "minima harum ut" + }, + { + "id": 18, + "active": true, + "fullname": "Blake Hudson I", + "email": "Israel88@hotmail.com", + "role": "user", + "username": "Crystal.Brakus29", + "avatar": "https://randomuser.me/api/portraits/men/9.jpg", + "notes": "sint culpa voluptatem" + }, + { + "id": 19, + "active": true, + "fullname": "Alison Mueller", + "email": "Darien_Mayer@gmail.com", + "role": "user", + "username": "Cordie.Grant", + "avatar": "https://randomuser.me/api/portraits/men/10.jpg", + "notes": "officia autem aliquam" + }, + { + "id": 20, + "active": false, + "fullname": "Miss Angelina Jenkins", + "email": "Cristal.Sauer@yahoo.com", + "role": "user", + "username": "Peggie.Runolfsdottir", + "avatar": "https://randomuser.me/api/portraits/men/11.jpg", + "notes": "rerum rerum rerum" + }, + { + "id": 21, + "active": true, + "fullname": "Mack Boyle", + "email": "Shanny30@gmail.com", + "role": "user", + "username": "Phoebe67", + "avatar": "https://randomuser.me/api/portraits/men/12.jpg", + "notes": "voluptatibus et soluta" + }, + { + "id": 21, + "active": true, + "fullname": "Raymond Simonis", + "email": "Tressie.Bruen45@gmail.com", + "role": "user", + "username": "Percy37", + "avatar": "https://randomuser.me/api/portraits/men/13.jpg", + "notes": "aut id molestiae" + }, + { + "id": 22, + "active": true, + "fullname": "Janice Sporer", + "email": "Anastasia85@hotmail.com", + "role": "user", + "username": "Kali84", + "avatar": "https://randomuser.me/api/portraits/men/14.jpg", + "notes": "magnam eum aliquam" + }, + { + "id": 23, + "active": true, + "fullname": "Francis Schowalter", + "email": "Tess56@gmail.com", + "role": "user", + "username": "Robyn.Kris", + "avatar": "https://randomuser.me/api/portraits/men/0.jpg", + "notes": "similique architecto in" + }, + { + "id": 24, + "active": true, + "fullname": "Emilio Hoppe", + "email": "Bruce49@yahoo.com", + "role": "user", + "username": "Clemmie.Kutch", + "avatar": "https://randomuser.me/api/portraits/men/16.jpg", + "notes": "rerum quae dolorem" + }, + { + "id": 25, + "active": true, + "fullname": "Janice Harber", + "email": "Jude38@hotmail.com", + "role": "user", + "username": "Neal70", + "avatar": "https://randomuser.me/api/portraits/men/17.jpg", + "notes": "iure dolor provident" + }, + { + "id": 26, + "fullname": "Evelyn Morar", + "email": "Laverne.Roberts@hotmail.com", + "role": "user", + "username": "Neal_Thompson84", + "active": true, + "avatar": "https://randomuser.me/api/portraits/men/18.jpg", + "notes": "quae eos placeat" + }, + { + "id": 27, + "fullname": "Antoinette Schneider", + "email": "Ambrose_Stehr25@gmail.com", + "role": "user", + "username": "Esta.Hickle", + "active": true, + "avatar": "https://randomuser.me/api/portraits/men/19.jpg", + "notes": "qui cumque unde" + }, + { + "id": 28, + "fullname": "Ebony Daniel", + "email": "Nyah44@hotmail.com", + "role": "user", + "username": "Jade.Kuhlman90", + "active": true, + "avatar": "https://randomuser.me/api/portraits/men/20.jpg", + "notes": "exercitationem velit consectetur" + } +] diff --git a/src/data/pages/users.ts b/src/data/pages/users.ts new file mode 100644 index 000000000..5e6988540 --- /dev/null +++ b/src/data/pages/users.ts @@ -0,0 +1,93 @@ +import { sleep } from '../../services/utils' +import { User } from './../../pages/users/types' +import usersDb from './users-db.json' +import projectsDb from './projects-db.json' +import { Project } from '../../pages/projects/types' + +export const users = usersDb as User[] + +const getUserProjects = (userId: number | string) => { + return projectsDb + .filter((project) => project.team.includes(Number(userId))) + .map((project) => ({ + ...project, + project_owner: users.find((user) => user.id === project.project_owner)!, + team: project.team.map((userId) => users.find((user) => user.id === userId)!), + status: project.status as Project['status'], + })) +} + +// Simulate API calls + +export type Pagination = { + page: number + perPage: number + total: number +} + +export type Sorting = { + sortBy: keyof User | undefined + sortingOrder: 'asc' | 'desc' | null +} + +export type Filters = { + isActive: boolean + search: string +} + +export const getUsers = async (filters: Partial) => { + await sleep(1000) + const { isActive, search, sortBy, sortingOrder } = filters + let filteredUsers = users + + filteredUsers = users.filter((user) => user.active === isActive) + + if (search) { + filteredUsers = users.filter((user) => user.fullname.toLowerCase().includes(search.toLowerCase())) + } + + if (sortBy && sortingOrder) { + filteredUsers = filteredUsers.sort((a, b) => { + const first = a[sortBy] + const second = b[sortBy] + if (first > second) { + return sortingOrder === 'asc' ? 1 : -1 + } + if (first < second) { + return sortingOrder === 'asc' ? -1 : 1 + } + return 0 + }) + } + + const { page = 1, perPage = 10 } = filters || {} + return { + data: filteredUsers + .slice((page - 1) * perPage, page * perPage) + .map((user) => ({ ...user, projects: getUserProjects(user.id) })), + pagination: { + page, + perPage, + total: filteredUsers.length, + }, + } +} + +export const addUser = async (user: User) => { + await sleep(1000) + users.unshift(user) +} + +export const updateUser = async (user: User) => { + await sleep(1000) + const index = users.findIndex((u) => u.id === user.id) + users[index] = user +} + +export const removeUser = async (user: User) => { + await sleep(1000) + users.splice( + users.findIndex((u) => u.id === user.id), + 1, + ) +} diff --git a/src/i18n/locales/gb.json b/src/i18n/locales/gb.json index a07ef8e1a..36273720a 100644 --- a/src/i18n/locales/gb.json +++ b/src/i18n/locales/gb.json @@ -342,7 +342,9 @@ "line-maps": "Line Maps", "login-singup": "Login/Signup", "404-pages": "404 Pages", - "faq": "Faq" + "faq": "Faq", + "users": "Users", + "projects": "Projects" }, "messages": { "all": "See all messages", diff --git a/src/pages/payments/payment-system/PaymentSystem.vue b/src/pages/payments/payment-system/PaymentSystem.vue index e13b3f99d..ded27e96c 100644 --- a/src/pages/payments/payment-system/PaymentSystem.vue +++ b/src/pages/payments/payment-system/PaymentSystem.vue @@ -5,8 +5,7 @@ - + + diff --git a/src/pages/projects/components/ProjectStatusBadge.vue b/src/pages/projects/components/ProjectStatusBadge.vue new file mode 100644 index 000000000..e5dd35314 --- /dev/null +++ b/src/pages/projects/components/ProjectStatusBadge.vue @@ -0,0 +1,22 @@ + + + diff --git a/src/pages/projects/composables/useProjectStatusColor.ts b/src/pages/projects/composables/useProjectStatusColor.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/pages/projects/composables/useProjects.ts b/src/pages/projects/composables/useProjects.ts new file mode 100644 index 000000000..420e4c52d --- /dev/null +++ b/src/pages/projects/composables/useProjects.ts @@ -0,0 +1,84 @@ +import { Ref, ref, unref } from 'vue' +import { + getProjects, + addProject, + updateProject, + removeProject, + Sorting, + Pagination, +} from '../../../data/pages/projects' +import { Project } from '../types' +import { watchIgnorable } from '@vueuse/core' + +const makePaginationRef = () => ref({ page: 1, perPage: 10, total: 0 }) +const makeSortingRef = () => ref({ sortBy: 'project_name', sortingOrder: null }) + +export const useProjects = (options?: { sorting: Ref; pagination: Ref }) => { + const isLoading = ref(false) + const projects = ref([]) + + const { sorting = makeSortingRef(), pagination = makePaginationRef() } = options ?? {} + + const fetch = async () => { + isLoading.value = true + const { data, pagination: newPagination } = await getProjects({ + ...unref(sorting), + ...unref(pagination), + }) + projects.value = data as Project[] + + ignoreUpdates(() => { + pagination.value = newPagination + }) + + isLoading.value = false + } + + const { ignoreUpdates } = watchIgnorable([pagination, sorting], fetch, { deep: true }) + + fetch() + + return { + isLoading, + + projects, + + fetch, + + async add(project: Omit) { + isLoading.value = true + const createdProject = await addProject({ + ...project, + project_owner: project.project_owner.id, + team: project.team.map((user) => user.id), + }) + projects.value.unshift(createdProject as Project) + isLoading.value = false + }, + + async update(project: Project) { + isLoading.value = true + await updateProject({ + ...project, + project_owner: project.project_owner.id, + team: project.team.map((user) => user.id), + }) + projects.value = projects.value.map((u) => (u.id === project.id ? project : u)) + isLoading.value = false + }, + + async remove(project: Project) { + isLoading.value = true + await removeProject({ + ...project, + project_owner: project.project_owner.id, + team: project.team.map((user) => user.id), + }) + projects.value = projects.value.filter((u) => u.id !== project.id) + isLoading.value = false + }, + + pagination, + sorting, + } +} diff --git a/src/pages/projects/types.ts b/src/pages/projects/types.ts new file mode 100644 index 000000000..f12efd847 --- /dev/null +++ b/src/pages/projects/types.ts @@ -0,0 +1,14 @@ +import { User } from '../users/types' + +export type Project = { + id: number + project_name: string + project_owner: Omit + team: Omit[] + status: 'important' | 'completed' | 'archived' | 'in progress' + creation_date: string +} + +export type EmptyProject = Omit & { + project_owner: Project['project_owner'] | undefined +} diff --git a/src/pages/projects/widgets/EditProjectForm.vue b/src/pages/projects/widgets/EditProjectForm.vue new file mode 100644 index 000000000..7cef9a48b --- /dev/null +++ b/src/pages/projects/widgets/EditProjectForm.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/src/pages/projects/widgets/ProjectCards.vue b/src/pages/projects/widgets/ProjectCards.vue new file mode 100644 index 000000000..4f787a029 --- /dev/null +++ b/src/pages/projects/widgets/ProjectCards.vue @@ -0,0 +1,69 @@ + + + diff --git a/src/pages/projects/widgets/ProjectsTable.vue b/src/pages/projects/widgets/ProjectsTable.vue new file mode 100644 index 000000000..30d65833b --- /dev/null +++ b/src/pages/projects/widgets/ProjectsTable.vue @@ -0,0 +1,132 @@ + + + diff --git a/src/pages/users/UsersPage.vue b/src/pages/users/UsersPage.vue new file mode 100644 index 000000000..286998cd7 --- /dev/null +++ b/src/pages/users/UsersPage.vue @@ -0,0 +1,93 @@ + + + diff --git a/src/pages/users/composables/useUsers.ts b/src/pages/users/composables/useUsers.ts new file mode 100644 index 000000000..0460b5947 --- /dev/null +++ b/src/pages/users/composables/useUsers.ts @@ -0,0 +1,89 @@ +import { Ref, ref, unref, watch } from 'vue' +import { getUsers, updateUser, addUser, removeUser, type Filters, Pagination, Sorting } from '../../../data/pages/users' +import { User } from '../types' +import { watchIgnorable } from '@vueuse/core' + +const makePaginationRef = () => ref({ page: 1, perPage: 10, total: 0 }) +const makeSortingRef = () => ref({ sortBy: 'fullname', sortingOrder: null }) +const makeFiltersRef = () => ref>({ isActive: true, search: '' }) + +export const useUsers = (options?: { + pagination?: Ref + sorting?: Ref + filters?: Ref> +}) => { + const isLoading = ref(false) + const users = ref([]) + + const { filters = makeFiltersRef(), sorting = makeSortingRef(), pagination = makePaginationRef() } = options || {} + + const fetch = async () => { + isLoading.value = true + const { data, pagination: newPagination } = await getUsers({ + ...unref(filters), + ...unref(sorting), + ...unref(pagination), + }) + users.value = data + + ignoreUpdates(() => { + pagination.value = newPagination + }) + + isLoading.value = false + } + + const { ignoreUpdates } = watchIgnorable([pagination, sorting], fetch, { deep: true }) + + watch( + filters, + () => { + // Reset pagination to first page when filters changed + pagination.value.page = 1 + fetch() + }, + { deep: true }, + ) + + fetch() + + return { + isLoading, + + filters, + sorting, + pagination, + + users, + + fetch, + + async add(user: User) { + isLoading.value = true + await addUser(user) + users.value.unshift(user) + isLoading.value = false + }, + + async update(user: User) { + isLoading.value = true + await updateUser(user) + users.value = users.value + .map((u) => (u.id === user.id ? user : u)) + .filter((u) => u.active === filters.value.isActive) + .filter((u) => { + if (!filters.value.search) return true + + return u.fullname.toLowerCase().includes(filters.value.search.toLowerCase()) + }) + isLoading.value = false + }, + + async remove(user: User) { + isLoading.value = true + await removeUser(user) + users.value = users.value.filter((u) => u.id !== user.id) + isLoading.value = false + }, + } +} diff --git a/src/pages/users/types.ts b/src/pages/users/types.ts new file mode 100644 index 000000000..7417cc1cc --- /dev/null +++ b/src/pages/users/types.ts @@ -0,0 +1,15 @@ +import { Project } from '../projects/types' + +export type UserRole = 'admin' | 'user' | 'owner' + +export type User = { + id: number + fullname: string + email: string + username: string + role: UserRole + avatar: string + projects: Project[] + notes: string + active: boolean +} diff --git a/src/pages/users/widgets/EditUserForm.vue b/src/pages/users/widgets/EditUserForm.vue new file mode 100644 index 000000000..d5f55059f --- /dev/null +++ b/src/pages/users/widgets/EditUserForm.vue @@ -0,0 +1,132 @@ + + + diff --git a/src/pages/users/widgets/UserAvatar.vue b/src/pages/users/widgets/UserAvatar.vue new file mode 100644 index 000000000..89911ca65 --- /dev/null +++ b/src/pages/users/widgets/UserAvatar.vue @@ -0,0 +1,34 @@ + + + diff --git a/src/pages/users/widgets/UsersTable.vue b/src/pages/users/widgets/UsersTable.vue new file mode 100644 index 000000000..d21ff7918 --- /dev/null +++ b/src/pages/users/widgets/UsersTable.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/src/router/index.ts b/src/router/index.ts index 3ad770062..777b47e47 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -153,6 +153,16 @@ const routes: Array = [ path: 'payments', component: () => import('../pages/payments/PaymentsPage.vue'), }, + { + name: 'users', + path: 'users', + component: () => import('../pages/users/UsersPage.vue'), + }, + { + name: 'projects', + path: 'projects', + component: () => import('../pages/projects/ProjectsPage.vue'), + }, { name: 'billing', path: 'billing', diff --git a/src/scss/main.scss b/src/scss/main.scss index b75eb27f4..0e955e9a7 100644 --- a/src/scss/main.scss +++ b/src/scss/main.scss @@ -56,4 +56,4 @@ pre { .text-regular-small { font-size: 0.8125rem; line-height: 1rem; -} \ No newline at end of file +} diff --git a/tsconfig.json b/tsconfig.json index 9adc24923..d1bf9acef 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,6 @@ "types": ["vite/client"], "allowJs": true }, - "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue", "vite.config.ts"], + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue", "vite.config.ts", "node_modules/vuestic-ui"], "exclude": ["node_modules/**/*"] } diff --git a/yarn.lock b/yarn.lock index 4bca17a3f..cdf8818ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3195,6 +3195,11 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.10.tgz#04ffa7f406ab628f7f7e97ca23e290cd8ab15efc" integrity sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA== +"@types/web-bluetooth@^0.0.20": + version "0.0.20" + resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz#f066abfcd1cbe66267cdbbf0de010d8a41b41597" + integrity sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow== + "@types/yargs-parser@*": version "21.0.3" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" @@ -3510,6 +3515,28 @@ resolved "https://registry.yarnpkg.com/@vuestic/tailwind/-/tailwind-0.1.3.tgz#9610e350171268711ad70dc484766b37c3d45fc1" integrity sha512-R5mQtUR0A0ygBo/csDNy3V1O+FWFzVyvJGgM0SDJ1WbC0/PnZVNdc5C0OdbqZwvbMcRlftbjAPf+HdXQFQSklg== +"@vueuse/core@^10.6.1": + version "10.6.1" + resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-10.6.1.tgz#5b16d8238054c6983b6cb7cd77a78035f098dd89" + integrity sha512-Pc26IJbqgC9VG1u6VY/xrXXfxD33hnvxBnKrLlA2LJlyHII+BSrRoTPJgGYq7qZOu61itITFUnm6QbacwZ4H8Q== + dependencies: + "@types/web-bluetooth" "^0.0.20" + "@vueuse/metadata" "10.6.1" + "@vueuse/shared" "10.6.1" + vue-demi ">=0.14.6" + +"@vueuse/metadata@10.6.1": + version "10.6.1" + resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-10.6.1.tgz#100faa0ced3c0ab4c014fb8e66e781e85e4eb88d" + integrity sha512-qhdwPI65Bgcj23e5lpGfQsxcy0bMjCAsUGoXkJ7DsoeDUdasbZ2DBa4dinFCOER3lF4gwUv+UD2AlA11zdzMFw== + +"@vueuse/shared@10.6.1": + version "10.6.1" + resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-10.6.1.tgz#1d9fc1e3f9083e45b59a693fc372bc50ad62a9e4" + integrity sha512-TECVDTIedFlL0NUfHWncf3zF9Gc4VfdxfQc8JFwoVZQmxpONhLxFrlm0eHQeidHj4rdTPL3KXJa0TZCk1wnc5Q== + dependencies: + vue-demi ">=0.14.6" + "@yarnpkg/esbuild-plugin-pnp@^3.0.0-rc.10": version "3.0.0-rc.15" resolved "https://registry.yarnpkg.com/@yarnpkg/esbuild-plugin-pnp/-/esbuild-plugin-pnp-3.0.0-rc.15.tgz#4e40e7d2eb28825c9a35ab9d04c363931d7c0e67" @@ -9494,7 +9521,7 @@ vue-component-type-helpers@latest: resolved "https://registry.yarnpkg.com/vue-component-type-helpers/-/vue-component-type-helpers-1.8.22.tgz#24c1f324885e5652c7bcc5d9cbd76800b0af3809" integrity sha512-LK3wJHs3vJxHG292C8cnsRusgyC5SEZDCzDCD01mdE/AoREFMl2tzLRuzwyuEsOIz13tqgBcnvysN3Lxsa14Fw== -vue-demi@>=0.13.0, vue-demi@>=0.14.5: +vue-demi@>=0.13.0, vue-demi@>=0.14.5, vue-demi@>=0.14.6: version "0.14.6" resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.6.tgz#dc706582851dc1cdc17a0054f4fec2eb6df74c92" integrity sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==