Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ✨ as a admin user I want to manage customers #50

Merged
merged 1 commit into from
Jan 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,8 +36,8 @@
}

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>>'.
}

async put<T>(url: string, data: any): Promise<T> {
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
Loading