|
@@ -3,6 +3,12 @@
|
|
|
vip管理
|
|
vip管理
|
|
|
</x-slot>
|
|
</x-slot>
|
|
|
|
|
|
|
|
|
|
+ @php
|
|
|
|
|
+ $importResult = session('member_import_result');
|
|
|
|
|
+ $canImport = bouncer()->hasPermission('member.import');
|
|
|
|
|
+ $showImportForm = $canImport && (! empty($importResult) || $errors->any());
|
|
|
|
|
+ @endphp
|
|
|
|
|
+
|
|
|
@if(session('success'))
|
|
@if(session('success'))
|
|
|
<div class="mb-4 px-4 py-3 bg-green-500 text-white rounded-lg">
|
|
<div class="mb-4 px-4 py-3 bg-green-500 text-white rounded-lg">
|
|
|
{{ session('success') }}
|
|
{{ session('success') }}
|
|
@@ -13,7 +19,446 @@
|
|
|
<p class="text-xl text-gray-800 font-bold">
|
|
<p class="text-xl text-gray-800 font-bold">
|
|
|
vip管理列表
|
|
vip管理列表
|
|
|
</p>
|
|
</p>
|
|
|
|
|
+
|
|
|
|
|
+ @if ($canImport)
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="primary-button"
|
|
|
|
|
+ onclick="document.getElementById('member-plus-import').classList.toggle('hidden')"
|
|
|
|
|
+ >
|
|
|
|
|
+ @lang('member::app.member.import.title')
|
|
|
|
|
+ </button>
|
|
|
|
|
+ @endif
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ @if ($canImport)
|
|
|
|
|
+ <div
|
|
|
|
|
+ id="member-plus-import"
|
|
|
|
|
+ class="mt-3.5 {{ $showImportForm ? '' : 'hidden' }}"
|
|
|
|
|
+ >
|
|
|
|
|
+ <x-admin::form
|
|
|
|
|
+ :action="route('admin.member.import')"
|
|
|
|
|
+ method="POST"
|
|
|
|
|
+ enctype="multipart/form-data"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="box-shadow rounded bg-white p-4 dark:bg-gray-900">
|
|
|
|
|
+ <p class="mb-4 text-base font-semibold text-gray-800 dark:text-white">
|
|
|
|
|
+ @lang('member::app.member.import.title')
|
|
|
|
|
+ </p>
|
|
|
|
|
+
|
|
|
|
|
+ <p class="mb-4 text-sm text-gray-600 dark:text-gray-300">
|
|
|
|
|
+ @lang('member::app.member.import.help')
|
|
|
|
|
+ </p>
|
|
|
|
|
+
|
|
|
|
|
+ <x-admin::form.control-group>
|
|
|
|
|
+ <x-admin::form.control-group.label class="required">
|
|
|
|
|
+ @lang('member::app.member.import.file-label')
|
|
|
|
|
+ </x-admin::form.control-group.label>
|
|
|
|
|
+
|
|
|
|
|
+ <x-admin::form.control-group.control
|
|
|
|
|
+ type="file"
|
|
|
|
|
+ name="import_file"
|
|
|
|
|
+ rules="required"
|
|
|
|
|
+ accept=".xlsx,.xls,.csv"
|
|
|
|
|
+ :label="trans('member::app.member.import.file-label')"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <x-admin::form.control-group.error control-name="import_file" />
|
|
|
|
|
+
|
|
|
|
|
+ <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
|
|
|
+ @lang('member::app.member.import.file-hint')
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </x-admin::form.control-group>
|
|
|
|
|
+
|
|
|
|
|
+ <p class="mb-2 text-sm font-semibold text-gray-600 dark:text-gray-300">
|
|
|
|
|
+ @lang('member::app.member.import.columns-title')
|
|
|
|
|
+ </p>
|
|
|
|
|
+
|
|
|
|
|
+ <ul class="list-inside list-disc space-y-1 text-sm text-gray-600 dark:text-gray-300">
|
|
|
|
|
+ <li>@lang('member::app.member.import.columns.email')</li>
|
|
|
|
|
+ <li>@lang('member::app.member.import.columns.days')</li>
|
|
|
|
|
+ </ul>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="mt-4 flex justify-end">
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="submit"
|
|
|
|
|
+ class="primary-button"
|
|
|
|
|
+ >
|
|
|
|
|
+ @lang('member::app.member.import.submit')
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </x-admin::form>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ @endif
|
|
|
|
|
+
|
|
|
|
|
+ @if (! empty($importResult))
|
|
|
|
|
+ <div class="mt-3.5 box-shadow rounded bg-white p-4 dark:bg-gray-900">
|
|
|
|
|
+ <p class="mb-3 text-base font-semibold text-gray-800 dark:text-white">
|
|
|
|
|
+ @lang('member::app.member.import.result-title')
|
|
|
|
|
+ </p>
|
|
|
|
|
+
|
|
|
|
|
+ <p class="mb-3 text-sm text-gray-600 dark:text-gray-300">
|
|
|
|
|
+ @lang('member::app.member.import.summary', [
|
|
|
|
|
+ 'updated' => $importResult['updated'] ?? 0,
|
|
|
|
|
+ 'failed' => $importResult['failed'] ?? 0,
|
|
|
|
|
+ 'skipped' => $importResult['skipped'] ?? 0,
|
|
|
|
|
+ ])
|
|
|
|
|
+ </p>
|
|
|
|
|
+
|
|
|
|
|
+ @if (! empty($importResult['errors']))
|
|
|
|
|
+ <div class="overflow-x-auto">
|
|
|
|
|
+ <table class="w-full text-left text-sm text-gray-600 dark:text-gray-300">
|
|
|
|
|
+ <thead>
|
|
|
|
|
+ <tr class="border-b dark:border-gray-800">
|
|
|
|
|
+ <th class="px-2 py-2">@lang('member::app.member.import.row')</th>
|
|
|
|
|
+ <th class="px-2 py-2">@lang('member::app.member.import.email')</th>
|
|
|
|
|
+ <th class="px-2 py-2">@lang('member::app.member.import.message')</th>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ </thead>
|
|
|
|
|
+
|
|
|
|
|
+ <tbody>
|
|
|
|
|
+ @foreach ($importResult['errors'] as $error)
|
|
|
|
|
+ <tr class="border-b dark:border-gray-800">
|
|
|
|
|
+ <td class="px-2 py-2">{{ $error['row'] }}</td>
|
|
|
|
|
+ <td class="px-2 py-2">{{ $error['email'] }}</td>
|
|
|
|
|
+ <td class="px-2 py-2 text-red-600">{{ $error['message'] }}</td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ @endforeach
|
|
|
|
|
+ </tbody>
|
|
|
|
|
+ </table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ @endif
|
|
|
|
|
+ </div>
|
|
|
|
|
+ @endif
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Plus 会员统计:默认收起,点按钮才异步加载 -->
|
|
|
|
|
+ <v-member-plus-statistics>
|
|
|
|
|
+ <!-- Shimmer -->
|
|
|
|
|
+ <div class="mt-3.5 box-shadow rounded bg-white dark:bg-gray-900">
|
|
|
|
|
+ <div class="flex flex-wrap items-center justify-between gap-4 px-4 py-3">
|
|
|
|
|
+ <p class="text-base font-semibold text-gray-800 dark:text-white">
|
|
|
|
|
+ @lang('member::app.member.stats.title')
|
|
|
|
|
+ </p>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="shimmer h-[39px] w-[140px] rounded-md"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </v-member-plus-statistics>
|
|
|
|
|
+
|
|
|
<x-admin::datagrid src="{{ route('admin.member.index') }}"></x-admin::datagrid>
|
|
<x-admin::datagrid src="{{ route('admin.member.index') }}"></x-admin::datagrid>
|
|
|
|
|
+
|
|
|
|
|
+ @pushOnce('scripts')
|
|
|
|
|
+ <script
|
|
|
|
|
+ type="module"
|
|
|
|
|
+ src="{{ bagisto_asset('js/chart.js') }}"
|
|
|
|
|
+ >
|
|
|
|
|
+ </script>
|
|
|
|
|
+
|
|
|
|
|
+ <script
|
|
|
|
|
+ type="text/x-template"
|
|
|
|
|
+ id="v-member-plus-statistics-template"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="mt-3.5 box-shadow rounded bg-white dark:bg-gray-900">
|
|
|
|
|
+ <!-- 标题 + 展开按钮 -->
|
|
|
|
|
+ <div class="flex flex-wrap items-center justify-between gap-4 px-4 py-3">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p class="text-base font-semibold text-gray-800 dark:text-white">
|
|
|
|
|
+ @lang('member::app.member.stats.title')
|
|
|
|
|
+ </p>
|
|
|
|
|
+
|
|
|
|
|
+ <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
|
|
|
+ @lang('member::app.member.stats.hint')
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="secondary-button"
|
|
|
|
|
+ @click="toggle()"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="flex items-center gap-x-2.5">
|
|
|
|
|
+ <span v-if="isOpen">@lang('member::app.member.stats.hide')</span>
|
|
|
|
|
+
|
|
|
|
|
+ <span v-else>@lang('member::app.member.stats.show')</span>
|
|
|
|
|
+
|
|
|
|
|
+ <span
|
|
|
|
|
+ class="text-2xl text-gray-400"
|
|
|
|
|
+ :class="isOpen ? 'icon-sort-up' : 'icon-sort-down'"
|
|
|
|
|
+ ></span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 统计内容 -->
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-if="isOpen"
|
|
|
|
|
+ class="border-t px-4 py-4 dark:border-gray-800"
|
|
|
|
|
+ >
|
|
|
|
|
+ <!-- 时间区间 -->
|
|
|
|
|
+ <div class="flex flex-wrap items-end gap-x-2.5 gap-y-2">
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="secondary-button"
|
|
|
|
|
+ @click="applyQuickRange(7)"
|
|
|
|
|
+ >
|
|
|
|
|
+ @lang('member::app.member.stats.range-7')
|
|
|
|
|
+ </button>
|
|
|
|
|
+
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="secondary-button"
|
|
|
|
|
+ @click="applyQuickRange(30)"
|
|
|
|
|
+ >
|
|
|
|
|
+ @lang('member::app.member.stats.range-30')
|
|
|
|
|
+ </button>
|
|
|
|
|
+
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="secondary-button"
|
|
|
|
|
+ @click="applyQuickRange(90)"
|
|
|
|
|
+ >
|
|
|
|
|
+ @lang('member::app.member.stats.range-90')
|
|
|
|
|
+ </button>
|
|
|
|
|
+
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="secondary-button"
|
|
|
|
|
+ @click="applyQuickRange(365)"
|
|
|
|
|
+ >
|
|
|
|
|
+ @lang('member::app.member.stats.range-365')
|
|
|
|
|
+ </button>
|
|
|
|
|
+
|
|
|
|
|
+ <x-admin::flat-picker.date class="!w-[140px]" ::allow-input="false">
|
|
|
|
|
+ <input
|
|
|
|
|
+ class="flex min-h-[39px] w-full rounded-md border px-3 py-2 text-sm text-gray-600 transition-all hover:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400"
|
|
|
|
|
+ v-model="filters.start"
|
|
|
|
|
+ placeholder="@lang('member::app.member.stats.start-date')"
|
|
|
|
|
+ />
|
|
|
|
|
+ </x-admin::flat-picker.date>
|
|
|
|
|
+
|
|
|
|
|
+ <x-admin::flat-picker.date class="!w-[140px]" ::allow-input="false">
|
|
|
|
|
+ <input
|
|
|
|
|
+ class="flex min-h-[39px] w-full rounded-md border px-3 py-2 text-sm text-gray-600 transition-all hover:border-gray-400 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:hover:border-gray-400"
|
|
|
|
|
+ v-model="filters.end"
|
|
|
|
|
+ placeholder="@lang('member::app.member.stats.end-date')"
|
|
|
|
|
+ />
|
|
|
|
|
+ </x-admin::flat-picker.date>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 概览 -->
|
|
|
|
|
+ <div class="mt-4 grid grid-cols-2 gap-4">
|
|
|
|
|
+ <div class="rounded-md bg-gray-50 p-4 dark:bg-gray-800">
|
|
|
|
|
+ <p class="text-xs text-gray-500 dark:text-gray-400">
|
|
|
|
|
+ @lang('member::app.member.stats.active')
|
|
|
|
|
+ </p>
|
|
|
|
|
+
|
|
|
|
|
+ <p class="mt-1 text-lg font-bold leading-none text-gray-800 dark:text-white">
|
|
|
|
|
+ @{{ summary.active ?? 0 }}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="rounded-md bg-gray-50 p-4 dark:bg-gray-800">
|
|
|
|
|
+ <p class="text-xs text-gray-500 dark:text-gray-400">
|
|
|
|
|
+ @lang('member::app.member.stats.expired-total')
|
|
|
|
|
+ </p>
|
|
|
|
|
+
|
|
|
|
|
+ <p class="mt-1 text-lg font-bold leading-none text-gray-800 dark:text-white">
|
|
|
|
|
+ @{{ summary.expired_total ?? 0 }}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="rounded-md bg-gray-50 p-4 dark:bg-gray-800">
|
|
|
|
|
+ <p class="text-xs text-gray-500 dark:text-gray-400">
|
|
|
|
|
+ @lang('member::app.member.stats.new-total')
|
|
|
|
|
+ </p>
|
|
|
|
|
+
|
|
|
|
|
+ <p class="mt-1 text-lg font-bold leading-none text-gray-800 dark:text-white">
|
|
|
|
|
+ @{{ summary.new ?? 0 }}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="rounded-md bg-gray-50 p-4 dark:bg-gray-800">
|
|
|
|
|
+ <p class="text-xs text-gray-500 dark:text-gray-400">
|
|
|
|
|
+ @lang('member::app.member.stats.expired')
|
|
|
|
|
+ </p>
|
|
|
|
|
+
|
|
|
|
|
+ <p class="mt-1 text-lg font-bold leading-none text-gray-800 dark:text-white">
|
|
|
|
|
+ @{{ summary.expired ?? 0 }}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 图例 -->
|
|
|
|
|
+ <div class="mt-4 flex flex-wrap justify-center gap-5">
|
|
|
|
|
+ <div class="flex items-center gap-1">
|
|
|
|
|
+ <span class="h-3.5 w-3.5 rounded-md" style="background-color: #598de6"></span>
|
|
|
|
|
+
|
|
|
|
|
+ <p class="text-xs text-gray-600 dark:text-gray-300">
|
|
|
|
|
+ @lang('member::app.member.stats.series-new')
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="flex items-center gap-1">
|
|
|
|
|
+ <span class="h-3.5 w-3.5 rounded-md" style="background-color: #f87171"></span>
|
|
|
|
|
+
|
|
|
|
|
+ <p class="text-xs text-gray-600 dark:text-gray-300">
|
|
|
|
|
+ @lang('member::app.member.stats.series-expired')
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 图表 -->
|
|
|
|
|
+ <template v-if="isLoading">
|
|
|
|
|
+ <div class="shimmer mt-4 h-[180px] w-full rounded-md"></div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <template v-else>
|
|
|
|
|
+ <x-admin::charts.bar
|
|
|
|
|
+ ::key="chartVersion"
|
|
|
|
|
+ ::labels="labels"
|
|
|
|
|
+ ::datasets="datasets"
|
|
|
|
|
+ ::aspect-ratio="3"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <p
|
|
|
|
|
+ v-if="isEmpty"
|
|
|
|
|
+ class="mt-2 text-center text-xs text-gray-500 dark:text-gray-400"
|
|
|
|
|
+ >
|
|
|
|
|
+ @lang('member::app.member.stats.empty')
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </script>
|
|
|
|
|
+
|
|
|
|
|
+ <script type="module">
|
|
|
|
|
+ app.component('v-member-plus-statistics', {
|
|
|
|
|
+ template: '#v-member-plus-statistics-template',
|
|
|
|
|
+
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ // 默认收起:不请求统计接口,等用户点按钮
|
|
|
|
|
+ isOpen: false,
|
|
|
|
|
+
|
|
|
|
|
+ report: {
|
|
|
|
|
+ labels: [],
|
|
|
|
|
+ new: [],
|
|
|
|
|
+ expired: [],
|
|
|
|
|
+ summary: {},
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ isLoading: true,
|
|
|
|
|
+
|
|
|
|
|
+ // 每次取数后 +1,用来强制图表重建(图表组件只在 mounted 时绘制)
|
|
|
|
|
+ chartVersion: 0,
|
|
|
|
|
+
|
|
|
|
|
+ today: "{{ now()->format('Y-m-d') }}",
|
|
|
|
|
+
|
|
|
|
|
+ filters: {
|
|
|
|
|
+ start: "{{ now()->subDays(29)->format('Y-m-d') }}",
|
|
|
|
|
+
|
|
|
|
|
+ end: "{{ now()->format('Y-m-d') }}",
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ computed: {
|
|
|
|
|
+ labels() {
|
|
|
|
|
+ return this.report.labels ?? [];
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ summary() {
|
|
|
|
|
+ return this.report.summary ?? {};
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ datasets() {
|
|
|
|
|
+ return [{
|
|
|
|
|
+ label: "{{ trans('member::app.member.stats.series-new') }}",
|
|
|
|
|
+ data: this.report.new ?? [],
|
|
|
|
|
+ backgroundColor: '#598de6',
|
|
|
|
|
+ barThickness: 8,
|
|
|
|
|
+ }, {
|
|
|
|
|
+ label: "{{ trans('member::app.member.stats.series-expired') }}",
|
|
|
|
|
+ data: this.report.expired ?? [],
|
|
|
|
|
+ backgroundColor: '#f87171',
|
|
|
|
|
+ barThickness: 8,
|
|
|
|
|
+ }];
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ isEmpty() {
|
|
|
|
|
+ if (this.isLoading) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return ! this.summary.new && ! this.summary.expired;
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ watch: {
|
|
|
|
|
+ filters: {
|
|
|
|
|
+ handler() {
|
|
|
|
|
+ this.getStats();
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ deep: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ toggle() {
|
|
|
|
|
+ this.isOpen = ! this.isOpen;
|
|
|
|
|
+
|
|
|
|
|
+ if (this.isOpen) {
|
|
|
|
|
+ this.getStats();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ getStats() {
|
|
|
|
|
+ this.isLoading = true;
|
|
|
|
|
+
|
|
|
|
|
+ this.$axios.get("{{ route('admin.member.statistics') }}", {
|
|
|
|
|
+ params: this.filters
|
|
|
|
|
+ })
|
|
|
|
|
+ .then(response => {
|
|
|
|
|
+ this.report = response.data;
|
|
|
|
|
+
|
|
|
|
|
+ this.isLoading = false;
|
|
|
|
|
+
|
|
|
|
|
+ this.chartVersion++;
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(error => {
|
|
|
|
|
+ this.isLoading = false;
|
|
|
|
|
+
|
|
|
|
|
+ let message = error.response?.data?.message;
|
|
|
|
|
+
|
|
|
|
|
+ if (message) {
|
|
|
|
|
+ this.$emitter.emit('add-flash', { type: 'error', message });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ applyQuickRange(days) {
|
|
|
|
|
+ this.filters = {
|
|
|
|
|
+ start: this.shiftDate(this.today, -(days - 1)),
|
|
|
|
|
+
|
|
|
|
|
+ end: this.today,
|
|
|
|
|
+ };
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ shiftDate(dateString, offsetDays) {
|
|
|
|
|
+ let date = new Date(dateString + 'T00:00:00');
|
|
|
|
|
+
|
|
|
|
|
+ date.setDate(date.getDate() + offsetDays);
|
|
|
|
|
+
|
|
|
|
|
+ let month = String(date.getMonth() + 1).padStart(2, '0');
|
|
|
|
|
+
|
|
|
|
|
+ let day = String(date.getDate()).padStart(2, '0');
|
|
|
|
|
+
|
|
|
|
|
+ return `${date.getFullYear()}-${month}-${day}`;
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+ </script>
|
|
|
|
|
+ @endPushOnce
|
|
|
</x-admin::layouts>
|
|
</x-admin::layouts>
|