index.blade.php
@extends('admin.layouts.app')
@section('title', 'Customers')
@php
$dataSearchForm = [
'inputs' => $inputs,
'posts' => $posts,
'jobs' => $jobs,
'prefectures' => $prefectures,
'sexes' => $sexes,
'mailmaga-flgs' => $mailmagaFlgs,
'tags' => $articleTags,
'statuses' => $statuses
];
@endphp
@section('content')
<page-admin-customers
:laravel-paginator="{{$customers->toJson()}}"
:search-form="{{json_encode($dataSearchForm)}}"
:max-file-size="'{{ config('oms.admin.csv_upload_max_filesize') }}'"
:export-url="'{{ route('admin.customers.report', $inputs ?? []) }}'"
/>
@endsection
Index.vue
<template>
<div class="customers">
<header class="customers__header">
<AppHeading :level="1" :visual="1" class="customers__title" dark>会員一覧</AppHeading>
<ul class="customers__btns">
<li class="customers__btn">
<CreateBtn href="/customers/create">新規追加</CreateBtn>
</li>
<li class="customers__btn">
<CreateBtn :href="exportUrl" :disabled="!isEnableCsvDownload" icon="cloud_download" primary>
CSV出力
</CreateBtn>
</li>
<li class="customers__btn">
<CreateBtn @click="isVisibleSearch = true" icon="search">検索</CreateBtn>
</li>
<li class="customers__btn">
<CreateBtn @click="isCsvImportVisible = true" icon="cloud" primary>CSVインポート</CreateBtn>
</li>
</ul>
</header>
<template v-if="paginator.total">
<DataCounter
:from="paginator.from"
:to="paginator.to"
:total="paginator.total"
class="paymentTypes__counter"
dark
/>
</template>
<CustomerTable
v-model="selected"
:items="paginator.data"
@delete="handleDelete"
editable
deletable
dark
class="customers__table"
/>
<template v-if="paginator.lastPage > 1">
<AppPager
:current="paginator.currentPage"
:last="paginator.lastPage"
@click="handlePager"
class="deliveryTypes__pager"
dark
/>
</template>
<form ref="delete" :action="`/customers/${deleteId}`" method="post">
<input :value="csrfToken" type="hidden" name="_token" />
<input type="hidden" name="_method" value="delete" />
</form>
<CustomersSearch
:is-visible="isVisibleSearch"
:prefecture-options="getFormOptions('prefectures')"
:sex-options="getFormOptions('sexes')"
:position-options="getFormOptions('positions')"
:post-options="getFormOptions('posts')"
:job-options="getFormOptions('jobs')"
:tag-options="getFormOptions('tags')"
:status-options="getFormOptions('statuses')"
:mailmaga-flg-options="getFormOptions('mailmaga-flgs')"
:notification-mail-flg-options="getFormOptions('notification-mail-flgs')"
:default-values="search"
@cancel="isVisibleSearch = false"
@save="handleSearchInput"
/>
<form ref="search" method="get" action="/customers">
<input :value="search.customerId" type="hidden" name="customer_id" />
<input :value="search.email" type="hidden" name="email" />
<input :value="search.name" type="hidden" name="name" />
<input :value="search.kana" type="hidden" name="kana" />
<input :value="search.prefectureId" type="hidden" name="prefecture_id" />
<input :value="search.postcode" type="hidden" name="postcode" />
<input :value="search.city" type="hidden" name="city" />
<input :value="search.line1" type="hidden" name="line1" />
<template v-if="search.sexes.length">
<input v-for="(value, i) in search.sexes" :key="`sexes-${i}`" :value="value" type="hidden" name="sexes[]" />
</template>
<input :value="search.tel" type="hidden" name="tel" />
<input :value="search.fax" type="hidden" name="fax" />
<input :value="search.birthFrom" type="hidden" name="birth_from" />
<input :value="search.birthTo" type="hidden" name="birth_to" />
<input :value="search.birthMonth" type="hidden" name="birth_month" />
<input :value="search.company" type="hidden" name="company" />
<input :value="search.unit" type="hidden" name="unit" />
<template v-if="search.posts.length">
<input v-for="(value, i) in search.posts" :key="`posts-${i}`" :value="value" type="hidden" name="posts[]" />
</template>
<template v-if="search.jobs.length">
<input v-for="(value, i) in search.jobs" :key="`jobs-${i}`" :value="value" type="hidden" name="jobs[]" />
</template>
<template v-if="search.articleTags.length">
<input
v-for="(value, i) in search.articleTags"
:key="`tags-${i}`"
:value="value"
type="hidden"
name="article_tags[]"
/>
</template>
<input :value="search.createdAtFrom" type="hidden" name="created_at_from" />
<input :value="search.createdAtTo" type="hidden" name="created_at_to" />
<input :value="search.premiumDeadlineFrom" type="hidden" name="premium_deadline_from" />
<input :value="search.premiumDeadlineTo" type="hidden" name="premium_deadline_to" />
<input :value="search.lastLoginDateFrom" type="hidden" name="last_login_date_from" />
<input :value="search.lastLoginDateTo" type="hidden" name="last_login_date_to" />
<template v-if="search.statuses.length">
<input
v-for="(value, i) in search.statuses"
:key="`statuses-${i}`"
:value="value"
type="hidden"
name="statuses[]"
/>
</template>
<template v-if="search.mailmagaFlgs.length">
<input
v-for="(value, i) in search.mailmagaFlgs"
:key="`mailmagaFlgs-${i}`"
:value="value"
type="hidden"
name="mailmaga_flgs[]"
/>
</template>
</form>
<form ref="import" action="/customers/import_csv" method="post" enctype="multipart/form-data">
<input :value="csrfToken" type="hidden" name="_token" />
<CustomersCsvImport
:is-visible="isCsvImportVisible"
:max-file-size="maxFileSize"
@csv-import-button-submit="handleCsvImportSubmit"
@cancel="handleCsvImportCancel"
/>
</form>
</div>
</template>
<script lang="ts">
import Vue, { PropType } from 'vue'
import { AppHeading, AppPager } from '~/components/atoms'
import { CreateBtn, CustomerTable, DataCounter } from '~/components/molecules'
import { CustomersSearch, CustomersCsvImport } from '~/components/organisms'
import recursiveToCamels from '~/utils/recursiveToCamels'
import { LaravelPaginator, Paginator } from '~/types/models'
import { SearchFormValues, SearchData } from '~/types/Customers'
import { FormOption } from '~/types/models'
import queryString from 'query-string'
interface Data {
selected: string[]
search: SearchData
deleteId?: number
isVisibleSearch: boolean
isCsvImportVisible: boolean
}
const Customers = Vue.extend({
components: { AppHeading, AppPager, CreateBtn, CustomerTable, DataCounter, CustomersSearch, CustomersCsvImport },
props: {
laravelPaginator: { type: Object as PropType<LaravelPaginator>, required: true },
searchForm: { type: Object as PropType<SearchFormValues>, required: true },
maxFileSize: { type: String },
exportUrl: { type: String, required: true }
},
data(): Data {
const searchQuery = this.searchForm.inputs
return {
selected: [],
deleteId: null,
search: {
customerId: searchQuery.customer_id || '',
email: searchQuery.email || '',
name: searchQuery.name || '',
kana: searchQuery.kana || '',
prefectureId: searchQuery.prefecture_id || '',
postcode: searchQuery.postcode || '',
city: searchQuery.city || '',
line1: searchQuery.line1 || '',
sexes: searchQuery.sexes || [],
tel: searchQuery.tel || '',
fax: searchQuery.fax || '',
birthFrom: searchQuery.birth_from || '',
birthTo: searchQuery.birth_to || '',
birthMonth: searchQuery.birth_month || '',
company: searchQuery.company || '',
unit: searchQuery.unit || '',
posts: searchQuery.posts || [],
jobs: searchQuery.jobs || [],
articleTags: searchQuery.article_tags || [],
createdAtFrom: searchQuery.created_at_from || '',
createdAtTo: searchQuery.created_at_to || '',
premiumDeadlineFrom: searchQuery.premium_deadline_from || '',
premiumDeadlineTo: searchQuery.premium_deadline_to || '',
lastLoginDateFrom: searchQuery.last_login_date_from || '',
lastLoginDateTo: searchQuery.last_login_date_to || '',
statuses: searchQuery.statuses || [],
mailmagaFlgs: searchQuery.mailmaga_flgs || []
},
isVisibleSearch: false,
isCsvImportVisible: false
}
},
computed: {
paginator(): Paginator {
return recursiveToCamels(this.laravelPaginator) as Paginator
},
csrfToken(): string {
return this.$store.getters.csrfToken
},
isEnableCsvDownload(): boolean {
let isEnabled = false
Object.keys(this.searchForm.inputs).forEach(key => {
if (this.searchForm.inputs[key].length) {
isEnabled = true
return
}
})
return isEnabled
}
},
methods: {
getFormOptions(name: string): FormOption[] {
if (!(name in this.searchForm)) return []
return Object.keys(this.searchForm[name]).map(key => ({
id: Number(key),
label: this.searchForm[name][key],
value: key
}))
},
handleDelete(id: number) {
this.deleteId = id
if (window.confirm('削除してもよろしいですか?')) {
this.$nextTick(() => {
;(this.$refs.delete as HTMLFormElement).submit()
})
}
},
handlePager(page: number) {
const oldQuery = this.$route.query
delete oldQuery.page
const params = queryString.stringify(oldQuery)
const query = params ? `page=${page}&${params}` : `page=${page}`
window.location.href = `/customers?${query}`
},
handleSearchInput(data) {
this.search = data
this.$nextTick(() => {
;(this.$refs.search as HTMLFormElement).submit()
})
},
handleCsvImportSubmit() {
window.removeEventListener('beforeunload', (this as any).handleBeforeUnload)
;(this.$refs.import as HTMLFormElement).submit()
},
handleCsvImportCancel() {
this.isCsvImportVisible = false
}
}
})
export default Vue.component('page-admin-customers', Customers)
</script>
<style lang="scss">
</style>
CustomerCsvImport
<template>
<FormDialog
:is-visible="isVisible"
@save="$emit('csv-import-button-submit')"
@close="handleCancel"
@cancel="handleCancel"
title="CSVインポート"
save-label="インポート"
class="postCsvImport searchFields"
>
<div class="postCsvImport__content">
<InputFile :value="files" name="csv" accept="text/csv" />
</div>
<template v-if="maxFileSize">
<AppTexts class="mediaForm__notice">最大アップロードサイズ:{{ maxFileSize }}</AppTexts>
</template>
</FormDialog>
</template>
<script lang="ts">
import Vue from 'vue'
import SearchFields from './mixins/SearchFields.vue'
import { InputFile, AppTexts } from '~/components/atoms'
import { FormDialog } from '~/components/organisms'
interface Data {
files: File[]
}
export default Vue.extend({
components: { FormDialog, InputFile, AppTexts },
mixins: [SearchFields],
props: {
maxFileSize: { type: String }
},
data(): Data {
return {
files: []
}
},
methods: {
handleCancel() {
this.$emit('cancel')
}
}
})
</script>
InputFile
<template>
<label
:class="styledClasses"
:style="styled"
@dragover.prevent="checkDrag"
@dragleave.prevent="checkDrag"
@drop.prevent="handleDrop"
class="inputFile"
>
<input ref="files" v-bind="$attrs" :accept="accept" @change="handleChange" type="file" class="inputFile__input" />
<template v-if="!media">
<span class="inputFile__btn">ファイルを選択</span>
<span>{{ firstFileName }}</span>
</template>
<template v-if="isVisibleExtension">
<span class="inputFile__extension">{{ extension }}</span>
</template>
</label>
</template>
<script lang="ts">
import Vue, { PropType } from 'vue'
interface Data {
isDrag: boolean
previewUrl: string
firstFileName: string
}
export default Vue.extend({
inheritAttrs: false,
props: {
value: { type: Array as PropType<File[]>, required: true },
src: { type: String },
accept: { type: String },
media: { type: Boolean }
},
data(): Data {
return {
isDrag: false,
previewUrl: '',
firstFileName: ''
}
},
computed: {
styledClasses(): string[] {
const classes: string[] = []
if (this.media) classes.push('-media')
return classes
},
styled(): { [key: string]: string } {
const styled: { [key: string]: string } = {}
styled['--bg'] = `url("${require('~/assets/img/default-upload.png')}")`
if (this.src) {
styled['--bg'] = `url(${this.src})`
}
if (this.value.length) {
styled['--bg'] = `url(${this.previewUrl})`
}
return styled
},
extension(): string {
if (this.value.length) {
return this.value[0].name.replace(/^.+\.(.*?)$/, '$1')
}
if (this.src) {
return this.src.replace(/^.+\.(.*?)$/, '$1')
}
return ''
},
acceptMimeTypes(): RegExp[] | null {
if (!this.accept) return null
const mimeTypes: RegExp[] = this.accept
.replace(/\s+/, '')
.split(',')
.map(accept => {
if (/^(.*?)\/\*$/.test(accept)) {
return new RegExp(accept.replace(/^(.*?)\/\*$/, '$1\/.*$'))
}
return new RegExp(accept.replace(/^(.*?)\/(.*?)$/, '$1\/$2$'))
})
return mimeTypes
},
isVisibleExtension(): boolean {
if (this.value.length) {
return !/^.*?\.(jpe?g|png|gif|svg|webp|csv)$/.test(this.value[0].name)
}
if (this.src) {
return !/^.*?\.(jpe?g|png|gif|svg|webp|csv)$/.test(this.src)
}
return false
}
},
watch: {
value(newValue: File[]) {
if (!newValue.length) {
this.previewUrl = ''
} else {
this.createPreviewUrl(newValue[0])
}
}
},
methods: {
validation(fileList: FileList): boolean {
if (!this.accept) return true
const correctFiles: File[] = Array.from(fileList).filter(file => {
for (const mimeType of this.acceptMimeTypes) {
if (mimeType.test(file.type)) return true
}
return false
})
return correctFiles.length === fileList.length
},
createPreviewUrl(file: File) {
const reader = new FileReader()
reader.onload = (e: Event) => {
this.previewUrl = (e.target as any).result
}
reader.readAsDataURL(file)
},
convertFileListToArray(fileList: FileList): File[] {
const files: File[] = []
for (let i = 0, length = fileList.length; i < length; i++) {
files.push(fileList[i])
}
return files
},
checkDrag(e: DragEvent) {
if (e.dataTransfer.types.find(type => type === 'text/plain')) return
this.isDrag = e.type === 'dragover'
},
handleChange(e: Event) {
const fileList: FileList = (e.target as HTMLInputElement).files
// 複数選択の場合は一番最初のファイル名だけを表示
if (!this.media && fileList) this.firstFileName = fileList[0].name
this.$emit('change', this.convertFileListToArray(fileList))
},
handleDrop(e: DragEvent) {
if (!this.isDrag) return
const files: FileList = e.dataTransfer.files
if (!this.validation(files)) return
this.$emit('change', this.convertFileListToArray(files))
this.isDrag = false
;(this.$refs.files as HTMLInputElement).files = files
}
}
})
</script>
<style lang="scss" scoped>
</style>
コメント