diff --git a/package.json b/package.json index a68a026..fc279f8 100644 --- a/package.json +++ b/package.json @@ -46,10 +46,12 @@ }, "dependencies": { "@kintone/rest-api-client": "^2.0.17", + "@types/lodash": "^4.14.202", "@types/sprintf": "^0.1.2", "core-js": "^3.6.4", "date-fns": "^2.30.0", "kintone-ui-component": "^1.14.0", + "lodash": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", "sprintf": "^0.1.5", diff --git a/src/apps/入金予定結果/index.tsx b/src/apps/入金予定結果/index.tsx index f45e478..456e2ae 100644 --- a/src/apps/入金予定結果/index.tsx +++ b/src/apps/入金予定結果/index.tsx @@ -68,6 +68,16 @@ const getCallbackFillAmount = (record: 入金予定結果) => { }; }; +// クエリパラメータよりフィールドに値をセットする +const setData = (event: any, targets: string[]) => { + const param = new URLSearchParams(location.search.replace("?", "")); + targets.forEach((target) => { + const value = param.get(target); + if (value === null) return; + event.record[target].value = value; + event.record[target].lookup = true; + }); +}; (() => { console.info("script build at " + process.env.BUILD_TIME); @@ -81,4 +91,27 @@ const getCallbackFillAmount = (record: 入金予定結果) => { } } }); + + kintone.events.on("app.record.create.show", (event) => { + setData(event, [ + 入金予定結果フィールド名.車室契約情報レコード番号, + 入金予定結果フィールド名.初回振り込み関連申込レコード番号, + ]); + return event; + }); + + kintone.events.on("app.record.create.submit.success", (event) => { + const record = event.record as 入金予定結果; + // 初回請求確定時にはメールを送信する + if ( + record[入金予定結果フィールド名.初回振り込み関連申込レコード番号].value + ) { + sendEmail(EmailID.申込受付, { + season_ticket_contract_entry_record_no: Number( + record[入金予定結果フィールド名.初回振り込み関連申込レコード番号] + .value + ), + }); + } + }); })(); diff --git a/src/apps/各種申請/index.tsx b/src/apps/各種申請/index.tsx index b7d98ec..393597a 100644 --- a/src/apps/各種申請/index.tsx +++ b/src/apps/各種申請/index.tsx @@ -44,6 +44,7 @@ const 口座変更申請承認 = async (record: 各種申請) => { const customer = await get顧客マスタ({ 顧客コード: Number(record.顧客コード.value), }); + if (customer === null) throw new Error("顧客不正"); // 顧客マスタへの反映 await client.record.updateRecord({ @@ -141,7 +142,7 @@ const 利用者情報変更申請承認 = async (record: 各種申請) => { const customer = await get顧客マスタ({ 顧客コード: Number(record.顧客コード.value), }); - + if (customer === null) throw new Error("顧客不正"); await client.record.updateRecord({ app: AppID.顧客マスタ, id: customer.$id.value, diff --git a/src/apps/定期申込予約/customize-manifest.json b/src/apps/定期申込予約/customize-manifest.json new file mode 100644 index 0000000..0b7768d --- /dev/null +++ b/src/apps/定期申込予約/customize-manifest.json @@ -0,0 +1,12 @@ +{ + "app": "", + "scope": "ALL", + "desktop": { + "js": ["dist/定期申込予約.js"], + "css":[] + }, + "mobile": { + "js": [], + "css":[] + } +} diff --git a/src/apps/定期申込予約/index.tsx b/src/apps/定期申込予約/index.tsx new file mode 100644 index 0000000..0dcfb7c --- /dev/null +++ b/src/apps/定期申込予約/index.tsx @@ -0,0 +1,62 @@ +import { AppID } from "@/common/appids"; +import { setHeaderButton } from "@/common/header-button"; +import { CancelError } from "@/exception"; +import { + ShowConfirmDialog, + SuccessDialog, + WarningDialog, +} from "@/middleware/swal"; +import { getCreateUrl } from "@/rest-api/url"; +import { 入金予定結果フィールド名 } from "@/types/入金予定結果"; +import { 定期申込予約 } from "@/types/定期申込予約"; +import { 申込 } from "./自動承認"; + +(() => { + console.info("script build at " + process.env.BUILD_TIME); + + kintone.events.on("app.record.detail.show", (event) => { + const record: 定期申込予約 = event.record; + + if (!record.auto_confirm_status.value) { + setHeaderButton("自動承認", async () => { + const confirm = await ShowConfirmDialog({ + text: "承認しますか", + }); + if (!confirm.isConfirmed) return; + + try { + const entry = new 申込(record); + await entry.初期化(); + await entry.選定(); + + await SuccessDialog.fire(); + + await WarningDialog.fire({ + timer: 2000, + timerProgressBar: true, + text: "初回請求データを作成してください", + }); + const 契約 = entry.作成後契約取得(); + if (!契約) throw new Error(); + + const param = new URLSearchParams({ + [入金予定結果フィールド名.車室契約情報レコード番号]: 契約.$id.value, + [入金予定結果フィールド名.初回振り込み関連申込レコード番号]: + record.$id.value, + }); + const url = getCreateUrl(AppID.入金予定結果, param); + console.log(url, param); + window.open(url, "_blank"); + + location.reload(); + } catch (e) { + if (e instanceof CancelError) { + console.log("canceled"); + } else { + throw e; + } + } + }); + } + }); +})(); diff --git a/src/apps/定期申込予約/初回入金.ts b/src/apps/定期申込予約/初回入金.ts new file mode 100644 index 0000000..8057249 --- /dev/null +++ b/src/apps/定期申込予約/初回入金.ts @@ -0,0 +1,8 @@ +import { dateParse, now } from "@/common/datetime"; +import { 車室契約情報 } from "@/types/車室契約情報"; + +export const 初回請求情報取得 = (車室契約情報: 車室契約情報) => { + const 利用開始日 = dateParse(車室契約情報.契約日.value); + const 今日 = now(); + 今日.setHours(0, 0, 0, 0); +}; diff --git a/src/apps/定期申込予約/自動承認.ts b/src/apps/定期申込予約/自動承認.ts new file mode 100644 index 0000000..8e0ce64 --- /dev/null +++ b/src/apps/定期申込予約/自動承認.ts @@ -0,0 +1,251 @@ +import { AppID } from "@/common/appids"; +import { makeRecordData } from "@/common/rest-api-client"; +import { 契約状況同期 } from "@/logic/契約状況同期"; +import { + ConfirmDialog, + ShowConfirmDialog, + showLoadingDialog, +} from "@/middleware/swal"; +import { EmailID, sendEmail } from "@/mypage/メール"; +import { BulkRequest } from "@/rest-api/bulk"; +import { getDetailUrl } from "@/rest-api/url"; +import { get自動承認グループ } from "@/rest-api/自動承認グループ"; +import { get車室契約情報, get車室契約情報一覧 } from "@/rest-api/車室契約情報"; +import { get車室情報一覧 } from "@/rest-api/車室情報2"; +import { + getNextSMBC番号, + getNext顧客コード, + get顧客マスタ, +} from "@/rest-api/顧客マスタ"; +import { + 定期申込予約, + 定期申込予約フィールド名, + 状態Dropdown, + 自動承認ステータスDropdown, +} from "@/types/定期申込予約"; +import { 自動承認グループ } from "@/types/自動承認グループ"; +import { 車室契約情報, 車室契約情報フィールド名 } from "@/types/車室契約情報"; +import { 車室情報2 } from "@/types/車室情報2"; +import { 顧客マスタフィールド名 } from "@/types/顧客マスタ"; +import { sprintf } from "sprintf"; +import { CancelError } from "../../exception"; +import { 入金予定結果フィールド名 } from "@/types/入金予定結果"; +import { dateParse, now } from "@/common/datetime"; + +export class 申込 { + private 定期申込予約: 定期申込予約; + private 初期化済み: boolean = false; + + private 顧客情報: { + 顧客コード: number; + } | null = null; + + private 車室一覧: 車室情報2[] = []; + private 契約一覧: 車室契約情報[] = []; + private 自動承認グループ: 自動承認グループ | null = null; + + private 作成後契約: 車室契約情報 | null = null; + + private requests: BulkRequest = new BulkRequest(); + + constructor(定期申込予約: 定期申込予約) { + this.定期申込予約 = 定期申込予約; + } + + async 初期化() { + // 車室情報の取得 + await this.契約対象車室取得(); + // 対象のプランデータ取得 + await this.契約情報取得(); + // 自動承認データの取得 + await this.自動承認グループ取得(); + + this.初期化済み = true; + return this; + } + + async 選定() { + // 空き車室の特定 + const target = this.選定情報取得(); + // 顧客マスタの作成/取得 + await this.顧客マスタ取得(); + // 契約の作成 + await this.契約情報作成(target); + // 申込データの更新 + await this.申込情報完了(); + + // データ保存 + await this.save(); + + await 契約状況同期( + this.定期申込予約.駐車場.value, + Number(target.車室番号.value) + ); + } + + 選定情報取得() { + return this.対象車室取得(); + } + + 作成後契約取得() { + return this.作成後契約; + } + + private async 契約対象車室取得() { + if (!this.定期申込予約.駐車場.value) { + throw new Error("駐車場名の設定をしてください"); + } + + this.車室一覧 = await get車室情報一覧({ + 駐車場名: this.定期申込予約.駐車場.value, + }); + } + private async 契約情報取得() { + if (!this.定期申込予約.駐車場.value) { + throw new Error("駐車場名の設定をしてください"); + } + this.契約一覧 = await get車室契約情報一覧({ + 駐車場名: this.定期申込予約.駐車場.value, + 契約中のみ: true, + }); + } + private async 自動承認グループ取得() { + if (!this.定期申込予約.定期駐車場プラン.value) { + throw new Error("プラン名の設定をしてください"); + } + this.自動承認グループ = await get自動承認グループ( + this.定期申込予約.定期駐車場プラン.value + ); + } + + private 対象車室取得() { + if (!this.初期化済み) { + throw new Error("実装エラー 未初期化"); + } + const target = this.自動承認グループ?.対象車室番号.value + .filter(({ value: 定義 }) => { + // 自動承認車室でない場合は対象外とする + if (定義.自動承認スキップ.value.length !== 0) return false; + // 契約中の車室は対象外とする + const 同一車室の契約中情報: 車室契約情報 | undefined = + this.契約一覧.find((契約) => { + return 契約.車室番号.value === 定義.車室番号.value; + }); + if (!!同一車室の契約中情報) return false; + + return true; + }) + .sort((a, b) => { + return Number(a.value.割当順.value) < Number(b.value.割当順.value) + ? -1 + : 1; + }) + .find(() => true); + + if (!target) { + throw new Error("空き車室がありません"); + } + + const 車室番号 = Number(target.value.車室番号.value); + const 車室 = this.車室一覧.find((room) => { + return Number(room.車室番号.value) === 車室番号; + }); + if (!車室) { + throw new Error("車室がありません"); + } + + return 車室; + } + + private async 顧客マスタ取得() { + const customer = await get顧客マスタ({ + メールアドレス: this.定期申込予約.メールアドレス.value, + 必須: false, + }); + if (customer) { + const confirm = await ShowConfirmDialog({ + html: sprintf( + "

既存顧客[%s]様へ契約を追加しますか

", + getDetailUrl(AppID.顧客マスタ, customer.$id.value), + customer.CustomerName.value + ), + }); + + if (!confirm.isConfirmed) throw new CancelError(); + + this.顧客情報 = { + 顧客コード: Number(customer[顧客マスタフィールド名.顧客コード].value), + }; + } else { + this.顧客マスタ作成(); + } + } + + private async 顧客マスタ作成() { + const F = 顧客マスタフィールド名; + const 顧客コード = await getNext顧客コード(); + + this.requests.create({ + app: AppID.顧客マスタ, + record: makeRecordData({ + [F.顧客コード]: String(顧客コード), + [F.顧客名]: this.定期申込予約.氏名.value, + [F.顧客名カナ]: this.定期申込予約.フリガナ.value, + [F.電話番号]: this.定期申込予約.電話番号.value, + [F.メールアドレス]: this.定期申込予約.メールアドレス.value, + [F.SMBC契約番号]: String(await getNextSMBC番号()), + [F.支払方法]: "その他", + }), + }); + + this.顧客情報 = { + 顧客コード, + }; + } + + private async 契約情報作成(対象車室: 車室情報2) { + if (!this.顧客情報) throw new Error("顧客情報不正"); + if (!this.定期申込予約) throw new Error("定期申込予約不正"); + const F = 車室契約情報フィールド名; + + this.requests.create( + { + app: AppID.車室契約情報, + record: makeRecordData({ + [F.顧客コード]: String(this.顧客情報.顧客コード), + [F.契約日]: this.get契約開始日(), + [F.車両番号]: this.定期申込予約.車両番号.value, + [F.車室番号]: 対象車室.車室番号.value, + [F.プラン名]: this.定期申込予約.定期駐車場プラン.value, + }), + }, + async ({ id }) => { + if (!id) throw new Error(); + this.作成後契約 = await get車室契約情報(Number(id)); + } + ); + } + + private async 申込情報完了() { + const F = 定期申込予約フィールド名; + this.requests.update({ + app: AppID.定期申込予約, + id: this.定期申込予約.$id.value, + record: makeRecordData({ + [F.状態]: 状態Dropdown.承認_自動承認, + [F.自動承認ステータス]: 自動承認ステータスDropdown.承認済, + }), + }); + } + + private get契約開始日() { + const 利用開始希望日 = this.定期申込予約.利用開始希望日.value; + if (!利用開始希望日) throw new Error("利用開始希望日不正"); + return 利用開始希望日; + } + + private async save() { + this.requests.debug(); + await this.requests.save(); + } +} diff --git a/src/common/appids.ts b/src/common/appids.ts index f8cb5f1..4862d68 100644 --- a/src/common/appids.ts +++ b/src/common/appids.ts @@ -10,5 +10,7 @@ export const AppID = { 入金予定結果: APP_ID.入金予定結果, 各種申請: APP_ID.各種申請, 車室契約情報: APP_ID.車室契約情報, + 車室情報2: APP_ID.車室情報2, + 自動承認グループ: APP_ID.自動承認グループ, } as const; export type AppID = (typeof AppID)[keyof typeof AppID]; diff --git a/src/common/datetime.ts b/src/common/datetime.ts new file mode 100644 index 0000000..0fa71b3 --- /dev/null +++ b/src/common/datetime.ts @@ -0,0 +1,54 @@ +import { format, isValid, parse, parseISO } from "date-fns"; + +export const DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; +export const DEFAULT_DATE_TIME_FORMAT = "yyyy/MM/dd HH:mm:ss"; +export const DEFAULT_DATE_TIME_FORMAT_ANOTHER1 = "yyyy-MM-dd HH:mm:ss"; +export const DEFAULT_YYYYMM_FORMAT = "yyyyMM"; + +type Input = Date | string | null | undefined; + +export const formatDateStr = (source: Input) => { + return formatToStr(source, DEFAULT_DATE_FORMAT); +}; + +export const formatDateTimeStr = (source: Date | string | null | undefined) => { + return formatToStr(source, DEFAULT_DATE_TIME_FORMAT); +}; +export const formatYYYYMMStr = (source: Date | string | null | undefined) => { + return formatToStr(source, DEFAULT_YYYYMM_FORMAT); +}; + +const formatToStr = (source: Input, formatStr: string) => { + if (source === null || source === undefined) return ""; + if (source instanceof Date) return format(source, formatStr); + return format(parseISO(source), formatStr); +}; + +export const now = () => { + return new Date(); +}; +export const nowStr = (): string => { + return formatDateTimeStr(now()); +}; + +export const dateParse = (source: Input): Date | null => { + return parseFromFormat(source, DEFAULT_DATE_FORMAT); +}; + +export const dateTimeParse = (source: Input): Date | null => { + return ( + parseFromFormat(source, DEFAULT_DATE_TIME_FORMAT) ?? + parseFromFormat(source, DEFAULT_DATE_TIME_FORMAT_ANOTHER1) + ); +}; + +const parseFromFormat = (source: Input, format: string): Date | null => { + if (source === null || source === undefined) return null; + if (source instanceof Date) return source; + + const ret = parse(source, format, new Date()); + if (isValid(ret)) { + return ret; + } + return null; +}; diff --git a/src/common/rest-api-client.ts b/src/common/rest-api-client.ts index dc90bc2..21951f7 100644 --- a/src/common/rest-api-client.ts +++ b/src/common/rest-api-client.ts @@ -1,4 +1,13 @@ -export const setFieldValue = (fieldCode: string, value: string | object) => { +export type Field = { + [fieldCode: string]: { + value: unknown; + }; +}; + +export const setFieldValue = ( + fieldCode: string, + value: string | object +): Field => { return { [fieldCode]: { value, @@ -8,7 +17,7 @@ export const setFieldValue = (fieldCode: string, value: string | object) => { export const makeRecordData = (data: { [fieldCode: string]: string | object; -}) => { +}): Field => { let ret = {}; Object.keys(data).forEach((fieldCode) => { ret = { diff --git a/src/config/kintone.ts b/src/config/kintone.ts index 611399a..c5f1d71 100644 --- a/src/config/kintone.ts +++ b/src/config/kintone.ts @@ -7,6 +7,9 @@ export type KintoneConfig = { 入金予定結果: number; 各種申請: number; 車室契約情報: number; + 車室情報2: number; + 自動承認グループ: number; + 定期駐車場プランマスタ: number; }; }; @@ -21,6 +24,9 @@ export const kintoneConfig = (): KintoneConfig => { 入金予定結果: 272, 各種申請: 294, 車室契約情報: 253, + 車室情報2: 279, + 自動承認グループ: 286, + 定期駐車場プランマスタ: 257, }, }; } else { @@ -34,6 +40,9 @@ export const kintoneConfig = (): KintoneConfig => { 入金予定結果: 272, 各種申請: 294, 車室契約情報: 253, + 車室情報2: 279, + 自動承認グループ: 286, + 定期駐車場プランマスタ: 257, }, }; } diff --git a/src/exception/index.ts b/src/exception/index.ts new file mode 100644 index 0000000..72e75ce --- /dev/null +++ b/src/exception/index.ts @@ -0,0 +1 @@ +export class CancelError extends Error {} diff --git a/src/logic/契約状況同期.ts b/src/logic/契約状況同期.ts new file mode 100644 index 0000000..eed9527 --- /dev/null +++ b/src/logic/契約状況同期.ts @@ -0,0 +1,47 @@ +import { AppID } from "@/common/appids"; +import { makeRecordData } from "@/common/rest-api-client"; +import { BulkRequest } from "@/rest-api/bulk"; +import { get車室契約情報一覧 } from "@/rest-api/車室契約情報"; +import { get車室情報一覧 } from "@/rest-api/車室情報2"; +import { 状態Dropdown, 車室情報2フィールド名 } from "@/types/車室情報2"; + +export const 契約状況同期 = async (駐車場名: string, 車室番号?: number) => { + await 車室情報契約項目同期(駐車場名, 車室番号); +}; + +const 車室情報契約項目同期 = async (駐車場名: string, 車室番号?: number) => { + const requests = new BulkRequest(); + + const 契約一覧 = await get車室契約情報一覧({ + 駐車場名, + 車室番号, + 契約中のみ: true, + }); + + const 車室一覧 = await get車室情報一覧({ + 駐車場名, + 車室番号, + }); + + console.log({ 契約一覧, 車室一覧 }); + + 車室一覧.forEach((車室) => { + const 契約 = 契約一覧.find((c) => c.車室番号.value === 車室.車室番号.value); + + requests.update({ + app: AppID.車室情報2, + id: 車室.$id.value, + record: makeRecordData({ + [車室情報2フィールド名.状態]: !!契約 + ? 状態Dropdown.契約中 + : 状態Dropdown.空き, + [車室情報2フィールド名.契約レコード番号]: 契約?.$id.value ?? "", + }), + }); + }); + // 保存 + if (!requests.isSaved()) { + requests.debug(); + await requests.save(); + } +}; diff --git a/src/middleware/swal.ts b/src/middleware/swal.ts index 2734def..40fae89 100644 --- a/src/middleware/swal.ts +++ b/src/middleware/swal.ts @@ -1,5 +1,5 @@ import { delay } from "@/common/timer"; -import Swal from "sweetalert2"; +import Swal, { SweetAlertOptions } from "sweetalert2"; export const SuccessDialog = Swal.mixin({ icon: "success", @@ -12,6 +12,9 @@ export const ErrorDialog = Swal.mixin({ icon: "error", title: "エラーが発生しました", }); +export const WarningDialog = Swal.mixin({ + icon: "warning", +}); export const ConfirmDialog = Swal.mixin({ icon: "info", @@ -20,6 +23,15 @@ export const ConfirmDialog = Swal.mixin({ cancelButtonText: "キャンセル", }); +export const ShowConfirmDialog = (param: SweetAlertOptions) => { + return ConfirmDialog.fire(param).then((result) => { + if (result.isConfirmed) { + showLoadingDialog(); + } + return result; + }); +}; + export const LoadingDialog = Swal.mixin({ title: "実行中...", text: "画面を閉じないでください", @@ -35,7 +47,7 @@ export const LoadingDialog = Swal.mixin({ * 必ずawaitを入れて同期処理にすること * @param timer */ -export const showLoadingDialog = async (timer: number = 5000) => { +export const showLoadingDialog = async (timer: number = 10 * 1000) => { LoadingDialog.fire({ timer, }).then((result) => { diff --git a/src/rest-api/bulk.ts b/src/rest-api/bulk.ts new file mode 100644 index 0000000..53e0e5c --- /dev/null +++ b/src/rest-api/bulk.ts @@ -0,0 +1,106 @@ +import { AppID } from "@/common/appids"; +import { Field } from "@/common/rest-api-client"; +import { delay } from "@/common/timer"; +import { KintoneRestAPIClient } from "@kintone/rest-api-client"; +import { chunk } from "lodash"; + +const UPDATE_PARAM = { + method: "PUT", + api: "/k/v1/record.json", +} as const; +const CREATE_PARAM = { + method: "POST", + api: "/k/v1/record.json", +} as const; + +type Payload = { + app: AppID; + id?: string | number; + query?: string; + record: Field; +}; + +type BulkRequestResponse = { id?: string; revision?: string }; + +export type RequestParam = { + method: "POST" | "PUT"; + api: string; + payload: Payload; +}; + +type Callback = (result: BulkRequestResponse) => Promise; + +export class BulkRequest { + private client: KintoneRestAPIClient; + + private requests: Array = []; + + private callbacks: { + [index: number]: Callback; + } = {}; + + constructor() { + this.client = new KintoneRestAPIClient({ + baseUrl: location.origin, + }); + } + + update(payload: Payload, callback?: Callback) { + const request: RequestParam = { + ...UPDATE_PARAM, + payload, + }; + this.requests.push(request); + + if (callback) { + this.callbacks[this.requests.length - 1] = callback; + } + } + + create(payload: Payload, callback?: Callback) { + const request: RequestParam = { + ...CREATE_PARAM, + payload, + }; + this.requests.push(request); + + if (callback) { + this.callbacks[this.requests.length - 1] = callback; + } + } + + isSaved() { + return this.requests.length === 0; + } + + async save() { + let results: BulkRequestResponse[] = []; + for (const requests of chunk(this.requests, 20)) { + const res = await this.client.bulkRequest({ requests }); + + results = [...results, ...res.results]; + } + await this.execCallback(results); + this.clear(); + + return results; + } + + private async execCallback(results: BulkRequestResponse[]) { + for (const [index, result] of results.entries()) { + if (index in this.callbacks) { + console.log("exec callback", this.requests.at(index)); + const callback = this.callbacks[index]; + await callback(result); + } + } + } + + clear() { + this.requests = []; + } + + debug() { + console.log("BulkRequest debug", this, this.requests); + } +} diff --git a/src/rest-api/query.ts b/src/rest-api/query.ts new file mode 100644 index 0000000..797e2d7 --- /dev/null +++ b/src/rest-api/query.ts @@ -0,0 +1,53 @@ +import { sprintf } from "sprintf"; + +type ORDER = "asc" | "desc"; +type OrderCondition = { + カラム: string; + 並び: ORDER; +}; +export class QueryBuilder { + private whereConditions: string[] = []; + private order: OrderCondition[] = []; + private _limit: number | null = null; + + where(カラム: string, 条件: string | number, 比較文字列: string = "=") { + if (typeof 条件 === "string") { + this.whereConditions.push( + sprintf('%s %s "%s"', カラム, 比較文字列, 条件) + ); + } else { + this.whereConditions.push(sprintf("%s %s %d", カラム, 比較文字列, 条件)); + } + return this; + } + + orderBy(カラム: string, 並び: ORDER = "asc") { + this.order.push({ + カラム, + 並び, + }); + + return this; + } + + limit(limit: number) { + this._limit = limit; + return this; + } + + build(): string { + let query = ""; + query += this.whereConditions.join(" and ") + " "; + + this.order.forEach(({ カラム, 並び }) => { + query += sprintf(" order by %s %s ", カラム, 並び); + }); + + if (this._limit !== null) { + query += sprintf(" limit %d ", this._limit); + } + + console.log("QueryBuilder", query); + return query; + } +} diff --git a/src/rest-api/url.ts b/src/rest-api/url.ts new file mode 100644 index 0000000..40b9bb2 --- /dev/null +++ b/src/rest-api/url.ts @@ -0,0 +1,17 @@ +import { AppID } from "@/common/appids"; +import { sprintf } from "sprintf"; + +export const getDetailUrl = (app: AppID, レコード番号: string | number) => { + if (typeof レコード番号 === "string") { + レコード番号 = Number(レコード番号); + } + return sprintf("%s/k/%d/show#record=%d", location.origin, app, レコード番号); +}; +export const getCreateUrl = (app: AppID, param?: URLSearchParams) => { + return sprintf( + "%s/k/%d/edit?%s", + location.origin, + app, + !!param ? param.toString() : "" + ); +}; diff --git a/src/rest-api/自動承認グループ.ts b/src/rest-api/自動承認グループ.ts new file mode 100644 index 0000000..59cc281 --- /dev/null +++ b/src/rest-api/自動承認グループ.ts @@ -0,0 +1,35 @@ +import { AppID } from "@/common/appids"; +import { + 自動承認グループ, + 自動承認グループフィールド名, +} from "@/types/自動承認グループ"; +import { KintoneRestAPIClient } from "@kintone/rest-api-client"; +import { sprintf } from "sprintf"; + +const client = new KintoneRestAPIClient(); + +export const get自動承認グループ = async ( + プラン名: string +): Promise<自動承認グループ> => { + const query = sprintf( + sprintf('%s = "%s"', 自動承認グループフィールド名.プラン名, プラン名) + ); + + const { records } = await client.record.getRecords<自動承認グループ>({ + app: AppID.自動承認グループ, + query, + }); + + if (records.length !== 1) { + throw new Error( + sprintf( + "自動承認グループ取得数数エラー expect %d, actual %d query:%s", + 1, + records.length, + query + ) + ); + } + + return records[0]; +}; diff --git a/src/rest-api/車室契約情報.ts b/src/rest-api/車室契約情報.ts index 3b01f8e..e295fed 100644 --- a/src/rest-api/車室契約情報.ts +++ b/src/rest-api/車室契約情報.ts @@ -1,6 +1,8 @@ import { AppID } from "@/common/appids"; -import { 車室契約情報 } from "@/types/車室契約情報"; +import { formatDateStr, now } from "@/common/datetime"; +import { 車室契約情報, 車室契約情報フィールド名 } from "@/types/車室契約情報"; import { KintoneRestAPIClient } from "@kintone/rest-api-client"; +import { QueryBuilder } from "./query"; const client = new KintoneRestAPIClient(); @@ -14,3 +16,34 @@ export const get車室契約情報 = async ( return record; }; + +type get車室契約情報一覧オプション = { + 駐車場名?: string; + 車室番号?: number | string; + 契約中のみ?: boolean; +}; +export const get車室契約情報一覧 = async ({ + 駐車場名, + 車室番号, + 契約中のみ, +}: get車室契約情報一覧オプション): Promise<車室契約情報[]> => { + const builder = new QueryBuilder(); + + if (駐車場名) { + builder.where(車室契約情報フィールド名.定期駐車場名, 駐車場名); + } + if (車室番号) { + builder.where(車室契約情報フィールド名.車室番号, 車室番号); + } + if (契約中のみ) { + const todayStr = formatDateStr(now()); + builder.where(車室契約情報フィールド名.解約日, todayStr, ">"); + } + + const record = await client.record.getAllRecords<車室契約情報>({ + app: AppID.車室契約情報, + condition: builder.build(), + }); + + return record; +}; diff --git a/src/rest-api/車室情報2.ts b/src/rest-api/車室情報2.ts new file mode 100644 index 0000000..f8f0d00 --- /dev/null +++ b/src/rest-api/車室情報2.ts @@ -0,0 +1,31 @@ +import { AppID } from "@/common/appids"; +import { 車室情報2, 車室情報2フィールド名 } from "@/types/車室情報2"; +import { KintoneRestAPIClient } from "@kintone/rest-api-client"; +import { sprintf } from "sprintf"; +import { QueryBuilder } from "./query"; + +const client = new KintoneRestAPIClient(); + +type get車室情報一覧オプション = { + 駐車場名?: string; + 車室番号?: number | string; +}; +export const get車室情報一覧 = async ({ + 駐車場名, + 車室番号, +}: get車室情報一覧オプション): Promise<車室情報2[]> => { + const builder = new QueryBuilder(); + if (駐車場名) { + builder.where(車室情報2フィールド名.駐車場名, 駐車場名); + } + if (車室番号) { + builder.where(車室情報2フィールド名.車室番号, 車室番号); + } + + const { records } = await client.record.getRecords<車室情報2>({ + app: AppID.車室情報2, + query: builder.build(), + }); + + return records; +}; diff --git a/src/rest-api/顧客マスタ.ts b/src/rest-api/顧客マスタ.ts index 37dbf7e..6a569c7 100644 --- a/src/rest-api/顧客マスタ.ts +++ b/src/rest-api/顧客マスタ.ts @@ -1,47 +1,89 @@ import { AppID } from "@/common/appids"; +import { フィールド名 } from "@/types"; import { 顧客マスタ, 顧客マスタフィールド名 } from "@/types/顧客マスタ"; import { KintoneRestAPIClient } from "@kintone/rest-api-client"; import { sprintf } from "sprintf"; +import { QueryBuilder } from "./query"; const client = new KintoneRestAPIClient(); type Props = { レコード番号?: number; 顧客コード?: number; + メールアドレス?: string; + 電話番号?: string; + 必須?: boolean; }; export const get顧客マスタ = async ({ レコード番号, 顧客コード, -}: Props): Promise<顧客マスタ> => { - if (!!レコード番号) { - const { record } = await client.record.getRecord<顧客マスタ>({ - app: AppID.顧客マスタ, - id: レコード番号, - }); + メールアドレス, + 電話番号, + 必須 = true, +}: Props): Promise<顧客マスタ | null> => { + const builder = new QueryBuilder(); - return record; + if (!!レコード番号) { + builder.where(フィールド名.レコード番号, レコード番号); } if (!!顧客コード) { - const query = sprintf( - sprintf("%s = %d", 顧客マスタフィールド名.顧客コード, 顧客コード) - ); + builder.where(顧客マスタフィールド名.顧客コード, 顧客コード); + } + if (!!メールアドレス) { + builder.where(顧客マスタフィールド名.メールアドレス, メールアドレス); + } + if (!!電話番号) { + builder.where(顧客マスタフィールド名.電話番号, 電話番号); + } - const { records } = await client.record.getRecords<顧客マスタ>({ - app: AppID.顧客マスタ, - query, - }); + const { records } = await client.record.getRecords<顧客マスタ>({ + app: AppID.顧客マスタ, + query: builder.build(), + }); - if (records.length !== 1) { + if (records.length === 0) { + if (必須) { throw new Error( - sprintf( - "顧客マスタ取得数エラー expect %d, actual %d", - 1, - records.length - ) + sprintf("顧客マスタ取得数エラー expect %d, actual %d", 1, 0) ); } - return records[0]; + return null; + } + + if (records.length !== 1) { + throw new Error( + sprintf("顧客マスタ取得数エラー expect %d, actual %d", 1, records.length) + ); } + return records[0]; +}; - throw new Error("get顧客マスタ 引数エラー"); +export const getNext顧客コード = async () => { + const query = sprintf( + "order by %s desc limit 1", + 顧客マスタフィールド名.顧客コード + ); + const { records } = await client.record.getRecords<顧客マスタ>({ + app: AppID.顧客マスタ, + query, + }); + if (records.length === 0) { + return 1; + } + return Number(records[0][顧客マスタフィールド名.顧客コード].value) + 1; +}; + +export const getNextSMBC番号 = async () => { + const query = sprintf( + "order by %s desc limit 1", + 顧客マスタフィールド名.SMBC契約番号 + ); + const { records } = await client.record.getRecords<顧客マスタ>({ + app: AppID.顧客マスタ, + query, + }); + if (records.length === 0) { + return 1; + } + return Number(records[0][顧客マスタフィールド名.SMBC契約番号].value) + 1; }; diff --git a/src/types/index.ts b/src/types/index.ts index ce003eb..2c47fc9 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,7 +1,13 @@ import { KintoneRecordField } from "@kintone/rest-api-client"; +export const フィールド名 = { + レコード番号: "$id", +} as const; + +const F = フィールド名; + export type AppRecord = { - $id: KintoneRecordField.ID; + [F.レコード番号]: KintoneRecordField.ID; $revision: KintoneRecordField.Revision; 作成者: KintoneRecordField.Creator; レコード番号: KintoneRecordField.RecordNumber; diff --git a/src/types/入金予定結果.ts b/src/types/入金予定結果.ts index 372b60d..01a6f37 100644 --- a/src/types/入金予定結果.ts +++ b/src/types/入金予定結果.ts @@ -2,9 +2,15 @@ import { KintoneRecordField } from "@kintone/rest-api-client"; import { AppRecord } from "."; const F = { + 車室契約情報レコード番号: "contract_record_number", + 支払対象_利用_年: "target_year", + 支払対象_利用_月: "target_month", + 支払対象_利用_月間数: "target_term_month", + 支払予定日: "payment_plan_date", + 支払予定金額: "payment_plan_amount", + 入金予定コード: "payment_plan_code", 入金日: "appropriation_date", 入金額: "appropriation_amount", - 支払予定金額: "payment_plan_amount", 残金: "remaining_amount", 初回振り込み関連申込レコード番号: "first_payment_entry_record_no", } as const; @@ -23,18 +29,18 @@ export type 支払種別Dropdown = export const 入金予定結果フィールド名 = F; export type 入金予定結果 = AppRecord & { - contract_record_number: KintoneRecordField.Number; - payment_plan_date: KintoneRecordField.Date; - payment_plan_code: KintoneRecordField.SingleLineText; + [F.車室契約情報レコード番号]: KintoneRecordField.Number; + [F.支払予定日]: KintoneRecordField.Date; + [F.入金予定コード]: KintoneRecordField.SingleLineText; [F.残金]: KintoneRecordField.Number; [F.入金日]: KintoneRecordField.Date; - target_year: KintoneRecordField.SingleLineText; - target_month: KintoneRecordField.SingleLineText; + [F.支払対象_利用_年]: KintoneRecordField.SingleLineText; + [F.支払対象_利用_月]: KintoneRecordField.SingleLineText; [F.入金額]: KintoneRecordField.Number; payment_type: KintoneRecordField.Dropdown; customer_name_kana: KintoneRecordField.SingleLineText; [F.支払予定金額]: KintoneRecordField.Number; - target_term_month: KintoneRecordField.SingleLineText; + [F.支払対象_利用_月間数]: KintoneRecordField.SingleLineText; customer_name: KintoneRecordField.SingleLineText; parking_name: KintoneRecordField.SingleLineText; customer_code: KintoneRecordField.Number; diff --git a/src/types/定期申込予約.ts b/src/types/定期申込予約.ts index f36bf0c..69228c7 100644 --- a/src/types/定期申込予約.ts +++ b/src/types/定期申込予約.ts @@ -3,6 +3,8 @@ import { AppRecord } from "."; const F = { 状態: "status", + 自動承認ステータス: "auto_confirm_status", + 利用開始希望日: "利用開始希望日", } as const; export const 状態Dropdown = { @@ -18,6 +20,13 @@ export const 状態Dropdown = { } as const; export type 状態Dropdown = (typeof 状態Dropdown)[keyof typeof 状態Dropdown]; +export const 自動承認ステータスDropdown = { + NONE: "", + 承認済: "承認済", +} as const; +export type 自動承認ステータスDropdown = + (typeof 自動承認ステータスDropdown)[keyof typeof 自動承認ステータスDropdown]; + export const 定期申込予約フィールド名 = F; export type 定期申込予約 = AppRecord & { @@ -54,6 +63,6 @@ export type 定期申込予約 = AppRecord & { ParkingNaviプラン: KintoneRecordField.SingleLineText; 日割り分_月: KintoneRecordField.Number; 支払方法: KintoneRecordField.Dropdown; - auto_confirm_status: KintoneRecordField.SingleLineText; + [F.自動承認ステータス]: KintoneRecordField.SingleLineText; [F.状態]: KintoneRecordField.Dropdown; }; diff --git a/src/types/定期駐車場プランマスタ.ts b/src/types/定期駐車場プランマスタ.ts new file mode 100644 index 0000000..ffcf5db --- /dev/null +++ b/src/types/定期駐車場プランマスタ.ts @@ -0,0 +1,64 @@ +import { KintoneRecordField } from "@kintone/rest-api-client"; +import { AppRecord } from "."; + +const F = { + プラン名: "key", +} as const; + +// export const 支払種別Dropdown = { +// 定期料金: "定期料金", +// 保証金: "保証金", +// 証明書郵送代: "証明書郵送代", +// 事務手数料: "事務手数料", +// 延滞金: "延滞金", +// 余剰金: "余剰金", +// } as const; +// export type 支払種別Dropdown = +// (typeof 支払種別Dropdown)[keyof typeof 支払種別Dropdown]; + +export const 定期駐車場プランマスタフィールド名 = F; + +export type 対象車室番号行データ = { + 割当順: KintoneRecordField.Number; + 車室番号: KintoneRecordField.SingleLineText; + 利用中: KintoneRecordField.CheckBox; + 自動承認スキップ: KintoneRecordField.CheckBox; +}; +export type 定期駐車場プランマスタ = AppRecord & { + 備考: KintoneRecordField.MultiLineText; + 状況: KintoneRecordField.RadioButton; + 支払パターン: KintoneRecordField.Dropdown; + 保証金: KintoneRecordField.Number; + 収容台数: KintoneRecordField.Number; + 住所_駐車場マスタ: KintoneRecordField.SingleLineText; + パスカード保証金: KintoneRecordField.Number; + 定期駐車場名: KintoneRecordField.SingleLineText; + 駐車場名: KintoneRecordField.SingleLineText; + key: KintoneRecordField.SingleLineText; + その他収入: KintoneRecordField.Number; + 住所_手入力: KintoneRecordField.SingleLineText; + 担当: KintoneRecordField.SingleLineText; + 内税: KintoneRecordField.Number; + 定期_駐車場名: KintoneRecordField.SingleLineText; + 契約金額: KintoneRecordField.Number; + 利用者へ表示するプラン名: KintoneRecordField.SingleLineText; + 税率: KintoneRecordField.Number; + 種別: KintoneRecordField.Dropdown; + 住所_使用: KintoneRecordField.SingleLineText; + 駐車場備考: KintoneRecordField.Dropdown; + 表示順: KintoneRecordField.Number; + プラン: KintoneRecordField.Dropdown; + チェーンゲート保証金: KintoneRecordField.Number; + 年額: KintoneRecordField.Calc; + 保証金合計額: KintoneRecordField.Calc; + 送付物: KintoneRecordField.CheckBox; + プラン変更申請にて変更可能なプラン一覧: { + type: "SUBTABLE"; + value: { + id: string; + value: { + プラン変更申請にて変更可能なプラン一覧_定期_駐車場名_月額金額_駐車場備考_プラン_種別_支払パターン: KintoneRecordField.SingleLineText; + }; + }[]; + }; +}; diff --git a/src/types/自動承認グループ.ts b/src/types/自動承認グループ.ts new file mode 100644 index 0000000..c020b18 --- /dev/null +++ b/src/types/自動承認グループ.ts @@ -0,0 +1,39 @@ +import { KintoneRecordField } from "@kintone/rest-api-client"; +import { AppRecord } from "."; + +const F = { + プラン名: "定期駐車場プラン", + 自動承認グループ名: "自動承認グループ名", + 対象車室番号: "対象車室番号", +} as const; + +// export const 支払種別Dropdown = { +// 定期料金: "定期料金", +// 保証金: "保証金", +// 証明書郵送代: "証明書郵送代", +// 事務手数料: "事務手数料", +// 延滞金: "延滞金", +// 余剰金: "余剰金", +// } as const; +// export type 支払種別Dropdown = +// (typeof 支払種別Dropdown)[keyof typeof 支払種別Dropdown]; + +export const 自動承認グループフィールド名 = F; + +export type 対象車室番号行データ = { + 割当順: KintoneRecordField.Number; + 車室番号: KintoneRecordField.SingleLineText; + 利用中: KintoneRecordField.CheckBox; + 自動承認スキップ: KintoneRecordField.CheckBox; +}; +export type 自動承認グループ = AppRecord & { + [F.プラン名]: KintoneRecordField.SingleLineText; + [F.自動承認グループ名]: KintoneRecordField.SingleLineText; + [F.対象車室番号]: { + type: "SUBTABLE"; + value: { + id: string; + value: 対象車室番号行データ; + }[]; + }; +}; diff --git a/src/types/車室契約情報.ts b/src/types/車室契約情報.ts index cf27e55..66f6e47 100644 --- a/src/types/車室契約情報.ts +++ b/src/types/車室契約情報.ts @@ -2,9 +2,14 @@ import { KintoneRecordField } from "@kintone/rest-api-client"; import { AppRecord } from "."; const F = { + 契約日: "契約日", 解約日: "解約日", 車両番号: "車両番号", 防犯登録番号: "防犯登録番号", + 定期駐車場名: "定期駐車場名", + 顧客コード: "顧客コード", + 車室番号: "車室番号", + プラン名: "契約駐車場_0", } as const; // export const 支払種別Dropdown = { @@ -22,14 +27,14 @@ export const 車室契約情報フィールド名 = F; export type 車室契約情報 = AppRecord & { 定期券番号_0: KintoneRecordField.SingleLineText; - 契約駐車場_0: KintoneRecordField.SingleLineText; + [F.プラン名]: KintoneRecordField.SingleLineText; [F.防犯登録番号]: KintoneRecordField.SingleLineText; 定額_3月: KintoneRecordField.Number; 入金日: KintoneRecordField.Date; - 定期駐車場名: KintoneRecordField.SingleLineText; + [F.定期駐車場名]: KintoneRecordField.SingleLineText; 担当: KintoneRecordField.SingleLineText; - 車室番号: KintoneRecordField.SingleLineText; - 契約日: KintoneRecordField.Date; + [F.車室番号]: KintoneRecordField.SingleLineText; + [F.契約日]: KintoneRecordField.Date; 定額_5月: KintoneRecordField.Number; 契約金額: KintoneRecordField.Number; 障がい者手帳有効期限: KintoneRecordField.Date; @@ -37,7 +42,7 @@ export type 車室契約情報 = AppRecord & { 定額_4月: KintoneRecordField.Number; 駐車場備考: KintoneRecordField.SingleLineText; 障がい者手帳画像更新日時: KintoneRecordField.DateTime; - 顧客コード: KintoneRecordField.Number; + [F.顧客コード]: KintoneRecordField.Number; 解約減額: KintoneRecordField.Number; 顧客名カナ_マスタ: KintoneRecordField.SingleLineText; 定額_6月: KintoneRecordField.Number; diff --git a/src/types/車室情報2.ts b/src/types/車室情報2.ts new file mode 100644 index 0000000..b2dc34f --- /dev/null +++ b/src/types/車室情報2.ts @@ -0,0 +1,34 @@ +import { KintoneRecordField } from "@kintone/rest-api-client"; +import { AppRecord } from "."; + +const F = { + 駐車場名: "定期駐車場", + 車室番号: "車室番号", + 状態: "状態", + 契約レコード番号: "契約情報", +} as const; + +export const 状態Dropdown = { + 空き: "空き", + 契約中: "契約中", +} as const; +export type 状態Dropdown = (typeof 状態Dropdown)[keyof typeof 状態Dropdown]; + +export const 車室情報2フィールド名 = F; + +export type 車室情報2 = AppRecord & { + 契約開始日: KintoneRecordField.Date; + [F.契約レコード番号]: KintoneRecordField.Number; + メモ: KintoneRecordField.MultiLineText; + [F.車室番号]: KintoneRecordField.SingleLineText; + 定期駐車場プラン: KintoneRecordField.SingleLineText; + 車室タイプ: KintoneRecordField.Dropdown; + 顧客名: KintoneRecordField.SingleLineText; + 顧客名カナ: KintoneRecordField.SingleLineText; + 定期券番号: KintoneRecordField.SingleLineText; + [F.状態]: KintoneRecordField.Dropdown; + 契約終了日: KintoneRecordField.Date; + 定期駐車場: KintoneRecordField.SingleLineText; + 契約駐車場プラン: KintoneRecordField.SingleLineText; + 自動承認: KintoneRecordField.CheckBox; +}; diff --git a/src/types/顧客マスタ.ts b/src/types/顧客マスタ.ts index c0ae0cf..228833f 100644 --- a/src/types/顧客マスタ.ts +++ b/src/types/顧客マスタ.ts @@ -16,6 +16,9 @@ const F = { 契約者_郵便番号: "契約者_郵便番号", 住所: "住所", 電話番号: "電話番号", + メールアドレス: "メールアドレス", + SMBC契約番号: "ContractNo", + 支払方法: "支払方法", } as const; // export const 支払種別Dropdown = { @@ -40,7 +43,7 @@ export type 顧客マスタ = AppRecord & { ChargedBranchCode: KintoneRecordField.SingleLineText; ChargedBranchName: KintoneRecordField.SingleLineText; [F.引落預金種別]: KintoneRecordField.Number; - ContractNo: KintoneRecordField.SingleLineText; + [F.SMBC契約番号]: KintoneRecordField.SingleLineText; [F.顧客コード]: KintoneRecordField.Number; [F.顧客名]: KintoneRecordField.SingleLineText; SMBC口座名義_1: KintoneRecordField.SingleLineText; @@ -50,7 +53,7 @@ export type 顧客マスタ = AppRecord & { [F.ゆうちょ口座番号]: KintoneRecordField.Number; クレジット入金額: KintoneRecordField.Number; コンビニ払入金額: KintoneRecordField.Number; - メールアドレス: KintoneRecordField.Link; + [F.メールアドレス]: KintoneRecordField.Link; ゆうちょ口座名義_0: KintoneRecordField.SingleLineText; ゆうちょ口座名義_1: KintoneRecordField.SingleLineText; ゆうちょ口座名義: KintoneRecordField.SingleLineText; @@ -60,7 +63,7 @@ export type 顧客マスタ = AppRecord & { [F.顧客名カナ]: KintoneRecordField.SingleLineText; 口座振替依頼書: KintoneRecordField.File; 口座登録催促予定日時: KintoneRecordField.DateTime; - 支払方法: KintoneRecordField.SingleLineText; + [F.支払方法]: KintoneRecordField.SingleLineText; [F.住所]: KintoneRecordField.SingleLineText; 送付方法: KintoneRecordField.Dropdown; 台数: KintoneRecordField.Number; diff --git a/webpack.config.js b/webpack.config.js index 0a6aec4..5739b31 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -70,40 +70,6 @@ module.exports = { // new Dotenv({ systemvars: true }), new ForkTsCheckerWebpackPlugin(), // new BundleAnalyzerPlugin(), - { - // watchモードのとき再ビルドされたものをアップロードする - apply: (compiler) => { - compiler.hooks.afterEmit.tapPromise( - 'upload javascript files', - (compilation) => { - if (!compiler.options.watch) return Promise.resolve(); - - const emittedFiles = Object.keys(compilation.assets) - .filter((file) => { - const source = compilation.assets[file]; - return source.emitted && source.existsAt; - }) - .map((file) => file.replace('.js', '')); - - const processes = glob - .sync(`@(${emittedFiles.join('|')})/customize-manifest.json`, { - cwd: basePath, - }) - .map((file) => { - console.log('\nuploading... ', file); - return exec( - `yarn upload ${path.resolve(basePath, file)}`, - (err, stdout, stderr) => { - if (stdout) process.stdout.write(stdout); - if (stderr) process.stderr.write(stderr); - } - ); - }); - return Promise.all(processes); - } - ); - }, - }, new webpack.DefinePlugin({ 'process.env.BUILD_TIME': JSON.stringify(format(new Date(), 'yyyy-MM-dd HH:mm:ss')), }), diff --git a/yarn.lock b/yarn.lock index e3f874d..9a0f7bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1264,6 +1264,11 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/lodash@^4.14.202": + version "4.14.202" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.202.tgz#f09dbd2fb082d507178b2f2a5c7e74bd72ff98f8" + integrity sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ== + "@types/mime@*": version "3.0.4" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.4.tgz#2198ac274de6017b44d941e00261d5bc6a0e0a45"