Skip to content

Commit

Permalink
feat: ✨ as a admin user I want to manage customers
Browse files Browse the repository at this point in the history
Fixes #47
  • Loading branch information
doroudi committed Jan 14, 2024
1 parent 5da9e48 commit 5d2b49a
Show file tree
Hide file tree
Showing 16 changed files with 347 additions and 12 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@vueuse/head": "^1.3.1",
"apexcharts": "^3.44.2",
"axios": "^0.27.2",
"moment": "^2.30.1",
"nprogress": "^0.2.0",
"pinia": "^2.1.7",
"vue": "^3.3.11",
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/auto-imports.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ declare global {
const useCssVar: typeof import('@vueuse/core')['useCssVar']
const useCssVars: typeof import('vue')['useCssVars']
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
const useCustomerStore: typeof import('./store/customer')['useCustomerStore']
const useCycleList: typeof import('@vueuse/core')['useCycleList']
const useDark: typeof import('@vueuse/core')['useDark']
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
Expand Down Expand Up @@ -470,6 +471,7 @@ declare module 'vue' {
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
readonly useCustomerStore: UnwrapRef<typeof import('./store/customer.store')['useCustomerStore']>
readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']>
readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']>
readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']>
Expand Down Expand Up @@ -779,6 +781,7 @@ declare module '@vue/runtime-core' {
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
readonly useCustomerStore: UnwrapRef<typeof import('./store/customer.store')['useCustomerStore']>
readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']>
readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']>
readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']>
Expand Down
2 changes: 1 addition & 1 deletion src/common/api/api-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class ApiService {
}

async post<T>(url: string, data: any): Promise<T> {
return this.httpClient.post<T>(`${this.apiBase}/${url}`, data)
const response = this.httpClient.post<T>(`${this.apiBase}/${url}`, data)
return response.data

Check failure on line 40 in src/common/api/api-service.ts

View workflow job for this annotation

GitHub Actions / typecheck

Property 'data' does not exist on type 'Promise<AxiosResponse<T, any>>'.
}

Expand Down
19 changes: 19 additions & 0 deletions src/common/filters/date.filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import moment from 'moment'

const formattedDate = function (value: string, format = 'DD, MMM-YYYY, HH:mm') {
if (value === null)
return value
moment.locale('en')
return moment.utc(value).local().format(format)
}
const shortDate = function (value: string) {
moment.locale('en')
return moment.utc(value).local().format('YYYY/MM')
}
const friendlyTime = function (value: string) {
if (value === null)
return value
return moment(value).local().fromNow()
}

export default { formattedDate, shortDate, friendlyTime }
1 change: 1 addition & 0 deletions src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ declare module 'vue' {
CreateCategory: typeof import('./components/Category/CreateCategory.vue')['default']
CreateColor: typeof import('./components/Color/CreateColor.vue')['default']
CreateProduct: typeof import('./components/Products/CreateProduct.vue')['default']
CustomerManagement: typeof import('./components/Customers/CustomerManagement.vue')['default']
DashboardCard: typeof import('./components/DashboardCard.vue')['default']
DonutChart: typeof import('./components/DonutChart.vue')['default']
Editor: typeof import('./components/Editor.vue')['default']
Expand Down
166 changes: 166 additions & 0 deletions src/components/Customers/CustomerManagement.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<script setup lang='ts'>
import { getCurrentInstance } from 'vue'
import { type DataTableColumns, NButton, NIcon, NSpace, NText } from 'naive-ui/es/components'
import type { RowData } from 'naive-ui/es/data-table/src/interface'
import {
Delete24Regular as DeleteIcon,
Edit24Regular as EditIcon,
Add24Filled as PlusIcon,
} from '@vicons/fluent'
import { storeToRefs } from 'pinia'
import { useDialog, useMessage } from 'naive-ui'
const { t } = useI18n()
const store = useCustomerStore()
const { customers, isLoading } = storeToRefs(store)
const dialog = useDialog()
const message = useMessage()
const router = useRouter()
const { proxy } = getCurrentInstance()

Check failure on line 20 in src/components/Customers/CustomerManagement.vue

View workflow job for this annotation

GitHub Actions / typecheck

Property 'proxy' does not exist on type 'ComponentInternalInstance | null'.
onMounted(getItems)
const columns: DataTableColumns<RowData> = [
{
type: 'selection',
},
{
title: 'NAME',
key: 'name',
render: row =>
h(NSpace, {}, {
default: () => [
h(NText, {}, { default: () => `${row.firstName} ${row.lastName}` }),
],
}),
},
{
title: 'Join Date',
key: 'join-date',
render(row) {
return h(NText,
{}, {
default: () => proxy.$filters.friendlyTime(row.joinDate),
})
},
},
{
title: 'Phone',
key: 'phone',
render(row) {
return [
h(NText, {}, { default: () => row.mobile }),
]
},
},
{
title: 'Email',
key: 'email',
render(row) {
return h(NText,
{}, {
default: () => row.email,
})
},
},
{
title: 'Orders Count',
key: 'ordersCount',
},
{
title: 'Actions',
key: 'actions',
width: 110,
render(row) {
return [
h(
NButton,
{
size: 'medium',
renderIcon: renderIcon(EditIcon),
quaternary: true,
circle: true,
class: 'mr-2',
onClick: () => { },
},
),
h(
NButton,
{
size: 'medium',
quaternary: true,
circle: true,
renderIcon: renderIcon(DeleteIcon),
onClick: () => handleDeleteItem(row),
},
),
]
},
},
]
const { options } = storeToRefs(store)
function renderIcon(icon: any) {
return () => h(NIcon, null, { default: () => h(icon) })
}
function handleDeleteItem(row: RowData) {
dialog.error({
title: 'Confirm',
content: 'Are you sure?',
positiveText: 'Yes, Delete',
negativeText: 'Cancel',
onPositiveClick: () => {
store.deleteProduct(row.id)
message.success('Product was deleted!')
},
})
}
function rowKey(row: RowData) {
return row.id
}
function getItems() {
store.getCustomers(options.value)
}
function handlePageChange(page: number) {
options.value.page = page
getItems()
}
function handleSorterChange() {
getItems()
}
function handleFiltersChange() {
getItems()
}
</script>

<template>
<n-layout>
<n-layout-content>
<div class="px-3">
<NSpace justify="space-between" class="mb-3">
<n-input placeholder="Search" />
<NButton type="primary" @click="router.push('/Products/Create')">
<template #icon>
<NIcon>
<PlusIcon />
</NIcon>
</template>
{{ t('categories.createButton') }}
</NButton>
</NSpace>
<n-data-table
remote :columns="columns" :data="customers" :loading="isLoading" :pagination="options"
selectable :row-key="rowKey" @update:sorter="handleSorterChange" @update:filters="handleFiltersChange"
@update:page="handlePageChange"
/>
</div>
</n-layout-content>
</n-layout>
</template>

<style scoped lang='scss'></style>
12 changes: 6 additions & 6 deletions src/components/Sidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -106,22 +106,22 @@ const menuOptions: MenuOption[] = [
],
},
{
label: () => renderLabel(t('menu.customers'), 'customers'),
label: () => renderLabel(t('menu.customers'), '/customers'),
key: 'customers',
icon: renderIcon(CustomersIcon),
},
{
label: () => renderLabel(t('menu.announcement'), 'announcement'),
label: () => renderLabel(t('menu.announcement'), '/announcement'),
key: 'notify',
icon: renderIcon(NewsIcon),
children: [
{
label: () => renderLabel(t('menu.news'), 'news'),
label: () => renderLabel(t('menu.news'), '/news'),
key: 'news',
icon: renderIcon(NewsIcon),
},
{
label: () => renderLabel(t('menu.notifications'), 'notify'),
label: () => renderLabel(t('menu.notifications'), '/notify'),
key: 'notifications',
icon: renderIcon(NotifyIcon),
},
Expand All @@ -138,12 +138,12 @@ const menuOptions: MenuOption[] = [
icon: renderIcon(SettingsIcon),
children: [
{
label: () => renderLabel(t('menu.accountSettings'), 'account'),
label: () => renderLabel(t('menu.accountSettings'), '/account'),
key: 'account-settings',
icon: renderIcon(AccountSettingsIcon),
},
{
label: () => renderLabel(t('menu.websiteSettings'), 'website-settings'),
label: () => renderLabel(t('menu.websiteSettings'), '/website-settings'),
key: 'website-settings',
icon: renderIcon(WebsiteSettingsIcon),
},
Expand Down
11 changes: 10 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import 'uno.css'
import './styles/main.scss'

const routes = setupLayouts(generatedRoutes)

declare module '@vue/runtime-core' {
export interface ComponentCustomProperties {
$filters: any
}
}
async function enableMocking() {
const isMocking = import.meta.env.VITE_API_MOCKING_ENABLED
if (!isMocking)
Expand All @@ -28,6 +32,11 @@ app.use(router)
Object.values(import.meta.glob<{ install: AppModule }>('./modules/*.ts', { eager: true }))
.forEach(i => i.install?.(app, router))

// register filters
app.config.globalProperties.$filters = {}
Object.values(import.meta.glob<any>('./common/filters/*.filter.ts', { eager: true, import: 'default' }))
.forEach(filters => Object.keys(filters).forEach(func => app.config.globalProperties.$filters[func] = filters[func]))

router.beforeEach((to, from, next) => {
// @ts-expect-error "Type instantiation is excessively deep and possibly infinite.ts(2589)"
const { t } = i18n.global
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { HttpResponse, delay, http } from 'msw'
import type { LoginResponse, LoginViewModel } from '~/models/Login'
import type { LoginResponse, LoginViewModel } from '@/models/Login'

Check failure on line 2 in src/mocks/handlers/account.handler.ts

View workflow job for this annotation

GitHub Actions / typecheck

Cannot find module '@/models/Login' or its corresponding type declarations.

const handlers = [
http.post('/api/account/login', async ({ request }) => {
Expand Down
52 changes: 52 additions & 0 deletions src/mocks/handlers/customer.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { HttpResponse, http } from 'msw'
import _ from 'lodash'
import { faker } from '@faker-js/faker'
import { CreatePagedResponse } from '../handlers.utility'

import type { Customer, CustomerCreateModel } from '~/models/Customer'

const customers = _.times(65, createFakeCustomer)
const handlers = [
http.get('/api/customer', ({ request }) => {
const response = CreatePagedResponse<Customer>(request, customers)
return HttpResponse.json(response, { status: 200 })
}),
http.post('/api/customer', async ({ request }) => {
const newItem = await request.json() as CustomerCreateModel
const customer: CustomerCreateModel = {
id: faker.number.int({ max: 2000 }).toString(),
firstName: newItem.firstName,
lastName: newItem.lastName,
address: [],
mobile: newItem.mobile,
joinDate: new Date(),
birthDate: newItem.birthDate,
email: newItem.email,
}
customers.push(customer)
return HttpResponse.json(customer, { status: 201 })
}),
http.delete('/api/customer/:id', ({ params }) => {
const { id } = params
const itemIndex = customers.findIndex(x => x.id === id)
customers.splice(itemIndex, 1)
return HttpResponse.json(true, { status: 200 })
}),

]

function createFakeCustomer(): Customer {
return {
id: faker.number.int().toString(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
address: [],
mobile: faker.phone.number(),
joinDate: faker.date.past(),
birthDate: faker.date.birthdate(),
email: faker.internet.email(),
ordersCount: faker.number.int({ max: 50 }),
}
}

export default handlers
Loading

0 comments on commit 5d2b49a

Please sign in to comment.