优化
This commit is contained in:
86
monisuo-admin/src/pages/users/components/columns.ts
Normal file
86
monisuo-admin/src/pages/users/components/columns.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { ColumnDef } from '@tanstack/vue-table'
|
||||
|
||||
import { h } from 'vue'
|
||||
|
||||
import { DataTableColumnHeader, SelectColumn } from '@/components/data-table'
|
||||
import { Copy } from '@/components/prop-ui/copy'
|
||||
import Badge from '@/components/ui/badge/Badge.vue'
|
||||
|
||||
import type { User } from '../data/schema'
|
||||
|
||||
import { callTypes, userTypes } from '../data/data'
|
||||
import DataTableRowActions from './data-table-row-actions.vue'
|
||||
|
||||
export const columns: ColumnDef<User>[] = [
|
||||
SelectColumn as ColumnDef<User>,
|
||||
{
|
||||
accessorKey: 'username',
|
||||
header: ({ column }) => h(DataTableColumnHeader<User>, { column, title: 'username' }),
|
||||
cell: ({ row }) => h('div', { }, row.getValue('username')),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
enableResizing: true,
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: 'email',
|
||||
header: ({ column }) => h(DataTableColumnHeader<User>, { column, title: 'Email' }),
|
||||
cell: ({ row }) => h('div', { }, [
|
||||
h('span', {}, row.getValue('email')),
|
||||
h(Copy, { class: 'ml-2', size: 'sm', content: (row.getValue('email') || '') as string }),
|
||||
]),
|
||||
enableSorting: false,
|
||||
enableResizing: true,
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: 'phoneNumber',
|
||||
header: ({ column }) => h(DataTableColumnHeader<User>, { column, title: 'PhoneNumber' }),
|
||||
cell: ({ row }) => h('div', { }, row.getValue('phoneNumber')),
|
||||
enableSorting: false,
|
||||
enableResizing: true,
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: ({ column }) => h(DataTableColumnHeader<User>, { column, title: 'Status' }),
|
||||
|
||||
cell: ({ row }) => {
|
||||
const callType = callTypes.find(callType => callType.value === row.getValue('status'))
|
||||
|
||||
if (!callType)
|
||||
return null
|
||||
|
||||
return h(Badge, { class: `${callType.style || ''}`, variant: 'outline' }, () => callType.label)
|
||||
},
|
||||
filterFn: (row, id, value) => {
|
||||
return value.includes(row.getValue(id))
|
||||
},
|
||||
enableResizing: true,
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: 'role',
|
||||
header: ({ column }) => h(DataTableColumnHeader<User>, { column, title: 'Role' }),
|
||||
cell: ({ row }) => {
|
||||
const priority = userTypes.find(
|
||||
priority => priority.value === row.getValue('role'),
|
||||
)
|
||||
|
||||
if (!priority)
|
||||
return null
|
||||
|
||||
return h('div', { class: 'flex items-center' }, [
|
||||
priority.icon && h(priority.icon, { class: 'mr-2 h-4 w-4 text-muted-foreground' }),
|
||||
h('span', {}, priority.label),
|
||||
])
|
||||
},
|
||||
enableSorting: false,
|
||||
enableResizing: true,
|
||||
},
|
||||
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) => h(DataTableRowActions, { row }),
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import type { Row } from '@tanstack/vue-table'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
import { Ellipsis } from 'lucide-vue-next'
|
||||
|
||||
import { Modal, ModalContent } from '@/components/prop-ui/modal'
|
||||
|
||||
import type { User } from '../data/schema'
|
||||
|
||||
interface DataTableRowActionsProps {
|
||||
row: Row<User>
|
||||
}
|
||||
const props = defineProps<DataTableRowActionsProps>()
|
||||
const user = computed(() => props.row.original)
|
||||
const isOpen = ref(false)
|
||||
|
||||
const showComponent = shallowRef<Component | null>(null)
|
||||
type TCommand = 'edit' | 'delete'
|
||||
|
||||
const componentLoader: Record<TCommand, () => Promise<{ default: Component }>> = {
|
||||
edit: () => import('./user-resource.vue'),
|
||||
delete: () => import('./user-delete.vue'),
|
||||
}
|
||||
|
||||
async function handleSelect(command: TCommand) {
|
||||
try {
|
||||
const { default: component } = await componentLoader[command]()
|
||||
showComponent.value = component
|
||||
isOpen.value = true
|
||||
}
|
||||
catch (e) {
|
||||
console.error(`Failed to load component for "${command}"`, e)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal v-model:open="isOpen">
|
||||
<UiDropdownMenu>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<UiButton
|
||||
variant="ghost"
|
||||
class="flex h-8 w-8 p-0 data-[state=open]:bg-muted"
|
||||
>
|
||||
<Ellipsis class="size-4" />
|
||||
<span class="sr-only">Open menu</span>
|
||||
</UiButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent align="end" class="w-[160px]">
|
||||
<UiDropdownMenuItem @click.stop="handleSelect('edit')">
|
||||
Edit
|
||||
</UiDropdownMenuItem>
|
||||
|
||||
<UiDropdownMenuItem @click.stop="handleSelect('delete')">
|
||||
Delete
|
||||
<UiDropdownMenuShortcut>⌘⌫</UiDropdownMenuShortcut>
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
|
||||
<ModalContent>
|
||||
<component :is="showComponent" :user="user" @close="isOpen = false" />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import type { Table } from '@tanstack/vue-table'
|
||||
|
||||
import { X } from 'lucide-vue-next'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { DataTableFacetedFilter, DataTableViewOptions } from '@/components/data-table'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
import type { User } from '../data/schema'
|
||||
|
||||
import { callTypes, userTypes } from '../data/data'
|
||||
|
||||
interface DataTableToolbarProps {
|
||||
table: Table<User>
|
||||
}
|
||||
|
||||
const props = defineProps<DataTableToolbarProps>()
|
||||
|
||||
const isFiltered = computed(() => props.table.getState().columnFilters.length > 0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center flex-1 space-x-2">
|
||||
<Input
|
||||
placeholder="Filter users by username..."
|
||||
:model-value="(table.getColumn('username')?.getFilterValue() as string) ?? ''"
|
||||
class="h-8 w-[150px] lg:w-[250px]"
|
||||
@input="table.getColumn('username')?.setFilterValue($event.target.value)"
|
||||
/>
|
||||
<DataTableFacetedFilter
|
||||
v-if="table.getColumn('status')"
|
||||
:column="table.getColumn('status')"
|
||||
title="Status"
|
||||
:options="callTypes"
|
||||
/>
|
||||
<DataTableFacetedFilter
|
||||
v-if="table.getColumn('role')"
|
||||
:column="table.getColumn('role')"
|
||||
title="Role"
|
||||
:options="userTypes"
|
||||
/>
|
||||
|
||||
<Button
|
||||
v-if="isFiltered"
|
||||
variant="ghost"
|
||||
class="h-8 px-2 lg:px-3"
|
||||
@click="table.resetColumnFilters()"
|
||||
>
|
||||
Reset
|
||||
<X class="size-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
<DataTableViewOptions :table="table" />
|
||||
</div>
|
||||
</template>
|
||||
21
monisuo-admin/src/pages/users/components/data-table.vue
Normal file
21
monisuo-admin/src/pages/users/components/data-table.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { DataTableProps } from '@/components/data-table'
|
||||
|
||||
import { DataTable, useGenerateVueTable } from '@/components/data-table'
|
||||
|
||||
import type { User } from '../data/schema'
|
||||
|
||||
import DataTableToolbar from './data-table-toolbar.vue'
|
||||
|
||||
const props = defineProps<DataTableProps<User>>()
|
||||
|
||||
const table = useGenerateVueTable<User>(props)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DataTable :columns :data :loading :table>
|
||||
<template #toolbar>
|
||||
<DataTableToolbar :table class="w-full overflow-x-auto" />
|
||||
</template>
|
||||
</DataTable>
|
||||
</template>
|
||||
24
monisuo-admin/src/pages/users/components/user-create.vue
Normal file
24
monisuo-admin/src/pages/users/components/user-create.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts" setup>
|
||||
import { UserRoundPlus } from 'lucide-vue-next'
|
||||
|
||||
import { Modal, ModalContent, ModalTrigger } from '@/components/prop-ui/modal'
|
||||
|
||||
import UserResource from './user-resource.vue'
|
||||
|
||||
const isOpen = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal v-model:open="isOpen">
|
||||
<ModalTrigger as-child>
|
||||
<UiButton>
|
||||
<UserRoundPlus />
|
||||
Create User
|
||||
</UiButton>
|
||||
</ModalTrigger>
|
||||
|
||||
<ModalContent>
|
||||
<UserResource @close="isOpen = false" />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</template>
|
||||
51
monisuo-admin/src/pages/users/components/user-delete.vue
Normal file
51
monisuo-admin/src/pages/users/components/user-delete.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<script lang="ts" setup>
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import { ModalClose, ModalDescription, ModalFooter, ModalHeader, ModalTitle } from '@/components/prop-ui/modal'
|
||||
|
||||
import type { User } from '../data/schema'
|
||||
|
||||
const { user } = defineProps<{
|
||||
user: User
|
||||
}>()
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'remove'): void
|
||||
}>()
|
||||
|
||||
function handleRemove() {
|
||||
toast(`The following task has been deleted:`, {
|
||||
description: h('pre', { class: 'mt-2 w-[340px] rounded-md bg-slate-950 p-4' }, h('code', { class: 'text-white' }, JSON.stringify(user, null, 2))),
|
||||
})
|
||||
|
||||
emits('remove')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ModalHeader>
|
||||
<ModalTitle>
|
||||
Delete this user: {{ user.username }} ?
|
||||
</ModalTitle>
|
||||
|
||||
<ModalDescription>
|
||||
You are about to delete a user with the ID {{ user.id }}. This action cannot be undone.
|
||||
</ModalDescription>
|
||||
</ModalHeader>
|
||||
|
||||
<ModalFooter>
|
||||
<ModalClose as-child>
|
||||
<UiButton variant="outline">
|
||||
Cancel
|
||||
</UiButton>
|
||||
</ModalClose>
|
||||
|
||||
<ModalClose as-child>
|
||||
<UiButton variant="destructive" @click="handleRemove">
|
||||
Delete
|
||||
</UiButton>
|
||||
</ModalClose>
|
||||
</ModalFooter>
|
||||
</div>
|
||||
</template>
|
||||
161
monisuo-admin/src/pages/users/components/user-form.vue
Normal file
161
monisuo-admin/src/pages/users/components/user-form.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<script lang="ts" setup>
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
|
||||
import type { User } from '../data/schema'
|
||||
import type { UserValidator } from '../validators/user.validator'
|
||||
|
||||
import { userValidator } from '../validators/user.validator'
|
||||
|
||||
const { user } = defineProps<{
|
||||
user?: User
|
||||
}>()
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'close'): void
|
||||
}>()
|
||||
|
||||
const roles = ['superadmin', 'admin', 'cashier', 'manager'] as const
|
||||
const status = ['active', 'inactive', 'invited', 'suspended'] as const
|
||||
|
||||
const initialValues = reactive<UserValidator>({
|
||||
firstName: user?.firstName || '',
|
||||
lastName: user?.lastName || '',
|
||||
username: user?.username || '',
|
||||
email: user?.email || '',
|
||||
phoneNumber: user?.phoneNumber || '',
|
||||
status: user?.status || 'active',
|
||||
role: user?.role || 'cashier',
|
||||
})
|
||||
|
||||
const userFormSchema = toTypedSchema(userValidator)
|
||||
const { handleSubmit } = useForm({
|
||||
validationSchema: userFormSchema,
|
||||
initialValues,
|
||||
})
|
||||
|
||||
const onSubmit = handleSubmit((values) => {
|
||||
const submitUser = { ...values }
|
||||
if (user) {
|
||||
submitUser.id = user.id
|
||||
}
|
||||
toast('You submitted the following values:', {
|
||||
description: h(
|
||||
'pre',
|
||||
{ class: 'mt-2 w-[340px] rounded-md bg-slate-950 p-4' },
|
||||
h('code', { class: 'text-white' }, JSON.stringify(submitUser, null, 2)),
|
||||
),
|
||||
})
|
||||
|
||||
emits('close')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="max-h-[500px] overflow-y-auto">
|
||||
<form class="space-y-8" @submit="onSubmit">
|
||||
<FormField v-slot="{ componentField }" name="firstName">
|
||||
<FormItem>
|
||||
<FormLabel>First Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField v-slot="{ componentField }" name="lastName">
|
||||
<FormItem>
|
||||
<FormLabel>Last Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField v-slot="{ componentField }" name="username">
|
||||
<FormItem>
|
||||
<FormLabel>User Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="email">
|
||||
<FormItem>
|
||||
<FormLabel>Email address</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="phoneNumber">
|
||||
<FormItem>
|
||||
<FormLabel>Phone Number</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="status">
|
||||
<FormItem>
|
||||
<FormLabel>Status</FormLabel>
|
||||
<FormControl>
|
||||
<Select v-bind="componentField">
|
||||
<FormControl>
|
||||
<SelectTrigger class="w-full">
|
||||
<SelectValue placeholder="Select a status" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem v-for="state in status" :key="state" :value="state">
|
||||
{{ state }}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField v-slot="{ componentField }" name="role">
|
||||
<FormItem>
|
||||
<FormLabel>Role</FormLabel>
|
||||
<FormControl>
|
||||
<Select v-bind="componentField">
|
||||
<FormControl>
|
||||
<SelectTrigger class="w-full">
|
||||
<SelectValue placeholder="Select a role" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem v-for="role in roles" :key="role" :value="role">
|
||||
{{ role }}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<Button type="submit" class="w-full">
|
||||
SaveChanges
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { Send } from 'lucide-vue-next'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
|
||||
import type { UserInviteValidator } from '../validators/user-invite.validator'
|
||||
|
||||
import { userInviteValidator } from '../validators/user-invite.validator'
|
||||
|
||||
const roles = ['superadmin', 'admin', 'cashier', 'manager'] as const
|
||||
|
||||
const initialValues = reactive<UserInviteValidator>({
|
||||
email: '',
|
||||
role: 'cashier',
|
||||
description: '',
|
||||
})
|
||||
const userInviteFormSchema = toTypedSchema(userInviteValidator)
|
||||
const { handleSubmit } = useForm({
|
||||
validationSchema: userInviteFormSchema,
|
||||
initialValues,
|
||||
})
|
||||
|
||||
const onSubmit = handleSubmit((values) => {
|
||||
toast('You submitted the following values:', {
|
||||
description: h(
|
||||
'pre',
|
||||
{ class: 'mt-2 w-[340px] rounded-md bg-slate-950 p-4' },
|
||||
h('code', { class: 'text-white' }, JSON.stringify(values, null, 2)),
|
||||
),
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="space-y-8" @submit="onSubmit">
|
||||
<FormField v-slot="{ componentField }" name="email">
|
||||
<FormItem>
|
||||
<FormLabel>Email address</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="role">
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Role
|
||||
<span class="text-destructive"> *</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select v-bind="componentField">
|
||||
<FormControl>
|
||||
<SelectTrigger class="w-full">
|
||||
<SelectValue placeholder="Select a role" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem v-for="role in roles" :key="role" :value="role">
|
||||
{{ role }}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="description">
|
||||
<FormItem>
|
||||
<FormLabel>Description(Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<Button type="submit" class="w-full">
|
||||
Invite
|
||||
<Send />
|
||||
</Button>
|
||||
</form>
|
||||
</template>
|
||||
46
monisuo-admin/src/pages/users/components/user-invite.vue
Normal file
46
monisuo-admin/src/pages/users/components/user-invite.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import { MailPlus } from 'lucide-vue-next'
|
||||
|
||||
import { Modal, ModalClose, ModalContent, ModalDescription, ModalFooter, ModalHeader, ModalTitle, ModalTrigger, useModal } from '@/components/prop-ui/modal'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import UserInviteForm from './user-invite-form.vue'
|
||||
|
||||
const { isDesktop } = useModal()
|
||||
const isOpen = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal v-model:open="isOpen">
|
||||
<ModalTrigger as-child>
|
||||
<Button variant="outline">
|
||||
<MailPlus />
|
||||
Invite User
|
||||
</Button>
|
||||
</ModalTrigger>
|
||||
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<ModalTitle as-child>
|
||||
<div class="flex items-center gap-2">
|
||||
<MailPlus />
|
||||
<span>Invite User</span>
|
||||
</div>
|
||||
</ModalTitle>
|
||||
<ModalDescription>
|
||||
Invite new user to join your team by sending them an email invitation. Assign a role to define their access level.
|
||||
</ModalDescription>
|
||||
</ModalHeader>
|
||||
|
||||
<UserInviteForm />
|
||||
|
||||
<ModalFooter v-if="!isDesktop" class="pt-2">
|
||||
<ModalClose as-child>
|
||||
<Button variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</template>
|
||||
31
monisuo-admin/src/pages/users/components/user-resource.vue
Normal file
31
monisuo-admin/src/pages/users/components/user-resource.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script lang="ts" setup>
|
||||
import { ModalDescription, ModalHeader, ModalTitle } from '@/components/prop-ui/modal'
|
||||
|
||||
import type { User } from '../data/schema'
|
||||
|
||||
import UserForm from './user-form.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
user?: User
|
||||
}>()
|
||||
defineEmits(['close'])
|
||||
|
||||
const user = computed(() => props.user)
|
||||
const title = computed(() => user.value?.id ? `Edit User` : 'New User')
|
||||
const description = computed(() => user.value?.id ? `Edit user ${user.value.username}` : 'Create new user')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ModalHeader>
|
||||
<ModalTitle>
|
||||
{{ title }}
|
||||
</ModalTitle>
|
||||
<ModalDescription>
|
||||
{{ description }}
|
||||
</ModalDescription>
|
||||
</ModalHeader>
|
||||
|
||||
<UserForm :user="user" @close="$emit('close')" />
|
||||
</div>
|
||||
</template>
|
||||
50
monisuo-admin/src/pages/users/data/data.ts
Normal file
50
monisuo-admin/src/pages/users/data/data.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Award, BadgeDollarSign, Handshake, Shield } from 'lucide-vue-next'
|
||||
import { h } from 'vue'
|
||||
|
||||
import type { FacetedFilterOption } from '@/components/data-table'
|
||||
|
||||
export const callTypes: (FacetedFilterOption & { style: string })[] = [
|
||||
{
|
||||
label: 'Active',
|
||||
value: 'active',
|
||||
style: 'bg-teal-100/30 text-teal-900 dark:text-teal-200 border-teal-200',
|
||||
},
|
||||
{
|
||||
label: 'Inactive',
|
||||
value: 'inactive',
|
||||
style: 'bg-neutral-300/40 border-neutral-300',
|
||||
},
|
||||
{
|
||||
label: 'Invited',
|
||||
value: 'invited',
|
||||
style: 'bg-sky-200/40 text-sky-900 dark:text-sky-100 border-sky-300',
|
||||
},
|
||||
{
|
||||
label: 'Suspended',
|
||||
value: 'suspended',
|
||||
style: 'bg-destructive/10 dark:bg-destructive/50 text-destructive dark:text-primary border-destructive/10',
|
||||
},
|
||||
]
|
||||
|
||||
export const userTypes: FacetedFilterOption[] = [
|
||||
{
|
||||
label: 'Superadmin',
|
||||
value: 'superadmin',
|
||||
icon: h(BadgeDollarSign),
|
||||
},
|
||||
{
|
||||
label: 'Admin',
|
||||
value: 'admin',
|
||||
icon: h(Handshake),
|
||||
},
|
||||
{
|
||||
label: 'Manager',
|
||||
value: 'manager',
|
||||
icon: h(Award),
|
||||
},
|
||||
{
|
||||
label: 'Cashier',
|
||||
value: 'cashier',
|
||||
icon: h(Shield),
|
||||
},
|
||||
] as const
|
||||
23
monisuo-admin/src/pages/users/data/schema.ts
Normal file
23
monisuo-admin/src/pages/users/data/schema.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const userStatusSchema = z.enum(['active', 'inactive', 'invited', 'suspended'])
|
||||
export type UserStatus = z.infer<typeof userStatusSchema>
|
||||
|
||||
export const userRoleSchema = z.enum(['superadmin', 'admin', 'cashier', 'manager'])
|
||||
export type UserRole = z.infer<typeof userRoleSchema>
|
||||
|
||||
export const userSchema = z.object({
|
||||
id: z.string(),
|
||||
firstName: z.string(),
|
||||
lastName: z.string(),
|
||||
username: z.string(),
|
||||
email: z.string(),
|
||||
phoneNumber: z.string(),
|
||||
status: userStatusSchema,
|
||||
role: userRoleSchema,
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
})
|
||||
export type User = z.infer<typeof userSchema>
|
||||
|
||||
export const userListSchema = z.array(userSchema)
|
||||
30
monisuo-admin/src/pages/users/data/users.ts
Normal file
30
monisuo-admin/src/pages/users/data/users.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { faker } from '@faker-js/faker'
|
||||
|
||||
export const users = Array.from({ length: 20 }, () => {
|
||||
const firstName = faker.person.firstName()
|
||||
const lastName = faker.person.lastName()
|
||||
return {
|
||||
id: faker.string.uuid(),
|
||||
firstName,
|
||||
lastName,
|
||||
username: faker.internet
|
||||
.username({ firstName, lastName })
|
||||
.toLocaleLowerCase(),
|
||||
email: faker.internet.email({ firstName }).toLocaleLowerCase(),
|
||||
phoneNumber: faker.phone.number({ style: 'international' }),
|
||||
status: faker.helpers.arrayElement([
|
||||
'active',
|
||||
'inactive',
|
||||
'invited',
|
||||
'suspended',
|
||||
]),
|
||||
role: faker.helpers.arrayElement([
|
||||
'superadmin',
|
||||
'admin',
|
||||
'cashier',
|
||||
'manager',
|
||||
]),
|
||||
createdAt: faker.date.past(),
|
||||
updatedAt: faker.date.recent(),
|
||||
}
|
||||
})
|
||||
39
monisuo-admin/src/pages/users/index.vue
Normal file
39
monisuo-admin/src/pages/users/index.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { Loader } from 'lucide-vue-next'
|
||||
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
|
||||
import { columns } from './components/columns'
|
||||
import DataTable from './components/data-table.vue'
|
||||
import UserCreate from './components/user-create.vue'
|
||||
import UserInvite from './components/user-invite.vue'
|
||||
import { users } from './data/users'
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
function mockLoading() {
|
||||
loading.value = true
|
||||
setTimeout(() => {
|
||||
loading.value = false
|
||||
}, 2000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
title="Users"
|
||||
description="Users description"
|
||||
sticky
|
||||
>
|
||||
<template #actions>
|
||||
<UserInvite />
|
||||
<UserCreate />
|
||||
<UiButton variant="outline" @click="mockLoading">
|
||||
<Loader />Mock Loading
|
||||
</UiButton>
|
||||
</template>
|
||||
<div class="overflow-x-auto">
|
||||
<DataTable :loading :data="users" :columns="columns" />
|
||||
</div>
|
||||
</BasicPage>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const userInviteValidator = z.object({
|
||||
email: z.email(),
|
||||
role: z.enum(['superadmin', 'admin', 'cashier', 'manager']),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
export type UserInviteValidator = z.infer<typeof userInviteValidator>
|
||||
16
monisuo-admin/src/pages/users/validators/user.validator.ts
Normal file
16
monisuo-admin/src/pages/users/validators/user.validator.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import { userRoleSchema, userStatusSchema } from '../data/schema'
|
||||
|
||||
export const userValidator = z.object({
|
||||
id: z.string().optional(),
|
||||
firstName: z.string().min(1),
|
||||
lastName: z.string().min(1),
|
||||
username: z.string().min(1),
|
||||
email: z.email().min(1),
|
||||
phoneNumber: z.string().min(1),
|
||||
status: userStatusSchema,
|
||||
role: userRoleSchema,
|
||||
})
|
||||
|
||||
export type UserValidator = z.infer<typeof userValidator>
|
||||
Reference in New Issue
Block a user