広告の曜日を設定できるように (#10095)

* 曜日選択できるように

* ラベル選択でもチェックが変更されるように

* adを参照しないといけないかも

* smallint -> integer

* 異物混入だったので取りだし

* タイムゾーン指定(Date2つ使うのなんか違和感

* 未テスト

* これにすると出てこないかも

* UIチョット変更

* UI変更 fix bug

* 畳むように修正

* dayofweek->dayOfWeek

* マイグレ時にnot null,default設定してるのでnullable:falseでよさそう

* コメントの記載

* Update packages/backend/src/server/api/endpoints/meta.ts

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

---------

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
This commit is contained in:
nenohi 2023-07-08 08:56:11 +09:00 committed by GitHub
parent 1f181536ae
commit 3c6175d959
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 51 additions and 11 deletions

View File

@ -1451,6 +1451,7 @@ _ad:
back: "戻る" back: "戻る"
reduceFrequencyOfThisAd: "この広告の表示頻度を下げる" reduceFrequencyOfThisAd: "この広告の表示頻度を下げる"
hide: "表示しない" hide: "表示しない"
timezoneinfo: "曜日はサーバーのタイムゾーンを元に指定されます。"
_forgotPassword: _forgotPassword:
enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。" enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。"

View File

@ -0,0 +1,9 @@
export class ad1677054292210 {
name = 'ad1677054292210';
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "ad" ADD "dayOfWeek" integer NOT NULL Default 0`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "ad" DROP COLUMN "dayOfWeek"`);
}
}

View File

@ -55,7 +55,10 @@ export class Ad {
length: 8192, nullable: false, length: 8192, nullable: false,
}) })
public memo: string; public memo: string;
@Column('integer', {
default: 0, nullable: false,
})
public dayOfWeek: number;
constructor(data: Partial<Ad>) { constructor(data: Partial<Ad>) {
if (data == null) return; if (data == null) return;

View File

@ -22,8 +22,9 @@ export const paramDef = {
expiresAt: { type: 'integer' }, expiresAt: { type: 'integer' },
startsAt: { type: 'integer' }, startsAt: { type: 'integer' },
imageUrl: { type: 'string', minLength: 1 }, imageUrl: { type: 'string', minLength: 1 },
dayOfWeek: { type: 'integer' },
}, },
required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'imageUrl'], required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'imageUrl', 'dayOfWeek'],
} as const; } as const;
// eslint-disable-next-line import/no-default-export // eslint-disable-next-line import/no-default-export
@ -41,6 +42,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
createdAt: new Date(), createdAt: new Date(),
expiresAt: new Date(ps.expiresAt), expiresAt: new Date(ps.expiresAt),
startsAt: new Date(ps.startsAt), startsAt: new Date(ps.startsAt),
dayOfWeek: ps.dayOfWeek,
url: ps.url, url: ps.url,
imageUrl: ps.imageUrl, imageUrl: ps.imageUrl,
priority: ps.priority, priority: ps.priority,

View File

@ -31,8 +31,9 @@ export const paramDef = {
ratio: { type: 'integer' }, ratio: { type: 'integer' },
expiresAt: { type: 'integer' }, expiresAt: { type: 'integer' },
startsAt: { type: 'integer' }, startsAt: { type: 'integer' },
dayOfWeek: { type: 'integer' },
}, },
required: ['id', 'memo', 'url', 'imageUrl', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt'], required: ['id', 'memo', 'url', 'imageUrl', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'dayOfWeek'],
} as const; } as const;
// eslint-disable-next-line import/no-default-export // eslint-disable-next-line import/no-default-export
@ -56,6 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
imageUrl: ps.imageUrl, imageUrl: ps.imageUrl,
expiresAt: new Date(ps.expiresAt), expiresAt: new Date(ps.expiresAt),
startsAt: new Date(ps.startsAt), startsAt: new Date(ps.startsAt),
dayOfWeek: ps.dayOfWeek,
}); });
}); });
} }

View File

@ -1,4 +1,4 @@
import { IsNull, LessThanOrEqual, MoreThan } from 'typeorm'; import { IsNull, LessThanOrEqual, MoreThan, Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import JSON5 from 'json5'; import JSON5 from 'json5';
import type { AdsRepository, UsersRepository } from '@/models/index.js'; import type { AdsRepository, UsersRepository } from '@/models/index.js';
@ -263,12 +263,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const instance = await this.metaService.fetch(true); const instance = await this.metaService.fetch(true);
const ads = await this.adsRepository.find({ const ads = await this.adsRepository.createQueryBuilder("ads")
where: { .where('ads.expiresAt > :now', { now: new Date() })
expiresAt: MoreThan(new Date()), .andWhere('ads.startsAt <= :now', { now: new Date() })
startsAt: LessThanOrEqual(new Date()), .andWhere(new Brackets(qb => {
}, // 曜日のビットフラグを確認する
}); qb.where('ads.dayOfWeek & :dayOfWeek > 0', { dayOfWeek: 1 << new Date().getDay() })
.orWhere('ads.dayOfWeek = 0');
}))
.getMany();
const response: any = { const response: any = {
maintainerName: instance.maintainerName, maintainerName: instance.maintainerName,
@ -311,6 +314,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
place: ad.place, place: ad.place,
ratio: ad.ratio, ratio: ad.ratio,
imageUrl: ad.imageUrl, imageUrl: ad.imageUrl,
dayOfWeek: ad.dayOfWeek,
})), })),
enableEmail: instance.enableEmail, enableEmail: instance.enableEmail,
enableServiceWorker: instance.enableServiceWorker, enableServiceWorker: instance.enableServiceWorker,

View File

@ -36,6 +36,16 @@
<template #label>{{ i18n.ts.expiration }}</template> <template #label>{{ i18n.ts.expiration }}</template>
</MkInput> </MkInput>
</FormSplit> </FormSplit>
<MkFolder>
<template #label>{{ i18n.ts.advancedSettings }}</template>
<span>
{{ i18n.ts._ad.timezoneinfo }}
<div v-for="(day, index) in daysOfWeek" :key="index">
<input :id="`ad${ad.id}-${index}`" type="checkbox" :checked="(ad.dayOfWeek & (1 << index)) !== 0" @change="toggleDayOfWeek(ad, index)">
<label :for="`ad${ad.id}-${index}`">{{ day }}</label>
</div>
</span>
</MkFolder>
<MkTextarea v-model="ad.memo"> <MkTextarea v-model="ad.memo">
<template #label>{{ i18n.ts.memo }}</template> <template #label>{{ i18n.ts.memo }}</template>
</MkTextarea> </MkTextarea>
@ -59,6 +69,7 @@ import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue'; import MkTextarea from '@/components/MkTextarea.vue';
import MkRadios from '@/components/MkRadios.vue'; import MkRadios from '@/components/MkRadios.vue';
import MkFolder from '@/components/MkFolder.vue';
import FormSplit from '@/components/form/split.vue'; import FormSplit from '@/components/form/split.vue';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
@ -69,6 +80,7 @@ let ads: any[] = $ref([]);
// ISOTZUTCTZ // ISOTZUTCTZ
const localTime = new Date(); const localTime = new Date();
const localTimeDiff = localTime.getTimezoneOffset() * 60 * 1000; const localTimeDiff = localTime.getTimezoneOffset() * 60 * 1000;
const daysOfWeek: string[] = [i18n.ts._weekday.sunday, i18n.ts._weekday.monday, i18n.ts._weekday.tuesday, i18n.ts._weekday.wednesday, i18n.ts._weekday.thursday, i18n.ts._weekday.friday, i18n.ts._weekday.saturday];
os.api('admin/ad/list').then(adsResponse => { os.api('admin/ad/list').then(adsResponse => {
ads = adsResponse.map(r => { ads = adsResponse.map(r => {
@ -84,6 +96,11 @@ os.api('admin/ad/list').then(adsResponse => {
}); });
}); });
// (index)
function toggleDayOfWeek(ad, index) {
ad.dayOfWeek ^= 1 << index;
}
function add() { function add() {
ads.unshift({ ads.unshift({
id: null, id: null,
@ -95,6 +112,7 @@ function add() {
imageUrl: null, imageUrl: null,
expiresAt: null, expiresAt: null,
startsAt: null, startsAt: null,
dayOfWeek: 0,
}); });
} }
@ -105,6 +123,7 @@ function remove(ad) {
}).then(({ canceled }) => { }).then(({ canceled }) => {
if (canceled) return; if (canceled) return;
ads = ads.filter(x => x !== ad); ads = ads.filter(x => x !== ad);
if (ad.id == null) return;
os.apiWithDialog('admin/ad/delete', { os.apiWithDialog('admin/ad/delete', {
id: ad.id, id: ad.id,
}); });