diff --git a/.vscode/settings.json b/.vscode/settings.json index c4419968..13314b92 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "eslint.workingDirectories": ["./api", "./web"] + "eslint.workingDirectories": ["./api", "./web"], + "eslint.validate": ["javascript", "typescript", "vue"] } diff --git a/api/package-lock.json b/api/package-lock.json index a3d64bd3..5bee418a 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -31,7 +31,7 @@ "@types/cors": "^2.8.12", "@types/express": "^4.17.14", "@types/express-jwt": "^6.0.4", - "@types/lodash": "^4.14.191", + "@types/lodash": "^4.17.9", "@types/luxon": "^3.4.2", "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "^6.6.0", @@ -1345,9 +1345,9 @@ } }, "node_modules/@types/lodash": { - "version": "4.14.191", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz", - "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==", + "version": "4.17.9", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.9.tgz", + "integrity": "sha512-w9iWudx1XWOHW5lQRS9iKpK/XuRhnN+0T7HvdCCd802FYkT1AMTnxndJHGrNJwRoRHkslGr4S29tjm1cT7x/7w==", "dev": true }, "node_modules/@types/luxon": { @@ -7745,9 +7745,9 @@ } }, "@types/lodash": { - "version": "4.14.191", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz", - "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==", + "version": "4.17.9", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.9.tgz", + "integrity": "sha512-w9iWudx1XWOHW5lQRS9iKpK/XuRhnN+0T7HvdCCd802FYkT1AMTnxndJHGrNJwRoRHkslGr4S29tjm1cT7x/7w==", "dev": true }, "@types/luxon": { diff --git a/api/package.json b/api/package.json index b433d84f..97bcd34e 100644 --- a/api/package.json +++ b/api/package.json @@ -40,7 +40,7 @@ "@types/cors": "^2.8.12", "@types/express": "^4.17.14", "@types/express-jwt": "^6.0.4", - "@types/lodash": "^4.14.191", + "@types/lodash": "^4.17.9", "@types/luxon": "^3.4.2", "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "^6.6.0", diff --git a/api/src/config.ts b/api/src/config.ts index 9a9ad912..13ab1496 100644 --- a/api/src/config.ts +++ b/api/src/config.ts @@ -36,5 +36,16 @@ export const DB_PASS = process.env.DB_PASS || "" export const DB_NAME = process.env.DB_NAME || "" export const DB_PORT = parseInt(process.env.DB_PORT || "1433") +export const DB_HEALTH_CHECK_INTERVAL_SECONDS = parseInt( + process.env.DB_HEALTH_CHECK_INTERVAL_SECONDS || "5" +) +export const DB_HEALTH_CHECK_TIMEOUT_SECONDS = parseInt( + process.env.DB_HEALTH_CHECK_TIMEOUT_SECONDS || "10" +) +export const DB_HEALTH_CHECK_RETRIES = parseInt(process.env.DB_HEALTH_CHECK_RETRIES || "3") +export const DB_HEALTH_CHECK_START_PERIOD_SECONDS = parseInt( + process.env.DB_HEALTH_CHECK_START_PERIOD_SECONDS || "5" +) + export const RELEASE_TAG = process.env.RELEASE_TAG || "" export const GIT_COMMIT_HASH = process.env.GIT_COMMIT_HASH || "" diff --git a/api/src/controllers/base-controller.ts b/api/src/controllers/base-controller.ts index 32b022c0..72eb0d39 100644 --- a/api/src/controllers/base-controller.ts +++ b/api/src/controllers/base-controller.ts @@ -1,21 +1,70 @@ import { NextFunction, Request, Response } from "express" +import { Attributes, Model, ScopeOptions, WhereOptions } from "sequelize" +import { isEmpty } from "lodash" +import { AuthorizedRequest } from "@/middleware/authz.middleware" import User from "@/models/user" export type Actions = "index" | "show" | "new" | "edit" | "create" | "update" | "destroy" - -// See https://guides.rubyonrails.org/routing.html#crud-verbs-and-actions -export class BaseController { - protected request: Request +// See api/node_modules/sequelize/types/model.d.ts -> Model -> scope +export type BaseScopeOptions = string | ScopeOptions + +// Keep in sync with web/src/api/base-api.ts +const MAX_PER_PAGE = 1000 +const MAX_PER_PAGE_EQUIVALENT = -1 +const DEFAULT_PER_PAGE = 10 + +/** + * See https://guides.rubyonrails.org/routing.html#crud-verbs-and-actions + * + * Usage: + * ```ts + * router + * .route("/api/users") + * .get(UsersController.index) + * .post(UsersController.create) + * route + * .route("/api/users/:userId") + * .get(UsersController.show) + * .patch(UsersController.update) + * .delete(UsersController.destroy) + * ``` + * or + * ```ts + * router.get("/api/users", UsersController.index) + * // etc + * ``` + * + * ```ts + * router.route("/api/users").get(UsersController.index) + * ``` + * maps `/api/users` to `UsersController#index` method. + */ +export class BaseController { + protected request: AuthorizedRequest protected response: Response protected next: NextFunction constructor(req: Request, res: Response, next: NextFunction) { - this.request = req + // Assumes authorization has occured first in + // api/src/middlewares/jwt-middleware.ts and api/src/middlewares/authorization-middleware.ts + // At some future point it would make sense to do all that logic as + // controller actions + this.request = req as AuthorizedRequest this.response = res this.next = next } + /** + * Usage: + * ```ts + * router.route("/api/users").get(UsersController.index) + * ``` + * or + * ```ts + * router.get("/api/users", UsersController.index) + * ``` + */ static get index() { return async (req: Request, res: Response, next: NextFunction) => { const controllerInstance = new this(req, res, next) @@ -23,8 +72,16 @@ export class BaseController { } } - // Usage app.post("/api/users", UsersController.create) - // maps /api/users to UsersController#create() + /** + * Usage: + * ```ts + * router.route("/api/users").post(UsersController.create) + * ``` + * or + * ```ts + * router.post("/api/users", UsersController.create) + * ``` + */ static get create() { return async (req: Request, res: Response, next: NextFunction) => { const controllerInstance = new this(req, res, next) @@ -32,6 +89,16 @@ export class BaseController { } } + /** + * Usage: + * ```ts + * router.route("/api/users/:userId").get(UsersController.show) + * ``` + * or + * ```ts + * router.get("/api/users/:userId", UsersController.show) + * ``` + */ static get show() { return async (req: Request, res: Response, next: NextFunction) => { const controllerInstance = new this(req, res, next) @@ -80,7 +147,7 @@ export class BaseController { // Child controllers that are on an non-authorizable route should override this method // and return undefined get currentUser(): User { - return this.request.user as User + return this.request.user } get params() { @@ -93,8 +160,8 @@ export class BaseController { get pagination() { const page = parseInt(this.query.page?.toString() || "") || 1 - const perPage = parseInt(this.query.perPage?.toString() || "") || 10 - const limit = perPage === -1 ? 1000 : Math.max(1, Math.min(perPage, 1000)) + const perPage = parseInt(this.query.perPage?.toString() || "") || DEFAULT_PER_PAGE + const limit = this.determineLimit(perPage) const offset = (page - 1) * limit return { page, @@ -103,6 +170,42 @@ export class BaseController { offset, } } + + buildWhere( + overridableOptions: WhereOptions> = {}, + nonOverridableOptions: WhereOptions> = {} + ): WhereOptions> { + // TODO: consider if we should add parsing of Sequelize [Op.is] and [Op.not] here + // or in the api/src/utils/enhanced-qs-decoder.ts function + const queryWhere = this.query.where as WhereOptions> + return { + ...overridableOptions, + ...queryWhere, + ...nonOverridableOptions, + } as WhereOptions> + } + + buildFilterScopes>( + initialScopes: BaseScopeOptions[] = [] + ): BaseScopeOptions[] { + const filters = this.query.filters as FilterOptions + const scopes = initialScopes + if (!isEmpty(filters)) { + Object.entries(filters).forEach(([key, value]) => { + scopes.push({ method: [key, value] }) + }) + } + + return scopes + } + + private determineLimit(perPage: number) { + if (perPage === MAX_PER_PAGE_EQUIVALENT) { + return MAX_PER_PAGE + } + + return Math.max(1, Math.min(perPage, MAX_PER_PAGE)) + } } export default BaseController diff --git a/api/src/controllers/fiscal-periods-controller.ts b/api/src/controllers/fiscal-periods-controller.ts index 721a6e11..229495e1 100644 --- a/api/src/controllers/fiscal-periods-controller.ts +++ b/api/src/controllers/fiscal-periods-controller.ts @@ -1,14 +1,16 @@ -import { WhereOptions } from "sequelize" - import BaseController from "./base-controller" import { FiscalPeriod } from "@/models" -export class FiscalPeriodsController extends BaseController { +export class FiscalPeriodsController extends BaseController { async index() { - const where = this.query.where as WhereOptions try { - const fiscalPeriods = await FiscalPeriod.findAll({ + const where = this.buildWhere() + const scopes = this.buildFilterScopes() + const scopedFiscalPeriods = FiscalPeriod.scope(scopes) + + const totalCount = await scopedFiscalPeriods.count({ where }) + const fiscalPeriods = await scopedFiscalPeriods.findAll({ where, order: [ ["dateStart", "ASC"], @@ -17,6 +19,7 @@ export class FiscalPeriodsController extends BaseController { }) return this.response.json({ fiscalPeriods, + totalCount, }) } catch (error) { return this.response diff --git a/api/src/controllers/funding-submission-line-jsons-controller.ts b/api/src/controllers/funding-submission-line-jsons-controller.ts index 58b89bef..f74d9c6c 100644 --- a/api/src/controllers/funding-submission-line-jsons-controller.ts +++ b/api/src/controllers/funding-submission-line-jsons-controller.ts @@ -1,93 +1,109 @@ import { isNil } from "lodash" -import { WhereOptions } from "sequelize" - -import BaseController from "./base-controller" import { FundingSubmissionLineJson } from "@/models" import { FundingSubmissionLineJsonSerializer } from "@/serializers" +import BaseController from "@/controllers/base-controller" -export class FundingSubmissionLineJsonsController extends BaseController { +export class FundingSubmissionLineJsonsController extends BaseController { async index() { - const where = this.query.where as WhereOptions - return FundingSubmissionLineJson.findAll({ - where, - order: ["dateStart"], - }) - .then((fundingSubmissionLineJsons) => { - const serializedfundingSubmissionLineJsons = FundingSubmissionLineJsonSerializer.asTable( - fundingSubmissionLineJsons - ) - return this.response.json({ - fundingSubmissionLineJsons: serializedfundingSubmissionLineJsons, - }) + try { + const where = this.buildWhere() + const scopes = this.buildFilterScopes() + const scopedFundingSubmissionLineJsons = FundingSubmissionLineJson.scope(scopes) + + const totalCount = await scopedFundingSubmissionLineJsons.count({ where }) + const fundingSubmissionLineJsons = await scopedFundingSubmissionLineJsons.findAll({ + where, + order: ["dateStart"], }) - .catch((error) => { - return this.response - .status(400) - .json({ message: `Invalid query for fundingSubmissionLineJsons: ${error}` }) + const serializedfundingSubmissionLineJsons = FundingSubmissionLineJsonSerializer.asTable( + fundingSubmissionLineJsons + ) + return this.response.json({ + fundingSubmissionLineJsons: serializedfundingSubmissionLineJsons, + totalCount, }) + } catch (error) { + return this.response.status(400).json({ + message: `Invalid query for fundingSubmissionLineJsons: ${error}`, + }) + } } async show() { - const fundingSubmissionLineJson = await this.loadFundingSubmissionLineJson() - if (isNil(fundingSubmissionLineJson)) - return this.response.status(404).json({ message: "FundingSubmissionLineJson not found." }) + try { + const fundingSubmissionLineJson = await this.loadFundingSubmissionLineJson() + if (isNil(fundingSubmissionLineJson)) { + return this.response.status(404).json({ + message: "FundingSubmissionLineJson not found.", + }) + } - const serializedfundingSubmissionLineJson = - FundingSubmissionLineJsonSerializer.asDetailed(fundingSubmissionLineJson) - return this.response.json({ - fundingSubmissionLineJson: serializedfundingSubmissionLineJson, - }) + const serializedfundingSubmissionLineJson = + FundingSubmissionLineJsonSerializer.asDetailed(fundingSubmissionLineJson) + return this.response.json({ + fundingSubmissionLineJson: serializedfundingSubmissionLineJson, + }) + } catch (error) { + return this.response.status(400).json({ + message: `Error fetching fundingSubmissionLineJson: ${error}`, + }) + } } async create() { - return FundingSubmissionLineJson.create(this.request.body) - .then((fundingSubmissionLineJson) => { - return this.response.status(201).json({ fundingSubmissionLineJson }) + try { + const fundingSubmissionLineJson = await FundingSubmissionLineJson.create(this.request.body) + const serializedfundingSubmissionLineJson = + FundingSubmissionLineJsonSerializer.asDetailed(fundingSubmissionLineJson) + return this.response.status(201).json({ + fundingSubmissionLineJson: serializedfundingSubmissionLineJson, }) - .catch((error) => { - return this.response - .status(422) - .json({ message: `FundingSubmissionLineJson creation failed: ${error}` }) + } catch (error) { + return this.response.status(422).json({ + message: `FundingSubmissionLineJson creation failed: ${error}`, }) + } } async update() { - const fundingSubmissionLineJson = await this.loadFundingSubmissionLineJson() - if (isNil(fundingSubmissionLineJson)) - return this.response.status(404).json({ message: "FundingSubmissionLineJson not found." }) - - return fundingSubmissionLineJson - .update(this.request.body) - .then((fundingSubmissionLineJson) => { - const serializedfundingSubmissionLineJson = - FundingSubmissionLineJsonSerializer.asDetailed(fundingSubmissionLineJson) - return this.response.json({ - fundingSubmissionLineJson: serializedfundingSubmissionLineJson, + try { + const fundingSubmissionLineJson = await this.loadFundingSubmissionLineJson() + if (isNil(fundingSubmissionLineJson)) { + return this.response.status(404).json({ + message: "FundingSubmissionLineJson not found.", }) + } + + await fundingSubmissionLineJson.update(this.request.body) + const serializedfundingSubmissionLineJson = + FundingSubmissionLineJsonSerializer.asDetailed(fundingSubmissionLineJson) + return this.response.json({ + fundingSubmissionLineJson: serializedfundingSubmissionLineJson, }) - .catch((error) => { - return this.response - .status(422) - .json({ message: `FundingSubmissionLineJson update failed: ${error}` }) + } catch (error) { + return this.response.status(422).json({ + message: `FundingSubmissionLineJson update failed: ${error}`, }) + } } async destroy() { - const fundingSubmissionLineJson = await this.loadFundingSubmissionLineJson() - if (isNil(fundingSubmissionLineJson)) - return this.response.status(404).json({ message: "FundingSubmissionLineJson not found." }) + try { + const fundingSubmissionLineJson = await this.loadFundingSubmissionLineJson() + if (isNil(fundingSubmissionLineJson)) { + return this.response.status(404).json({ + message: "FundingSubmissionLineJson not found.", + }) + } - return fundingSubmissionLineJson - .destroy() - .then(() => { - return this.response.status(204).end() - }) - .catch((error) => { - return this.response - .status(422) - .json({ message: `FundingSubmissionLineJson deletion failed: ${error}` }) + await fundingSubmissionLineJson.destroy() + return this.response.status(204).end() + } catch (error) { + return this.response.status(422).json({ + message: `FundingSubmissionLineJson deletion failed: ${error}`, }) + } } private loadFundingSubmissionLineJson(): Promise { diff --git a/api/src/db/data/funding-submission-lines.json b/api/src/db/data/funding-submission-lines.json index f319c90f..9ecaf30b 100644 --- a/api/src/db/data/funding-submission-lines.json +++ b/api/src/db/data/funding-submission-lines.json @@ -62,7 +62,7 @@ { "fiscalYear": "2022/23", "sectionName": "Child Care Spaces", - "lineName": "School Age (PT)", + "lineName": "School Age (FT)", "fromAge": 5, "toAge": 6, "monthlyAmount": 500.0, @@ -132,7 +132,7 @@ { "fiscalYear": "2022/23", "sectionName": "Administration (10% of Spaces)", - "lineName": "School Age (PT)", + "lineName": "School Age (FT)", "fromAge": 5, "toAge": 6, "monthlyAmount": 50.0, @@ -202,7 +202,7 @@ { "fiscalYear": "2022/23", "sectionName": "Quality Program Enhancement", - "lineName": "School Age (PT)", + "lineName": "School Age (FT)", "fromAge": 5, "toAge": 6, "monthlyAmount": 97.33, @@ -522,7 +522,7 @@ { "fiscalYear": "2023/24", "sectionName": "Child Care Spaces", - "lineName": "School Age (PT)", + "lineName": "School Age (FT)", "fromAge": 5, "toAge": 6, "monthlyAmount": 500.0, @@ -592,7 +592,7 @@ { "fiscalYear": "2023/24", "sectionName": "Administration (10% of Spaces)", - "lineName": "School Age (PT)", + "lineName": "School Age (FT)", "fromAge": 5, "toAge": 6, "monthlyAmount": 50.0, @@ -662,7 +662,7 @@ { "fiscalYear": "2023/24", "sectionName": "Quality Program Enhancement", - "lineName": "School Age (PT)", + "lineName": "School Age (FT)", "fromAge": 5, "toAge": 6, "monthlyAmount": 97.33, diff --git a/api/src/db/seeds/development/2023.12.12T00.25.24.fill-fiscal-periods-table.ts b/api/src/db/seeds/development/2023.12.12T00.25.24.fill-fiscal-periods-table.ts index 0eac92ed..2c3b7998 100644 --- a/api/src/db/seeds/development/2023.12.12T00.25.24.fill-fiscal-periods-table.ts +++ b/api/src/db/seeds/development/2023.12.12T00.25.24.fill-fiscal-periods-table.ts @@ -1,6 +1,7 @@ import { DateTime } from "luxon" import type { SeedMigration } from "@/db/umzug" +import { FiscalPeriodMonths } from "@/models/fiscal-period" export const up: SeedMigration = async ({ context: { FiscalPeriod } }) => { const today = DateTime.now() @@ -15,7 +16,7 @@ export const up: SeedMigration = async ({ context: { FiscalPeriod } }) => { for (let i = 0; i < 12; i++) { const dateStart = date.startOf("month") const dateEnd = date.endOf("month").set({ millisecond: 0 }) - const dateName = dateStart.toFormat("MMMM").toLowerCase() + const dateName = dateStart.toFormat("MMMM").toLowerCase() as FiscalPeriodMonths const fiscalYear = `${year}-${(year + 1).toString().slice(-2)}` diff --git a/api/src/db/seeds/production/2023.12.12T00.25.24.fill-fiscal-periods-table.ts b/api/src/db/seeds/production/2023.12.12T00.25.24.fill-fiscal-periods-table.ts index bf07559e..3935596f 100644 --- a/api/src/db/seeds/production/2023.12.12T00.25.24.fill-fiscal-periods-table.ts +++ b/api/src/db/seeds/production/2023.12.12T00.25.24.fill-fiscal-periods-table.ts @@ -1,6 +1,7 @@ import moment from "moment" import type { SeedMigration } from "@/db/umzug" +import { FiscalPeriodMonths } from "@/models/fiscal-period" export const up: SeedMigration = async ({ context: { FiscalPeriod } }) => { const APRIL = 3 // JS Date months are zero-indexed @@ -11,7 +12,7 @@ export const up: SeedMigration = async ({ context: { FiscalPeriod } }) => { for (let i = 0; i < 12; i++) { const dateStart = moment(date).startOf("month") const dateEnd = moment(dateStart).endOf("month").milliseconds(0) - const dateName = dateStart.format("MMMM").toLowerCase() + const dateName = dateStart.format("MMMM").toLowerCase() as FiscalPeriodMonths const fiscalYear = `${year}-${(year + 1).toString().slice(-2)}` diff --git a/api/src/initializers/05-wait-for-database.ts b/api/src/initializers/05-wait-for-database.ts index f551d3e0..4dc121d9 100644 --- a/api/src/initializers/05-wait-for-database.ts +++ b/api/src/initializers/05-wait-for-database.ts @@ -1,10 +1,15 @@ -import { DB_HOST, DB_NAME, DB_PASS, DB_PORT, DB_USER, NODE_ENV } from "@/config" import { Sequelize } from "sequelize" +import { merge } from "lodash" -const INTERVAL_SECONDS = 5 -const TIMEOUT_SECONDS = 5 -const RETRIES = 3 -const START_PERIOD_SECONDS = 10 +import { + DB_HEALTH_CHECK_INTERVAL_SECONDS, + DB_HEALTH_CHECK_RETRIES, + DB_HEALTH_CHECK_START_PERIOD_SECONDS, + DB_HEALTH_CHECK_TIMEOUT_SECONDS, +} from "@/config" +import { SEQUELIZE_CONFIG } from "@/db/db-client" +import sleep from "@/utils/sleep" +import { isCredentialFailure, isNetworkFailure, isSocketFailure } from "@/utils/db-error-helpers" function checkHealth(db: Sequelize, timeoutSeconds: number) { return new Promise((resolve, reject) => { @@ -18,61 +23,62 @@ function checkHealth(db: Sequelize, timeoutSeconds: number) { }) } -function sleep(seconds: number) { - return new Promise((resolve) => setTimeout(resolve, seconds * 1000)) -} - export async function waitForDatabase({ - intervalSeconds = INTERVAL_SECONDS, - timeoutSeconds = TIMEOUT_SECONDS, - retries = RETRIES, - startPeriodSeconds = NODE_ENV === "test" ? 0 : START_PERIOD_SECONDS, + intervalSeconds = DB_HEALTH_CHECK_INTERVAL_SECONDS, + timeoutSeconds = DB_HEALTH_CHECK_TIMEOUT_SECONDS, + retries = DB_HEALTH_CHECK_RETRIES, + startPeriodSeconds = DB_HEALTH_CHECK_START_PERIOD_SECONDS, }: { intervalSeconds?: number timeoutSeconds?: number retries?: number startPeriodSeconds?: number } = {}): Promise { - let username: string - let database: string - if ( - NODE_ENV === "production" && - process.env.PRODUCTION_DATABASE_SA_MASTER_CREDS_AVAILABLE !== "true" - ) { - console.info( - "Falling back to local database credentials because production database sa:master credentials are not available." - ) - username = DB_USER - database = DB_NAME - } else { - username = "sa" // default user that should always exist - database = "master" // default database that should always exist - } + await sleep(startPeriodSeconds) - const db = new Sequelize({ - dialect: "mssql", - username, - database, - password: DB_PASS, - host: DB_HOST, - port: DB_PORT, - schema: "dbo", - logging: NODE_ENV === "development" ? console.log : false, - }) + console.info("Attempting direct to database connection...") + const databaseConfig = SEQUELIZE_CONFIG + let dbMigrationClient = new Sequelize(databaseConfig) + let isDatabaseSocketReady = false await sleep(startPeriodSeconds) for (let i = 0; i < retries; i++) { try { - await checkHealth(db, timeoutSeconds) - console.log("Database connection successful.") + await checkHealth(dbMigrationClient, timeoutSeconds) + console.info("Database connection successful.") return } catch (error) { - console.warn("Database connection failed, retrying...", error) - await sleep(intervalSeconds) + if (isSocketFailure(error)) { + console.info(`Database socket is not ready, retrying... ${error}`, { error }) + await sleep(intervalSeconds) + } else if (isNetworkFailure(error)) { + console.info(`Network error, retrying... ${error}`, { error }) + await sleep(intervalSeconds) + } else if (isCredentialFailure(error)) { + if (isDatabaseSocketReady) { + console.error(`Database connection failed due to invalid credentials: ${error}`, { + error, + }) + throw error + } else { + console.info( + "Falling back to database server-level connection (database might not exist)..." + ) + const serverLevelConfig = merge(SEQUELIZE_CONFIG, { database: "" }) + dbMigrationClient = new Sequelize(serverLevelConfig) + i -= 1 + isDatabaseSocketReady = true + continue + } + } else { + console.error(`Unknown database connection error: ${error}`, { error }) + throw error + } } } - throw new Error("Failed to connect to the database after retries.") + + throw new Error(`Failed to connect to the database due to timeout.`) } export default waitForDatabase diff --git a/api/src/initializers/10-create-database.ts b/api/src/initializers/10-create-database.ts deleted file mode 100644 index 0c4ac314..00000000 --- a/api/src/initializers/10-create-database.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { DB_NAME, DB_HOST, DB_PASS, DB_PORT, NODE_ENV } from "@/config" -import { QueryTypes, Sequelize } from "sequelize" - -async function databaseExists(db: Sequelize, databaseName: string): Promise { - const result = await db.query("SELECT 1 FROM sys.databases WHERE name = ?", { - type: QueryTypes.SELECT, - replacements: [databaseName], - }) - - return result.length > 0 -} - -async function createDatabase(): Promise { - if ( - NODE_ENV === "production" && - process.env.PRODUCTION_DATABASE_SA_MASTER_CREDS_AVAILABLE !== "true" - ) { - console.info( - "Skipping database creation initializer because production database sa:master credentials are not available." - ) - return - } - - const db = new Sequelize({ - dialect: "mssql", - username: "sa", // default user that should always exist - database: "master", // default database that should always exist - password: DB_PASS, - host: DB_HOST, - port: DB_PORT, - schema: "dbo", - logging: NODE_ENV === "development" ? console.log : false, - }) - - if (await databaseExists(db, DB_NAME)) return - - console.log(`Database ${DB_NAME} does not exist: creating...`) - await db.query(`CREATE DATABASE ${DB_NAME}`, { raw: true }).catch((error) => { - console.error("Failed to create database:", error) - }) - return -} - -export default createDatabase diff --git a/api/src/initializers/10-ensure-database.ts b/api/src/initializers/10-ensure-database.ts new file mode 100644 index 00000000..cbeec761 --- /dev/null +++ b/api/src/initializers/10-ensure-database.ts @@ -0,0 +1,70 @@ +import { QueryTypes, Sequelize } from "sequelize" +import { merge } from "lodash" + +import { DB_NAME } from "@/config" +import { SEQUELIZE_CONFIG } from "@/db/db-client" +import { isCredentialFailure } from "@/utils/db-error-helpers" + +async function databaseExists(db: Sequelize, databaseName: string): Promise { + const result = await db.query("SELECT 1 FROM sys.databases WHERE name = ?", { + type: QueryTypes.SELECT, + replacements: [databaseName], + }) + + return result.length > 0 +} + +async function ensureDatabase(): Promise { + console.info("Attempting direct to database connection to determine if database exists...") + const databaseConfig = SEQUELIZE_CONFIG + let dbMigrationClient = new Sequelize(databaseConfig) + let isCredentialFailureError = false + + try { + if (await databaseExists(dbMigrationClient, DB_NAME)) { + return true + } + } catch (error) { + if (isCredentialFailure(error)) { + isCredentialFailureError = true + console.info("Database connection failed due to invalid credential, retrying...") + } else { + console.error( + `Unknown connection failure, could not determine if database exists: ${error}`, + { + error, + } + ) + throw error + } + } + + if (isCredentialFailureError) { + console.info("Attempting server-level connection to determine if database exists...") + const serverLevelConfig = merge(SEQUELIZE_CONFIG, { database: "" }) + dbMigrationClient = new Sequelize(serverLevelConfig) + try { + if (await databaseExists(dbMigrationClient, DB_NAME)) { + return true + } + } catch (error) { + console.error( + `Could not determine if database exists database with server-level connection: ${error}`, + { error } + ) + throw error + } + } + + console.info(`Database ${DB_NAME} does not exist: creating...`) + try { + await dbMigrationClient.query(`CREATE DATABASE ${DB_NAME}`, { raw: true }) + } catch (error) { + console.error(`Failed to create database: ${error}`, { error }) + throw error + } + + return true +} + +export default ensureDatabase diff --git a/api/src/initializers/20-run-migrations.ts b/api/src/initializers/20-run-migrations.ts index 49dcf4be..b77389d8 100644 --- a/api/src/initializers/20-run-migrations.ts +++ b/api/src/initializers/20-run-migrations.ts @@ -2,6 +2,7 @@ import { migrator } from "@/db/umzug" export async function runMigrations(): Promise { await migrator.up() + console.info("All migrations completed successfully.") return } diff --git a/api/src/initializers/30-run-seeds.ts b/api/src/initializers/30-run-seeds.ts index 5240976f..7b2b1b1a 100644 --- a/api/src/initializers/30-run-seeds.ts +++ b/api/src/initializers/30-run-seeds.ts @@ -3,14 +3,21 @@ import { Centre } from "@/models" export async function runSeeds(): Promise { if (process.env.SKIP_SEEDING_UNLESS_EMPTY === "true") { - const count = await Centre.count() + const count = await Centre.count({ logging: false }) + if (count > 0) { - console.log("Skipping seeding as SKIP_SEEDING_UNLESS_EMPTY set, and data already seeded.") + console.warn("Skipping seeding as SKIP_SEEDING_UNLESS_EMPTY set, and data already seeded.") return } } - await seeder.up() + try { + await seeder.up() + } catch (error) { + console.error(`Error running seeds: ${error}`, { error }) + throw error + } + return } diff --git a/api/src/initializers/index.ts b/api/src/initializers/index.ts index dcd350b6..d7c767f7 100644 --- a/api/src/initializers/index.ts +++ b/api/src/initializers/index.ts @@ -6,29 +6,33 @@ const NON_INITIALIZER_REGEX = /^index\.(ts|js)$/ export async function importAndExecuteInitializers() { const files = await fs.readdir(__dirname) - return files.reduce(async (previousInitializerAction, file) => { - await previousInitializerAction - - if (NON_INITIALIZER_REGEX.test(file)) return + for (const file of files) { + if (NON_INITIALIZER_REGEX.test(file)) continue const modulePath = path.join(__dirname, file) - console.log(`Running initializer: ${modulePath}`) - - const { default: initializerAction } = await import(modulePath) - - return initializerAction().catch((error: unknown) => { - console.error(`Initialization error in ${modulePath}:`, error) - return Promise.reject(error) - }) - }, Promise.resolve()) + console.info(`Running initializer: ${modulePath}`) + + try { + const { default: initializerAction } = await require(modulePath) + await initializerAction() + } catch (error) { + console.error(`Failed to run initializer: ${modulePath}`, { error }) + throw error + } + } + + return true } if (require.main === module) { // TODO: add some kind of middleware that 503s? if initialization failed? - importAndExecuteInitializers() - .then(() => process.exit(0)) - .catch(() => { - console.log("Failed to complete initialization!") - return process.exit(0) - }) + ;(async () => { + try { + await importAndExecuteInitializers() + } catch { + console.error("Failed to complete initialization!") + } + + process.exit(0) + })() } diff --git a/api/src/middleware/authz.middleware.ts b/api/src/middleware/authz.middleware.ts index 08f7dcfa..eb1a3ec9 100644 --- a/api/src/middleware/authz.middleware.ts +++ b/api/src/middleware/authz.middleware.ts @@ -8,6 +8,10 @@ import { User } from "@/models" import { UserStatus } from "@/models/user" import auth0Integration from "@/integrations/auth0-integration" +export type AuthorizedRequest = Request & { + user: User +} + export const checkJwt = jwt({ secret: jwksRsa.expressJwtSecret({ cache: true, @@ -21,7 +25,7 @@ export const checkJwt = jwt({ algorithms: ["RS256"], }) -function isAuthenticatedRequest(req: Request): req is Request & { user: User } { +function isAuthenticatedRequest(req: Request): req is AuthorizedRequest & { user: User } { // TODO: check if this should also check user.status or any other fields const user = req.user if (user instanceof User && !isNil(user.sub) && user.status === UserStatus.ACTIVE) { diff --git a/api/src/models/fiscal-period.ts b/api/src/models/fiscal-period.ts index b42ff494..ddfc4fe8 100644 --- a/api/src/models/fiscal-period.ts +++ b/api/src/models/fiscal-period.ts @@ -8,13 +8,31 @@ import { import sequelize from "@/db/db-client" +/** Keep in sync with web/src/api/fiscal-periods-api.ts */ +export enum FiscalPeriodMonths { + APRIL = "april", + MAY = "may", + JUNE = "june", + JULY = "july", + AUGUST = "august", + SEPTEMBER = "september", + OCTOBER = "october", + NOVEMBER = "november", + DECEMBER = "december", + JANUARY = "january", + FEBRUARY = "february", + MARCH = "march", +} + export class FiscalPeriod extends Model< InferAttributes, InferCreationAttributes > { + static readonly Months = FiscalPeriodMonths + declare id: CreationOptional declare fiscalYear: string - declare month: string + declare month: FiscalPeriodMonths declare dateStart: Date declare dateEnd: Date declare createdAt: CreationOptional @@ -36,6 +54,12 @@ FiscalPeriod.init( month: { type: DataTypes.STRING(10), allowNull: false, + validate: { + isIn: { + args: [Object.values(FiscalPeriodMonths)], + msg: `Month must be one of: ${Object.values(FiscalPeriodMonths).join(", ")}`, + }, + }, }, dateStart: { type: DataTypes.DATE, diff --git a/api/src/models/funding-submission-line-json.ts b/api/src/models/funding-submission-line-json.ts index ae600c07..037b9c39 100644 --- a/api/src/models/funding-submission-line-json.ts +++ b/api/src/models/funding-submission-line-json.ts @@ -24,6 +24,7 @@ export class FundingSubmissionLineJson extends BaseModel< > { declare id: CreationOptional declare centreId: ForeignKey + // TODO: link this to a fiscal period, and remove fiscalYear, dateName, dateStart, and dateEnd declare fiscalYear: string declare dateName: string declare dateStart: Date diff --git a/api/src/utils/db-error-helpers.ts b/api/src/utils/db-error-helpers.ts new file mode 100644 index 00000000..a6a96e3b --- /dev/null +++ b/api/src/utils/db-error-helpers.ts @@ -0,0 +1,21 @@ +import { has } from "lodash" + +export function isCredentialFailure(error: unknown) { + return ( + error instanceof Error && + ((has(error, "code") && error.code === "ELOGIN") || + error.message.includes("Login failed for user")) + ) +} + +export function isSocketFailure(error: unknown) { + return error instanceof Error && has(error, "code") && error.code === "ESOCKET" +} + +export function isNetworkFailure(error: unknown) { + return ( + error instanceof Error && + ((has(error, "code") && error.code === "EAI_AGAIN") || + error.message.includes("getaddrinfo EAI_AGAIN")) + ) +} diff --git a/api/src/utils/sleep.ts b/api/src/utils/sleep.ts new file mode 100644 index 00000000..2744dbd1 --- /dev/null +++ b/api/src/utils/sleep.ts @@ -0,0 +1,5 @@ +function sleep(seconds: number) { + return new Promise((resolve) => setTimeout(resolve, seconds * 1000)) +} + +export default sleep diff --git a/bin/dev b/bin/dev index 073c96df..a3258d7c 100755 --- a/bin/dev +++ b/bin/dev @@ -1,17 +1,21 @@ #!/usr/bin/env ruby -require_relative './jira_api' +require_relative "./jira_api" +require_relative "./pull-request-editor" class DevHelper # Support dashes in command names COMMAND_TO_METHOD = { "ts-node" => :ts_node, "branch-from" => :branch_from, + "description-from" => :description_from, + "check-types" => :check_types, + "edit-pr" => :edit_pr, } METHOD_TO_COMMAND = COMMAND_TO_METHOD.invert - REPLACE_PROCESS = 'replace_process' - WAIT_FOR_PROCESS = 'wait_for_process' + REPLACE_PROCESS = "replace_process" + WAIT_FOR_PROCESS = "wait_for_process" # External Interface def self.call(*args) @@ -63,17 +67,18 @@ class DevHelper end # Custom helpers + + def check_types(*args, **kwargs) + run(*%w[api npm run check-types], *args, **kwargs) + end + def sh(*args, **kwargs) - run( - *%w[api sh], - *args, - **kwargs - ) + run(*%w[api sh], *args, **kwargs) end def debug - api_container_id = container_id('api') - puts 'Waiting for breakpoint to trigger...' + api_container_id = container_id("api") + puts "Waiting for breakpoint to trigger..." puts "'ctrl-c' to exit." command = "docker attach --detach-keys ctrl-c #{api_container_id}" puts "Running: #{command}" @@ -111,7 +116,7 @@ class DevHelper if RUBY_PLATFORM =~ /linux/ run(*%w[api npm run migrate], *args, execution_mode: WAIT_FOR_PROCESS, **kwargs) - file_or_directory = 'api/src/db/migrations' + file_or_directory = "api/src/db/migrations" exit(0) unless take_over_needed?(file_or_directory) ownit file_or_directory @@ -124,7 +129,7 @@ class DevHelper if RUBY_PLATFORM =~ /linux/ run(*%w[api npm run seed], *args, execution_mode: WAIT_FOR_PROCESS, **kwargs) - file_or_directory = 'api/src/db/seeds' + file_or_directory = "api/src/db/seeds" exit(0) unless take_over_needed?(file_or_directory) ownit file_or_directory @@ -135,29 +140,24 @@ class DevHelper # -I enable quoted identifiers, e.g. "table"."column" def sqlcmd(*args, **kwargs) # rubocop:disable Metrics/MethodLength - environment_hoist = ENV.slice(*%w[ - DB_NAME - DB_USER - DB_HOST - DB_PORT - ]).map { |key, value| - "export #{key}=\"#{value}\"" - }.join("\n ") - - compose( - <<~BASH, + environment_hoist = + ENV + .slice(*%w[DB_NAME DB_USER DB_HOST DB_PORT]) + .map { |key, value| "export #{key}=\"#{value}\"" } + .join("\n ") + + compose(<<~BASH, **kwargs) exec db sh -c ' #{environment_hoist} - /opt/mssql-tools/bin/sqlcmd \ + /opt/mssql-tools18/bin/sqlcmd \ -U "$DB_USER" \ -P "$DB_PASS" \ -H "$DB_HOST" \ -d "$DB_NAME" \ + -C \ -I #{args.join(" ")} ' BASH - **kwargs - ) end def db(*args, **kwargs) @@ -188,12 +188,32 @@ class DevHelper system("git checkout -b #{branch_name}") end + ## + # Fetches the description of a Jira issue and prints it to the console in markdown format. + # Example: + # dev description-from https://yg-hpw.atlassian.net/browse/ELCC-61 + # + # Produces: + # ... a bunch of markdown text ... + def description_from(jira_issue_url, *args, **kwargs) + description = JiraApi.fetch_description_as_markdown(jira_issue_url) + puts description + end + + ## + # Edits the description of a pull request. + # Example: + # dev edit-pr https://github.com/icefoganalytics/travel-authorization/pull/218 + def edit_pr(pull_request_url, *args, **kwargs) + PullRequestEditor.edit_pull_request_description(pull_request_url, *args, **kwargs) + exit(0) + end + def bash_completions - completions = public_methods(false).reject { |word| - %i[call].include?(word) - }.map { |word| - METHOD_TO_COMMAND.fetch(word, word) - } + completions = + public_methods(false) + .reject { |word| %i[call].include?(word) } + .map { |word| METHOD_TO_COMMAND.fetch(word, word) } puts completions end @@ -208,8 +228,8 @@ class DevHelper end def compose_command(*args, **kwargs) - environment = kwargs.fetch(:environment, 'development') - "cd #{project_root} && docker compose -f docker-compose.#{environment}.yaml #{args.join(' ')}" + environment = kwargs.fetch(:environment, "development") + "cd #{project_root} && docker compose -f docker-compose.#{environment}.yaml #{args.join(" ")}" end def project_root @@ -217,18 +237,23 @@ class DevHelper end def take_over_needed?(file_or_directory) - files_owned_by_others = system("find #{file_or_directory} -not -user #{user_id} -print -quit | grep -q .") + files_owned_by_others = + system("find #{file_or_directory} -not -user #{user_id} -print -quit | grep -q .") files_owned_by_others end def user_id - raise NotImplementedError, "Not implement for platform #{RUBY_PLATFORM}" unless RUBY_PLATFORM =~ /linux/ + unless RUBY_PLATFORM =~ /linux/ + raise NotImplementedError, "Not implement for platform #{RUBY_PLATFORM}" + end `id -u`.strip end def group_id - raise NotImplementedError, "Not implement for platform #{RUBY_PLATFORM}" unless RUBY_PLATFORM =~ /linux/ + unless RUBY_PLATFORM =~ /linux/ + raise NotImplementedError, "Not implement for platform #{RUBY_PLATFORM}" + end `id -g`.strip end diff --git a/bin/jira_api.rb b/bin/jira_api.rb index 77493dcb..68c61eed 100644 --- a/bin/jira_api.rb +++ b/bin/jira_api.rb @@ -2,6 +2,8 @@ require 'json' require 'uri' +require_relative "./prose_mirror_to_json.rb" + class JiraApi JIRA_USERNAME = ENV['JIRA_USERNAME'] JIRA_API_TOKEN = ENV['JIRA_API_TOKEN'] @@ -14,17 +16,30 @@ def self.build_branch_name(jira_ticket_url) end issue_key = extract_issue_key(jira_ticket_url) - issue_title = fetch_issue_title(issue_key) + issue_data = fetch_issue_data(issue_key) + issue_title = extract_issue_title(issue_data) format_branch_name(issue_key, issue_title) end + def self.fetch_description_as_markdown(jira_ticket_url) + if JIRA_USERNAME.nil? || JIRA_API_TOKEN.nil? + puts 'Please set JIRA_USERNAME and JIRA_API_TOKEN environment variables' + return + end + + issue_key = extract_issue_key(jira_ticket_url) + issue_data = fetch_issue_data(issue_key) + issue_description = extract_issue_description(issue_data) + prose_mirror_to_markdown(issue_description) + end + private def self.extract_issue_key(url) url.match(%r{/browse/([A-Z]+-\d+)})[1] end - def self.fetch_issue_title(issue_key) + def self.fetch_issue_data(issue_key) uri = URI("#{JIRA_SITE}/rest/api/3/issue/#{issue_key}") request = Net::HTTP::Get.new(uri) request.basic_auth(JIRA_USERNAME, JIRA_API_TOKEN) @@ -33,12 +48,23 @@ def self.fetch_issue_title(issue_key) http.request(request) end - data = JSON.parse(response.body) - data['fields']['summary'] + JSON.parse(response.body) + end + + def self.extract_issue_title(issue_data) + issue_data['fields']['summary'] + end + + def self.extract_issue_description(issue_data) + issue_data['fields']['description'] end def self.format_branch_name(issue_key, issue_title) formatted_title = issue_title.downcase.gsub(/\s+/, '-').gsub(/[^a-z0-9\-]/, '') "#{issue_key.downcase}/#{formatted_title}" end + + def self.prose_mirror_to_markdown(description) + ProseMirrorToMarkdown.call(description) + end end diff --git a/bin/prose_mirror_to_json.rb b/bin/prose_mirror_to_json.rb new file mode 100644 index 00000000..5f62e4a9 --- /dev/null +++ b/bin/prose_mirror_to_json.rb @@ -0,0 +1,128 @@ +require 'json' + +## +# Example usage: +# +# Suppose 'prosemirror_json' is the JSON data you've provided. +# +# Load the JSON data +# prosemirror_json = DownloadJiraDescription.call(jira_ticket_url) +# +# markdown = ProseMirrorToMarkdown.call(prosemirror_json) +# puts markdown +class ProseMirrorToMarkdown + def self.call(prosemirror_json) + new(prosemirror_json).to_markdown + end + + def initialize(prosemirror_json) + @doc = prosemirror_json + end + + def to_markdown + process_nodes(@doc['content']).join("\n") + end + + private + + def process_nodes(nodes) + nodes.flat_map { |node| process_node(node) } + end + + def process_node(node) + case node['type'] + when 'doc' + process_nodes(node['content']) + when 'paragraph' + process_paragraph(node) + when 'heading' + process_heading(node) + when 'blockquote' + process_blockquote(node) + when 'orderedList' + process_ordered_list(node) + when 'bulletList' + process_bullet_list(node) + when 'listItem' + process_list_item(node) + when 'text' + process_text(node) + else + # For any other nodes, process their content if they have any + if node['content'] + process_nodes(node['content']) + else + [] + end + end + end + + def process_paragraph(node) + [process_inline_content(node['content'])] + end + + def process_heading(node) + level = node['attrs']['level'] + content = process_inline_content(node['content']) + ["\n#{'#' * level} #{content}\n"] + end + + def process_blockquote(node) + content = process_nodes(node['content']).map { |line| "> #{line}" }.join("\n") + ["#{content}\n"] + end + + def process_ordered_list(node) + start_number = node['attrs']['order'] || 1 + items = node['content'] + process_list_items(items, ordered: true, start_number: start_number) + end + + def process_bullet_list(node) + items = node['content'] + process_list_items(items, ordered: false) + end + + def process_list_items(items, ordered:, start_number: 1) + counter = start_number + items.flat_map do |item| + lines = process_nodes(item['content']) + prefix = ordered ? "#{counter}. " : "- " + counter += 1 + lines.map.with_index do |line, idx| + if idx == 0 + "#{prefix}#{line}" + else + " #{line}" + end + end + end + end + + def process_text(node) + text = node['text'] + if node['marks'] + node['marks'].each do |mark| + case mark['type'] + when 'bold' + text = "**#{text}**" + when 'italic' + text = "*#{text}*" + when 'code' + text = "`#{text}`" + when 'link' + href = mark['attrs']['href'] + text = "[#{text}](#{href})" + when 'strike' + text = "~~#{text}~~" + # Add more mark types as needed + end + end + end + text + end + + def process_inline_content(content) + content.map { |node| process_node(node) }.join + end +end diff --git a/bin/pull-request-editor.rb b/bin/pull-request-editor.rb new file mode 100644 index 00000000..d90970b7 --- /dev/null +++ b/bin/pull-request-editor.rb @@ -0,0 +1,78 @@ +require "tempfile" +require "open3" + +## +# Supports fetching and editing PR descriptions from a full GitHub PR URL using SSH and GitHub CLI. +# +# Example usage: +# - PullRequestEditor.edit_pull_request_description('https://github.com/icefoganalytics/travel-authorization/pull/218') +class PullRequestEditor + # Edits the pull request description using GitHub CLI and VS Code + def self.edit_pull_request_description(pull_request_url) + if ENV["GITHUB_TOKEN"].nil? + puts "\nPlease set the GITHUB_TOKEN environment variable." + exit(1) + end + + repo, pull_request_number = extract_repo_and_pull_request_number(pull_request_url) + + pull_request_body = fetch_pull_request_body(repo, pull_request_number) + + tmp_dir = File.join(Dir.home, "tmp") + Dir.mkdir(tmp_dir) unless Dir.exist?(tmp_dir) + + Tempfile.create(["pull_request_description_#{pull_request_number}", ".md"], tmp_dir) do |file| + file.write(pull_request_body) + file.flush + + system("code --wait #{file.path}") + + updated_pull_request_body = File.read(file.path) + + if updated_pull_request_body.strip != pull_request_body.strip + update_pull_request_body(repo, pull_request_number, file.path) + else + puts "No changes made to the PR description." + end + end + end + + private + + # Extracts the repository name and PR number from a full GitHub PR URL + def self.extract_repo_and_pull_request_number(pull_request_url) + match_data = pull_request_url.match(%r{github.com/([^/]+)/([^/]+)/pull/(\d+)}) + repo = "#{match_data[1]}/#{match_data[2]}" + pull_request_number = match_data[3] + [repo, pull_request_number] + end + + # Fetches the PR body using the GitHub CLI + def self.fetch_pull_request_body(repo, pull_request_number) + command = "gh pr view #{pull_request_number} --repo #{repo} --json body --jq .body" + puts "running: #{command}" + stdout, stderr, status = Open3.capture3(command) + + if status.success? + stdout.strip + else + puts "Error fetching PR description: #{stderr}" + exit(1) + end + end + + # Updates the PR body using the GitHub CLI + def self.update_pull_request_body(repo, pull_request_number, new_body_file_path) + command = + "gh pr edit #{pull_request_number} --repo #{repo} --body-file \"#{new_body_file_path}\"" + puts "running: #{command}" + stdout, stderr, status = Open3.capture3(command) + + if status.success? + puts "stdout: #{stdout}" + puts "PR description updated successfully." + else + puts "Error updating PR description: #{stderr}" + end + end +end diff --git a/docker-compose.development.yaml b/docker-compose.development.yaml index 71717969..6f3c3ff3 100644 --- a/docker-compose.development.yaml +++ b/docker-compose.development.yaml @@ -1,5 +1,3 @@ -version: "3" - x-default-environment: &default-environment NODE_ENV: development TZ: "UTC" @@ -8,6 +6,10 @@ x-default-environment: &default-environment DB_NAME: elcc_development DB_PASS: &default-db-password DevPwd99! DB_PORT: &default-db-port 1433 + DB_HEALTH_CHECK_INTERVAL_SECONDS: 5 + DB_HEALTH_CHECK_TIMEOUT_SECONDS: 10 + DB_HEALTH_CHECK_RETRIES: 3 + DB_HEALTH_CHECK_START_PERIOD_SECONDS: 5 services: api: @@ -29,8 +31,7 @@ services: - ./.gitignore:/usr/src/.gitignore - ./.prettierrc.yaml:/usr/src/.prettierrc.yaml depends_on: - db: - condition: service_healthy + - db web: build: @@ -59,12 +60,12 @@ services: <<: *default-environment NODE_ENV: test DB_NAME: elcc_test + DB_HEALTH_CHECK_START_PERIOD_SECONDS: 0 tty: true volumes: - ./api:/usr/src/api depends_on: - db: - condition: service_healthy + - db test_web: build: @@ -79,7 +80,7 @@ services: - ./web:/usr/src/web db: - image: mcr.microsoft.com/mssql/server:2019-latest + image: mcr.microsoft.com/mssql/server:2019-CU28-ubuntu-20.04 user: root environment: <<: *default-environment @@ -90,22 +91,6 @@ services: - "1433:1433" volumes: - db_data:/var/opt/mssql/data - healthcheck: - # Ensures that server is started and that "ELCC" database is in an "online" state. - # https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-databases-transact-sql?view=sql-server-2017 - test: | - sh -c ' - /opt/mssql-tools/bin/sqlcmd \ - -h -1 \ - -t 1 \ - -U sa \ - -P "$$MSSQL_SA_PASSWORD" \ - -Q "SET NOCOUNT ON; Select SUM(state) from sys.databases WHERE name = '$$DB_NAME'" - ' - interval: 5s - timeout: 10s - retries: 3 - start_period: 5s # For easily generating large PlantUML diagrams # Not relevant to production environment. diff --git a/web/.eslint.config.js b/web/.eslintrc.cjs similarity index 78% rename from web/.eslint.config.js rename to web/.eslintrc.cjs index 4342b325..959f3700 100644 --- a/web/.eslint.config.js +++ b/web/.eslintrc.cjs @@ -1,5 +1,5 @@ // https://github.com/typescript-eslint/typescript-eslint/issues/251 -export default { +module.exports = { root: true, env: { browser: true, @@ -23,5 +23,13 @@ export default { sourceType: "module", }, plugins: ["vue", "@typescript-eslint"], - rules: {}, + rules: { + // Override/add rules' settings here + "vue/valid-v-slot": [ + "error", + { + allowModifiers: true, + }, + ], + }, } diff --git a/web/package-lock.json b/web/package-lock.json index 45837244..5c363d22 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@auth0/auth0-vue": "^2.0.0", "@mdi/font": "^7.1.96", + "@vueuse/router": "^11.1.0", "apexcharts": "^3.37.1", "axios": "^1.3.3", "lodash": "^4.17.21", @@ -27,18 +28,19 @@ "@types/lodash": "^4.14.191", "@types/luxon": "^3.3.7", "@types/qs": "^6.9.10", - "@typescript-eslint/eslint-plugin": "^6.6.0", - "@typescript-eslint/parser": "^6.6.0", - "@vitejs/plugin-vue": "^5.0.2", - "eslint": "^8.48.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-vue": "^9.17.0", - "prettier": "^3.0.3", - "typescript": "^4.9.3", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "@vitejs/plugin-vue": "^5.1.4", + "eslint": "^8.57.1", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-vue": "^9.28.0", + "prettier": "^3.3.3", + "typescript": "^5.6.2", "vite": "^5.0.10", - "vite-plugin-vuetify": "^2.0.1", + "vite-plugin-vuetify": "^2.0.4", "vitest": "^1.1.1", - "vue-tsc": "^1.0.11" + "vue-tsc": "^2.1.6" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -72,10 +74,29 @@ } } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/parser": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz", - "integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.7.tgz", + "integrity": "sha512-aZn7ETtQsjjGG5HruveUK06cU3Hljuhd9Iojm4M8WWv3wLE6OkE5PWbDUkItmMgegmccaITudyuW5RPYrYlgWw==", + "dependencies": { + "@babel/types": "^7.25.7" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -83,6 +104,19 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/types": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.7.tgz", + "integrity": "sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ==", + "dependencies": { + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.19.11", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz", @@ -467,18 +501,18 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.0.tgz", - "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==", + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -521,22 +555,23 @@ } }, "node_modules/@eslint/js": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.48.0.tgz", - "integrity": "sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", - "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", "minimatch": "^3.0.5" }, "engines": { @@ -579,9 +614,10 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", "dev": true }, "node_modules/@jest/schemas": { @@ -641,6 +677,18 @@ "node": ">= 8" } }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.9.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.2.tgz", @@ -817,9 +865,9 @@ "dev": true }, "node_modules/@types/json-schema": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", - "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "node_modules/@types/lodash": { @@ -841,22 +889,22 @@ "dev": true }, "node_modules/@types/semver": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.1.tgz", - "integrity": "sha512-cJRQXpObxfNKkFAZbJl2yjWtJCqELQIdShsogr1d2MilP8dKD9TE/nEKHkJgUNHdGKCQaf9HbIynuV2csLGVLg==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.6.0.tgz", - "integrity": "sha512-CW9YDGTQnNYMIo5lMeuiIG08p4E0cXrXTbcZ2saT/ETE7dWUrNxlijsQeU04qAAKkILiLzdQz+cGFxCJjaZUmA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.2.0.tgz", + "integrity": "sha512-mdekAHOqS9UjlmyF/LSs6AIEvfceV749GFxoBAjwAv0nkevfKHWQFDMcBZWUiIC5ft6ePWivXoS36aKQ0Cy3sw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.6.0", - "@typescript-eslint/type-utils": "6.6.0", - "@typescript-eslint/utils": "6.6.0", - "@typescript-eslint/visitor-keys": "6.6.0", + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/type-utils": "7.2.0", + "@typescript-eslint/utils": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -872,8 +920,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -881,31 +929,16 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/parser": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.6.0.tgz", - "integrity": "sha512-setq5aJgUwtzGrhW177/i+DMLqBaJbdwGj2CPIVFFLE0NCliy5ujIdLHd2D1ysmlmsjdL2GWW+hR85neEfc12w==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz", + "integrity": "sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.6.0", - "@typescript-eslint/types": "6.6.0", - "@typescript-eslint/typescript-estree": "6.6.0", - "@typescript-eslint/visitor-keys": "6.6.0", + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/typescript-estree": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", "debug": "^4.3.4" }, "engines": { @@ -916,7 +949,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -925,13 +958,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.6.0.tgz", - "integrity": "sha512-pT08u5W/GT4KjPUmEtc2kSYvrH8x89cVzkA0Sy2aaOUIw6YxOIjA8ilwLr/1fLjOedX1QAuBpG9XggWqIIfERw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz", + "integrity": "sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.6.0", - "@typescript-eslint/visitor-keys": "6.6.0" + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -942,13 +975,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.6.0.tgz", - "integrity": "sha512-8m16fwAcEnQc69IpeDyokNO+D5spo0w1jepWWY2Q6y5ZKNuj5EhVQXjtVAeDDqvW6Yg7dhclbsz6rTtOvcwpHg==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.2.0.tgz", + "integrity": "sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.6.0", - "@typescript-eslint/utils": "6.6.0", + "@typescript-eslint/typescript-estree": "7.2.0", + "@typescript-eslint/utils": "7.2.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -960,7 +993,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -969,9 +1002,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.6.0.tgz", - "integrity": "sha512-CB6QpJQ6BAHlJXdwUmiaXDBmTqIE2bzGTDLADgvqtHWuhfNP3rAOK7kAgRMAET5rDRr9Utt+qAzRBdu3AhR3sg==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz", + "integrity": "sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -982,16 +1015,17 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.6.0.tgz", - "integrity": "sha512-hMcTQ6Al8MP2E6JKBAaSxSVw5bDhdmbCEhGW/V8QXkb9oNsFkA4SBuOMYVPxD3jbtQ4R/vSODBsr76R6fP3tbA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz", + "integrity": "sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.6.0", - "@typescript-eslint/visitor-keys": "6.6.0", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", + "minimatch": "9.0.3", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" }, @@ -1008,33 +1042,18 @@ } } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/utils": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.6.0.tgz", - "integrity": "sha512-mPHFoNa2bPIWWglWYdR0QfY9GN0CfvvXX1Sv6DlSTive3jlMTUy+an67//Gysc+0Me9pjitrq0LJp0nGtLgftw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.2.0.tgz", + "integrity": "sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.6.0", - "@typescript-eslint/types": "6.6.0", - "@typescript-eslint/typescript-estree": "6.6.0", + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/typescript-estree": "7.2.0", "semver": "^7.5.4" }, "engines": { @@ -1045,31 +1064,16 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "eslint": "^8.56.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.6.0.tgz", - "integrity": "sha512-L61uJT26cMOfFQ+lMZKoJNbAEckLe539VhTxiGHrWl5XSKQgA0RTBZJW2HFPy5T0ZvPVSD93QsrTKDkfNwJGyQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz", + "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.6.0", + "@typescript-eslint/types": "7.2.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -1080,10 +1084,16 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/@vitejs/plugin-vue": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.2.tgz", - "integrity": "sha512-kEjJHrLb5ePBvjD0SPZwJlw1QTRcjjCA9sB5VyfonoXVBxTS7TMnqL6EkLt1Eu61RDeiuZ/WN9Hf6PxXhPI2uA==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.1.4.tgz", + "integrity": "sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==", "dev": true, "engines": { "node": "^18.0.0 || >=20.0.0" @@ -1162,57 +1172,29 @@ } }, "node_modules/@volar/language-core": { - "version": "1.0.24", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.0.24.tgz", - "integrity": "sha512-vTN+alJiWwK0Pax6POqrmevbtFW2dXhjwWiW/MW4f48eDYPLdyURWcr8TixO7EN/nHsUBj2udT7igFKPtjyAKg==", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.5.tgz", + "integrity": "sha512-F4tA0DCO5Q1F5mScHmca0umsi2ufKULAnMOVBfMsZdT4myhVl4WdKRwCaKcfOkIEuyrAVvtq1ESBdZ+rSyLVww==", "dev": true, "dependencies": { - "@volar/source-map": "1.0.24", - "muggle-string": "^0.1.0" + "@volar/source-map": "2.4.5" } }, "node_modules/@volar/source-map": { - "version": "1.0.24", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.0.24.tgz", - "integrity": "sha512-Qsv/tkplx18pgBr8lKAbM1vcDqgkGKQzbChg6NW+v0CZc3G7FLmK+WrqEPzKlN7Cwdc6XVL559Nod8WKAfKr4A==", - "dev": true, - "dependencies": { - "muggle-string": "^0.1.0" - } + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.5.tgz", + "integrity": "sha512-varwD7RaKE2J/Z+Zu6j3mNNJbNT394qIxXwdvz/4ao/vxOfyClZpSDtLKkwWmecinkOVos5+PWkWraelfMLfpw==", + "dev": true }, "node_modules/@volar/typescript": { - "version": "1.0.24", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.0.24.tgz", - "integrity": "sha512-f8hCSk+PfKR1/RQHxZ79V1NpDImHoivqoizK+mstphm25tn/YJ/JnKNjZHB+o21fuW0yKlI26NV3jkVb2Cc/7A==", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.5.tgz", + "integrity": "sha512-mcT1mHvLljAEtHviVcBuOyAwwMKz1ibXTi5uYtP/pf4XxoAzpdkQ+Br2IC0NPCvLCbjPZmbf3I0udndkfB1CDg==", "dev": true, "dependencies": { - "@volar/language-core": "1.0.24" - } - }, - "node_modules/@volar/vue-language-core": { - "version": "1.0.24", - "resolved": "https://registry.npmjs.org/@volar/vue-language-core/-/vue-language-core-1.0.24.tgz", - "integrity": "sha512-2NTJzSgrwKu6uYwPqLiTMuAzi7fAY3yFy5PJ255bGJc82If0Xr+cW8pC80vpjG0D/aVLmlwAdO4+Ya2BI8GdDg==", - "dev": true, - "dependencies": { - "@volar/language-core": "1.0.24", - "@volar/source-map": "1.0.24", - "@vue/compiler-dom": "^3.2.45", - "@vue/compiler-sfc": "^3.2.45", - "@vue/reactivity": "^3.2.45", - "@vue/shared": "^3.2.45", - "minimatch": "^5.1.1", - "vue-template-compiler": "^2.7.14" - } - }, - "node_modules/@volar/vue-typescript": { - "version": "1.0.24", - "resolved": "https://registry.npmjs.org/@volar/vue-typescript/-/vue-typescript-1.0.24.tgz", - "integrity": "sha512-9a25oHDvGaNC0okRS47uqJI6FxY4hUQZUsxeOUFHcqVxZEv8s17LPuP/pMMXyz7jPygrZubB/qXqHY5jEu/akA==", - "dev": true, - "dependencies": { - "@volar/typescript": "1.0.24", - "@volar/vue-language-core": "1.0.24" + "@volar/language-core": "2.4.5", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" } }, "node_modules/@vue/compiler-core": { @@ -1261,11 +1243,74 @@ "@vue/shared": "3.3.11" } }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, "node_modules/@vue/devtools-api": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.0.tgz", "integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==" }, + "node_modules/@vue/language-core": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.1.6.tgz", + "integrity": "sha512-MW569cSky9R/ooKMh6xa2g1D0AtRKbL56k83dzus/bx//RDJk24RHWkMzbAlXjMdDNyxAaagKPRquBIxkxlCkg==", + "dev": true, + "dependencies": { + "@volar/language-core": "~2.4.1", + "@vue/compiler-dom": "^3.4.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.4.0", + "computeds": "^0.0.1", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core/node_modules/@vue/compiler-core": { + "version": "3.5.11", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.11.tgz", + "integrity": "sha512-PwAdxs7/9Hc3ieBO12tXzmTD+Ln4qhT/56S+8DvrrZ4kLDn4Z/AMUr8tXJD0axiJBS0RKIoNaR0yMuQB9v9Udg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.11", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/language-core/node_modules/@vue/compiler-dom": { + "version": "3.5.11", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.11.tgz", + "integrity": "sha512-pyGf8zdbDDRkBrEzf8p7BQlMKNNF5Fk/Cf/fQ6PiUz9at4OaUfyXW0dGJTo2Vl1f5U9jSLCNf0EZJEogLXoeew==", + "dev": true, + "dependencies": { + "@vue/compiler-core": "3.5.11", + "@vue/shared": "3.5.11" + } + }, + "node_modules/@vue/language-core/node_modules/@vue/shared": { + "version": "3.5.11", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.11.tgz", + "integrity": "sha512-W8GgysJVnFo81FthhzurdRAWP/byq3q2qIw70e0JWblzVhjgOMiC2GyovXrZTFQJnFVryYaKGP3Tc9vYzYm6PQ==", + "dev": true + }, "node_modules/@vue/reactivity": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.11.tgz", @@ -1323,9 +1368,9 @@ "integrity": "sha512-u2G8ZQ9IhMWTMXaWqZycnK4UthG1fA238CD+DP4Dm4WJi5hdUKKLg0RMRaRpDPNMdkTwIDkp7WtD0Rd9BH9fLw==" }, "node_modules/@vuetify/loader-shared": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@vuetify/loader-shared/-/loader-shared-2.0.1.tgz", - "integrity": "sha512-zy5/ohEO7RcJaWYu2Xiy8TBEOkTb42XvWvSAJwXAtY8OlwqyGhzzBp9OvMVjLGIuFXumBpXKlsaVIkeN0OWWSw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@vuetify/loader-shared/-/loader-shared-2.0.3.tgz", + "integrity": "sha512-Ss3GC7eJYkp2SF6xVzsT7FAruEmdihmn4OCk2+UocREerlXKWgOKKzTN5PN3ZVN5q05jHHrsNhTuWbhN61Bpdg==", "devOptional": true, "dependencies": { "upath": "^2.0.1" @@ -1335,6 +1380,82 @@ "vuetify": "^3.0.0" } }, + "node_modules/@vueuse/router": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/router/-/router-11.1.0.tgz", + "integrity": "sha512-OjTNIOzX5jD1HDzmDApMke2QsCZ+gWKaydfndKJ3j9ttn41Pr11cQbSY6ZBp+bNjctAR+jhQBV/DGtL3iKpuHg==", + "dependencies": { + "@vueuse/shared": "11.1.0", + "vue-demi": ">=0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue-router": ">=4.0.0-rc.1" + } + }, + "node_modules/@vueuse/router/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/shared": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.1.0.tgz", + "integrity": "sha512-YUtIpY122q7osj+zsNMFAfMTubGz0sn5QzE5gPzAIiCmtt2ha3uQUY1+JPyL4gRCTsLPX82Y9brNbo/aqlA91w==", + "dependencies": { + "vue-demi": ">=0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -1479,12 +1600,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1596,6 +1717,12 @@ "node": ">= 0.8" } }, + "node_modules/computeds": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz", + "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", + "dev": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1728,6 +1855,18 @@ "node": ">=6.0.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/esbuild": { "version": "0.19.11", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz", @@ -1779,18 +1918,19 @@ } }, "node_modules/eslint": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.48.0.tgz", - "integrity": "sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.48.0", - "@humanwhocodes/config-array": "^0.11.10", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -1833,9 +1973,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", - "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, "bin": { "eslint-config-prettier": "bin/cli.js" @@ -1844,40 +1984,56 @@ "eslint": ">=7.0.0" } }, - "node_modules/eslint-plugin-vue": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.17.0.tgz", - "integrity": "sha512-r7Bp79pxQk9I5XDP0k2dpUC7Ots3OSWgvGZNu3BxmKK6Zg7NgVtcOB6OCna5Kb9oQwJPl5hq183WD0SY5tZtIQ==", + "node_modules/eslint-plugin-prettier": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", "dev": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "natural-compare": "^1.4.0", - "nth-check": "^2.1.1", - "postcss-selector-parser": "^6.0.13", - "semver": "^7.5.4", - "vue-eslint-parser": "^9.3.1", - "xml-name-validator": "^4.0.0" + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" }, "engines": { - "node": "^14.17.0 || >=16.0.0" + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" }, "peerDependencies": { - "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0" + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } } }, - "node_modules/eslint-plugin-vue/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "node_modules/eslint-plugin-vue": { + "version": "9.28.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.28.0.tgz", + "integrity": "sha512-ShrihdjIhOTxs+MfWun6oJWuk+g/LAhN+CiuOl/jjkG3l0F2AuK5NMTaWqyvBgkFtpYmyks6P4603mLmhNJW8g==", "dev": true, "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" + "@eslint-community/eslint-utils": "^4.4.0", + "globals": "^13.24.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^6.0.15", + "semver": "^7.6.3", + "vue-eslint-parser": "^9.4.3", + "xml-name-validator": "^4.0.0" }, "engines": { - "node": ">=10" + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" } }, "node_modules/eslint-scope": { @@ -2084,10 +2240,16 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -2146,9 +2308,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -2327,9 +2489,9 @@ } }, "node_modules/globals": { - "version": "13.21.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", - "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -2450,9 +2612,9 @@ } }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "engines": { "node": ">= 4" @@ -2650,18 +2812,6 @@ "get-func-name": "^2.0.1" } }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/luxon": { "version": "3.4.4", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", @@ -2697,12 +2847,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -2741,15 +2891,18 @@ } }, "node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/mlly": { @@ -2779,9 +2932,9 @@ "devOptional": true }, "node_modules/muggle-string": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.1.0.tgz", - "integrity": "sha512-Tr1knR3d2mKvvWthlk7202rywKbiOm4rVFLsfAaSIhJ6dt9o47W4S+JMtWhd/PW9Wrdew2/S2fSvhz3E2gkfEg==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", "dev": true }, "node_modules/nanoid": { @@ -2934,6 +3087,12 @@ "node": ">=6" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3091,9 +3250,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.0.13", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", - "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, "dependencies": { "cssesc": "^3.0.0", @@ -3113,9 +3272,9 @@ } }, "node_modules/prettier": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", - "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -3127,6 +3286,18 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -3159,9 +3330,9 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "engines": { "node": ">=6" @@ -3293,6 +3464,18 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/set-function-length": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", @@ -3369,9 +3552,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "engines": { "node": ">=0.10.0" } @@ -3531,6 +3714,22 @@ "node": ">= 0.8.0" } }, + "node_modules/synckit": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -3561,6 +3760,14 @@ "node": ">=14.0.0" } }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3574,17 +3781,23 @@ } }, "node_modules/ts-api-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.2.tgz", - "integrity": "sha512-Cbu4nIqnEdd+THNEsBdkolnOXhg0I8XteoHaEKgvsxpsbWda4IsUut2c187HxywQCvveojow0Dgw/amxtSKVkQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "dev": true, "engines": { - "node": ">=16.13.0" + "node": ">=16" }, "peerDependencies": { "typescript": ">=4.2.0" } }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3619,16 +3832,16 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/ufo": { @@ -3740,12 +3953,12 @@ } }, "node_modules/vite-plugin-vuetify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/vite-plugin-vuetify/-/vite-plugin-vuetify-2.0.1.tgz", - "integrity": "sha512-GlRVAruohE8b0FqmeYYh1cYg3n8THGOv066uMA44qLv9uhUxSLw55CS7fi2yU0wH363TJ2vq36zUsPTjRFrjGQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/vite-plugin-vuetify/-/vite-plugin-vuetify-2.0.4.tgz", + "integrity": "sha512-A4cliYUoP/u4AWSRVRvAPKgpgR987Pss7LpFa7s1GvOe8WjgDq92Rt3eVXrvgxGCWvZsPKziVqfHHdCMqeDhfw==", "devOptional": true, "dependencies": { - "@vuetify/loader-shared": "^2.0.1", + "@vuetify/loader-shared": "^2.0.3", "debug": "^4.3.3", "upath": "^2.0.1" }, @@ -3824,6 +4037,12 @@ } } }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "dev": true + }, "node_modules/vue": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.11.tgz", @@ -3853,9 +4072,9 @@ } }, "node_modules/vue-eslint-parser": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.1.tgz", - "integrity": "sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==", + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", "dev": true, "dependencies": { "debug": "^4.3.4", @@ -3876,21 +4095,6 @@ "eslint": ">=6.0.0" } }, - "node_modules/vue-eslint-parser/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/vue-router": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.5.tgz", @@ -3905,30 +4109,21 @@ "vue": "^3.2.0" } }, - "node_modules/vue-template-compiler": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz", - "integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==", - "dev": true, - "dependencies": { - "de-indent": "^1.0.2", - "he": "^1.2.0" - } - }, "node_modules/vue-tsc": { - "version": "1.0.24", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.0.24.tgz", - "integrity": "sha512-mmU1s5SAqE1nByQAiQnao9oU4vX+mSdsgI8H57SfKH6UVzq/jP9+Dbi2GaV+0b4Cn361d2ln8m6xeU60ApiEXg==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.1.6.tgz", + "integrity": "sha512-f98dyZp5FOukcYmbFpuSCJ4Z0vHSOSmxGttZJCsFeX0M4w/Rsq0s4uKXjcSRsZqsRgQa6z7SfuO+y0HVICE57Q==", "dev": true, "dependencies": { - "@volar/vue-language-core": "1.0.24", - "@volar/vue-typescript": "1.0.24" + "@volar/typescript": "~2.4.1", + "@vue/language-core": "2.1.6", + "semver": "^7.5.4" }, "bin": { "vue-tsc": "bin/vue-tsc.js" }, "peerDependencies": { - "typescript": "*" + "typescript": ">=5.0.0" } }, "node_modules/vue3-apexcharts": { @@ -4019,12 +4214,6 @@ "node": ">=12" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -4059,10 +4248,33 @@ "vue": "^3.2.41" } }, + "@babel/helper-string-parser": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==" + }, + "@babel/helper-validator-identifier": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==" + }, "@babel/parser": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz", - "integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==" + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.7.tgz", + "integrity": "sha512-aZn7ETtQsjjGG5HruveUK06cU3Hljuhd9Iojm4M8WWv3wLE6OkE5PWbDUkItmMgegmccaITudyuW5RPYrYlgWw==", + "requires": { + "@babel/types": "^7.25.7" + } + }, + "@babel/types": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.7.tgz", + "integrity": "sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ==", + "requires": { + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "to-fast-properties": "^2.0.0" + } }, "@esbuild/aix-ppc64": { "version": "0.19.11", @@ -4235,15 +4447,15 @@ } }, "@eslint-community/regexpp": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.0.tgz", - "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==", + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", "dev": true }, "@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -4279,19 +4491,19 @@ } }, "@eslint/js": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.48.0.tgz", - "integrity": "sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true }, "@humanwhocodes/config-array": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", - "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", "dev": true, "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", "minimatch": "^3.0.5" }, "dependencies": { @@ -4323,9 +4535,9 @@ "dev": true }, "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, "@jest/schemas": { @@ -4373,6 +4585,12 @@ "fastq": "^1.6.0" } }, + "@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true + }, "@rollup/rollup-android-arm-eabi": { "version": "4.9.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.2.tgz", @@ -4471,9 +4689,9 @@ "dev": true }, "@types/json-schema": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", - "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "@types/lodash": { @@ -4495,148 +4713,122 @@ "dev": true }, "@types/semver": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.1.tgz", - "integrity": "sha512-cJRQXpObxfNKkFAZbJl2yjWtJCqELQIdShsogr1d2MilP8dKD9TE/nEKHkJgUNHdGKCQaf9HbIynuV2csLGVLg==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.6.0.tgz", - "integrity": "sha512-CW9YDGTQnNYMIo5lMeuiIG08p4E0cXrXTbcZ2saT/ETE7dWUrNxlijsQeU04qAAKkILiLzdQz+cGFxCJjaZUmA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.2.0.tgz", + "integrity": "sha512-mdekAHOqS9UjlmyF/LSs6AIEvfceV749GFxoBAjwAv0nkevfKHWQFDMcBZWUiIC5ft6ePWivXoS36aKQ0Cy3sw==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.6.0", - "@typescript-eslint/type-utils": "6.6.0", - "@typescript-eslint/utils": "6.6.0", - "@typescript-eslint/visitor-keys": "6.6.0", + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/type-utils": "7.2.0", + "@typescript-eslint/utils": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", "natural-compare": "^1.4.0", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" - }, - "dependencies": { - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "@typescript-eslint/parser": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.6.0.tgz", - "integrity": "sha512-setq5aJgUwtzGrhW177/i+DMLqBaJbdwGj2CPIVFFLE0NCliy5ujIdLHd2D1ysmlmsjdL2GWW+hR85neEfc12w==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz", + "integrity": "sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "6.6.0", - "@typescript-eslint/types": "6.6.0", - "@typescript-eslint/typescript-estree": "6.6.0", - "@typescript-eslint/visitor-keys": "6.6.0", + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/typescript-estree": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.6.0.tgz", - "integrity": "sha512-pT08u5W/GT4KjPUmEtc2kSYvrH8x89cVzkA0Sy2aaOUIw6YxOIjA8ilwLr/1fLjOedX1QAuBpG9XggWqIIfERw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz", + "integrity": "sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==", "dev": true, "requires": { - "@typescript-eslint/types": "6.6.0", - "@typescript-eslint/visitor-keys": "6.6.0" + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0" } }, "@typescript-eslint/type-utils": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.6.0.tgz", - "integrity": "sha512-8m16fwAcEnQc69IpeDyokNO+D5spo0w1jepWWY2Q6y5ZKNuj5EhVQXjtVAeDDqvW6Yg7dhclbsz6rTtOvcwpHg==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.2.0.tgz", + "integrity": "sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "6.6.0", - "@typescript-eslint/utils": "6.6.0", + "@typescript-eslint/typescript-estree": "7.2.0", + "@typescript-eslint/utils": "7.2.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" } }, "@typescript-eslint/types": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.6.0.tgz", - "integrity": "sha512-CB6QpJQ6BAHlJXdwUmiaXDBmTqIE2bzGTDLADgvqtHWuhfNP3rAOK7kAgRMAET5rDRr9Utt+qAzRBdu3AhR3sg==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz", + "integrity": "sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.6.0.tgz", - "integrity": "sha512-hMcTQ6Al8MP2E6JKBAaSxSVw5bDhdmbCEhGW/V8QXkb9oNsFkA4SBuOMYVPxD3jbtQ4R/vSODBsr76R6fP3tbA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz", + "integrity": "sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==", "dev": true, "requires": { - "@typescript-eslint/types": "6.6.0", - "@typescript-eslint/visitor-keys": "6.6.0", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", + "minimatch": "9.0.3", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" - }, - "dependencies": { - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "@typescript-eslint/utils": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.6.0.tgz", - "integrity": "sha512-mPHFoNa2bPIWWglWYdR0QfY9GN0CfvvXX1Sv6DlSTive3jlMTUy+an67//Gysc+0Me9pjitrq0LJp0nGtLgftw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.2.0.tgz", + "integrity": "sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.6.0", - "@typescript-eslint/types": "6.6.0", - "@typescript-eslint/typescript-estree": "6.6.0", + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/typescript-estree": "7.2.0", "semver": "^7.5.4" - }, - "dependencies": { - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "@typescript-eslint/visitor-keys": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.6.0.tgz", - "integrity": "sha512-L61uJT26cMOfFQ+lMZKoJNbAEckLe539VhTxiGHrWl5XSKQgA0RTBZJW2HFPy5T0ZvPVSD93QsrTKDkfNwJGyQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz", + "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==", "dev": true, "requires": { - "@typescript-eslint/types": "6.6.0", + "@typescript-eslint/types": "7.2.0", "eslint-visitor-keys": "^3.4.1" } }, + "@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "@vitejs/plugin-vue": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.2.tgz", - "integrity": "sha512-kEjJHrLb5ePBvjD0SPZwJlw1QTRcjjCA9sB5VyfonoXVBxTS7TMnqL6EkLt1Eu61RDeiuZ/WN9Hf6PxXhPI2uA==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.1.4.tgz", + "integrity": "sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==", "dev": true, "requires": {} }, @@ -4694,57 +4886,29 @@ } }, "@volar/language-core": { - "version": "1.0.24", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.0.24.tgz", - "integrity": "sha512-vTN+alJiWwK0Pax6POqrmevbtFW2dXhjwWiW/MW4f48eDYPLdyURWcr8TixO7EN/nHsUBj2udT7igFKPtjyAKg==", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.5.tgz", + "integrity": "sha512-F4tA0DCO5Q1F5mScHmca0umsi2ufKULAnMOVBfMsZdT4myhVl4WdKRwCaKcfOkIEuyrAVvtq1ESBdZ+rSyLVww==", "dev": true, "requires": { - "@volar/source-map": "1.0.24", - "muggle-string": "^0.1.0" + "@volar/source-map": "2.4.5" } }, "@volar/source-map": { - "version": "1.0.24", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.0.24.tgz", - "integrity": "sha512-Qsv/tkplx18pgBr8lKAbM1vcDqgkGKQzbChg6NW+v0CZc3G7FLmK+WrqEPzKlN7Cwdc6XVL559Nod8WKAfKr4A==", - "dev": true, - "requires": { - "muggle-string": "^0.1.0" - } + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.5.tgz", + "integrity": "sha512-varwD7RaKE2J/Z+Zu6j3mNNJbNT394qIxXwdvz/4ao/vxOfyClZpSDtLKkwWmecinkOVos5+PWkWraelfMLfpw==", + "dev": true }, "@volar/typescript": { - "version": "1.0.24", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.0.24.tgz", - "integrity": "sha512-f8hCSk+PfKR1/RQHxZ79V1NpDImHoivqoizK+mstphm25tn/YJ/JnKNjZHB+o21fuW0yKlI26NV3jkVb2Cc/7A==", - "dev": true, - "requires": { - "@volar/language-core": "1.0.24" - } - }, - "@volar/vue-language-core": { - "version": "1.0.24", - "resolved": "https://registry.npmjs.org/@volar/vue-language-core/-/vue-language-core-1.0.24.tgz", - "integrity": "sha512-2NTJzSgrwKu6uYwPqLiTMuAzi7fAY3yFy5PJ255bGJc82If0Xr+cW8pC80vpjG0D/aVLmlwAdO4+Ya2BI8GdDg==", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.5.tgz", + "integrity": "sha512-mcT1mHvLljAEtHviVcBuOyAwwMKz1ibXTi5uYtP/pf4XxoAzpdkQ+Br2IC0NPCvLCbjPZmbf3I0udndkfB1CDg==", "dev": true, "requires": { - "@volar/language-core": "1.0.24", - "@volar/source-map": "1.0.24", - "@vue/compiler-dom": "^3.2.45", - "@vue/compiler-sfc": "^3.2.45", - "@vue/reactivity": "^3.2.45", - "@vue/shared": "^3.2.45", - "minimatch": "^5.1.1", - "vue-template-compiler": "^2.7.14" - } - }, - "@volar/vue-typescript": { - "version": "1.0.24", - "resolved": "https://registry.npmjs.org/@volar/vue-typescript/-/vue-typescript-1.0.24.tgz", - "integrity": "sha512-9a25oHDvGaNC0okRS47uqJI6FxY4hUQZUsxeOUFHcqVxZEv8s17LPuP/pMMXyz7jPygrZubB/qXqHY5jEu/akA==", - "dev": true, - "requires": { - "@volar/typescript": "1.0.24", - "@volar/vue-language-core": "1.0.24" + "@volar/language-core": "2.4.5", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" } }, "@vue/compiler-core": { @@ -4793,11 +4957,68 @@ "@vue/shared": "3.3.11" } }, + "@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "requires": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, "@vue/devtools-api": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.0.tgz", "integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==" }, + "@vue/language-core": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.1.6.tgz", + "integrity": "sha512-MW569cSky9R/ooKMh6xa2g1D0AtRKbL56k83dzus/bx//RDJk24RHWkMzbAlXjMdDNyxAaagKPRquBIxkxlCkg==", + "dev": true, + "requires": { + "@volar/language-core": "~2.4.1", + "@vue/compiler-dom": "^3.4.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.4.0", + "computeds": "^0.0.1", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "dependencies": { + "@vue/compiler-core": { + "version": "3.5.11", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.11.tgz", + "integrity": "sha512-PwAdxs7/9Hc3ieBO12tXzmTD+Ln4qhT/56S+8DvrrZ4kLDn4Z/AMUr8tXJD0axiJBS0RKIoNaR0yMuQB9v9Udg==", + "dev": true, + "requires": { + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.11", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "@vue/compiler-dom": { + "version": "3.5.11", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.11.tgz", + "integrity": "sha512-pyGf8zdbDDRkBrEzf8p7BQlMKNNF5Fk/Cf/fQ6PiUz9at4OaUfyXW0dGJTo2Vl1f5U9jSLCNf0EZJEogLXoeew==", + "dev": true, + "requires": { + "@vue/compiler-core": "3.5.11", + "@vue/shared": "3.5.11" + } + }, + "@vue/shared": { + "version": "3.5.11", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.11.tgz", + "integrity": "sha512-W8GgysJVnFo81FthhzurdRAWP/byq3q2qIw70e0JWblzVhjgOMiC2GyovXrZTFQJnFVryYaKGP3Tc9vYzYm6PQ==", + "dev": true + } + } + }, "@vue/reactivity": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.11.tgz", @@ -4852,14 +5073,47 @@ "integrity": "sha512-u2G8ZQ9IhMWTMXaWqZycnK4UthG1fA238CD+DP4Dm4WJi5hdUKKLg0RMRaRpDPNMdkTwIDkp7WtD0Rd9BH9fLw==" }, "@vuetify/loader-shared": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@vuetify/loader-shared/-/loader-shared-2.0.1.tgz", - "integrity": "sha512-zy5/ohEO7RcJaWYu2Xiy8TBEOkTb42XvWvSAJwXAtY8OlwqyGhzzBp9OvMVjLGIuFXumBpXKlsaVIkeN0OWWSw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@vuetify/loader-shared/-/loader-shared-2.0.3.tgz", + "integrity": "sha512-Ss3GC7eJYkp2SF6xVzsT7FAruEmdihmn4OCk2+UocREerlXKWgOKKzTN5PN3ZVN5q05jHHrsNhTuWbhN61Bpdg==", "devOptional": true, "requires": { "upath": "^2.0.1" } }, + "@vueuse/router": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/router/-/router-11.1.0.tgz", + "integrity": "sha512-OjTNIOzX5jD1HDzmDApMke2QsCZ+gWKaydfndKJ3j9ttn41Pr11cQbSY6ZBp+bNjctAR+jhQBV/DGtL3iKpuHg==", + "requires": { + "@vueuse/shared": "11.1.0", + "vue-demi": ">=0.14.10" + }, + "dependencies": { + "vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "requires": {} + } + } + }, + "@vueuse/shared": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.1.0.tgz", + "integrity": "sha512-YUtIpY122q7osj+zsNMFAfMTubGz0sn5QzE5gPzAIiCmtt2ha3uQUY1+JPyL4gRCTsLPX82Y9brNbo/aqlA91w==", + "requires": { + "vue-demi": ">=0.14.10" + }, + "dependencies": { + "vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "requires": {} + } + } + }, "acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -4974,12 +5228,12 @@ } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "cac": { @@ -5061,6 +5315,12 @@ "delayed-stream": "~1.0.0" } }, + "computeds": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz", + "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", + "dev": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5158,6 +5418,12 @@ "esutils": "^2.0.2" } }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + }, "esbuild": { "version": "0.19.11", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz", @@ -5196,18 +5462,19 @@ "dev": true }, "eslint": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.48.0.tgz", - "integrity": "sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.48.0", - "@humanwhocodes/config-array": "^0.11.10", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -5299,36 +5566,36 @@ } }, "eslint-config-prettier": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", - "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, "requires": {} }, + "eslint-plugin-prettier": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" + } + }, "eslint-plugin-vue": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.17.0.tgz", - "integrity": "sha512-r7Bp79pxQk9I5XDP0k2dpUC7Ots3OSWgvGZNu3BxmKK6Zg7NgVtcOB6OCna5Kb9oQwJPl5hq183WD0SY5tZtIQ==", + "version": "9.28.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.28.0.tgz", + "integrity": "sha512-ShrihdjIhOTxs+MfWun6oJWuk+g/LAhN+CiuOl/jjkG3l0F2AuK5NMTaWqyvBgkFtpYmyks6P4603mLmhNJW8g==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.4.0", + "globals": "^13.24.0", "natural-compare": "^1.4.0", "nth-check": "^2.1.1", - "postcss-selector-parser": "^6.0.13", - "semver": "^7.5.4", - "vue-eslint-parser": "^9.3.1", + "postcss-selector-parser": "^6.0.15", + "semver": "^7.6.3", + "vue-eslint-parser": "^9.4.3", "xml-name-validator": "^4.0.0" - }, - "dependencies": { - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "eslint-scope": { @@ -5416,10 +5683,16 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, "fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", @@ -5471,9 +5744,9 @@ } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { "to-regex-range": "^5.0.1" @@ -5597,9 +5870,9 @@ } }, "globals": { - "version": "13.21.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", - "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -5678,9 +5951,9 @@ "dev": true }, "ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true }, "import-fresh": { @@ -5836,15 +6109,6 @@ "get-func-name": "^2.0.1" } }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, "luxon": { "version": "3.4.4", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", @@ -5871,12 +6135,12 @@ "dev": true }, "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "requires": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" } }, @@ -5900,9 +6164,9 @@ "dev": true }, "minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, "requires": { "brace-expansion": "^2.0.1" @@ -5932,9 +6196,9 @@ "devOptional": true }, "muggle-string": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.1.0.tgz", - "integrity": "sha512-Tr1knR3d2mKvvWthlk7202rywKbiOm4rVFLsfAaSIhJ6dt9o47W4S+JMtWhd/PW9Wrdew2/S2fSvhz3E2gkfEg==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", "dev": true }, "nanoid": { @@ -6037,6 +6301,12 @@ "callsites": "^3.0.0" } }, + "path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -6123,9 +6393,9 @@ } }, "postcss-selector-parser": { - "version": "6.0.13", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", - "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, "requires": { "cssesc": "^3.0.0", @@ -6139,11 +6409,20 @@ "dev": true }, "prettier": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", - "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, "pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -6169,9 +6448,9 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true }, "qs": { @@ -6246,6 +6525,12 @@ "queue-microtask": "^1.2.2" } }, + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true + }, "set-function-length": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", @@ -6301,9 +6586,9 @@ "dev": true }, "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" }, "stackback": { "version": "0.0.2", @@ -6420,6 +6705,16 @@ "svg.js": "^2.6.5" } }, + "synckit": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", + "dev": true, + "requires": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + } + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -6444,6 +6739,11 @@ "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", "dev": true }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6454,12 +6754,18 @@ } }, "ts-api-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.2.tgz", - "integrity": "sha512-Cbu4nIqnEdd+THNEsBdkolnOXhg0I8XteoHaEKgvsxpsbWda4IsUut2c187HxywQCvveojow0Dgw/amxtSKVkQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "dev": true, "requires": {} }, + "tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6482,9 +6788,9 @@ "dev": true }, "typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", "devOptional": true }, "ufo": { @@ -6540,12 +6846,12 @@ } }, "vite-plugin-vuetify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/vite-plugin-vuetify/-/vite-plugin-vuetify-2.0.1.tgz", - "integrity": "sha512-GlRVAruohE8b0FqmeYYh1cYg3n8THGOv066uMA44qLv9uhUxSLw55CS7fi2yU0wH363TJ2vq36zUsPTjRFrjGQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/vite-plugin-vuetify/-/vite-plugin-vuetify-2.0.4.tgz", + "integrity": "sha512-A4cliYUoP/u4AWSRVRvAPKgpgR987Pss7LpFa7s1GvOe8WjgDq92Rt3eVXrvgxGCWvZsPKziVqfHHdCMqeDhfw==", "devOptional": true, "requires": { - "@vuetify/loader-shared": "^2.0.1", + "@vuetify/loader-shared": "^2.0.3", "debug": "^4.3.3", "upath": "^2.0.1" } @@ -6579,6 +6885,12 @@ "why-is-node-running": "^2.2.2" } }, + "vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "dev": true + }, "vue": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.11.tgz", @@ -6598,9 +6910,9 @@ "requires": {} }, "vue-eslint-parser": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.1.tgz", - "integrity": "sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==", + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", "dev": true, "requires": { "debug": "^4.3.4", @@ -6610,17 +6922,6 @@ "esquery": "^1.4.0", "lodash": "^4.17.21", "semver": "^7.3.6" - }, - "dependencies": { - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "vue-router": { @@ -6631,24 +6932,15 @@ "@vue/devtools-api": "^6.5.0" } }, - "vue-template-compiler": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz", - "integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==", - "dev": true, - "requires": { - "de-indent": "^1.0.2", - "he": "^1.2.0" - } - }, "vue-tsc": { - "version": "1.0.24", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.0.24.tgz", - "integrity": "sha512-mmU1s5SAqE1nByQAiQnao9oU4vX+mSdsgI8H57SfKH6UVzq/jP9+Dbi2GaV+0b4Cn361d2ln8m6xeU60ApiEXg==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.1.6.tgz", + "integrity": "sha512-f98dyZp5FOukcYmbFpuSCJ4Z0vHSOSmxGttZJCsFeX0M4w/Rsq0s4uKXjcSRsZqsRgQa6z7SfuO+y0HVICE57Q==", "dev": true, "requires": { - "@volar/vue-language-core": "1.0.24", - "@volar/vue-typescript": "1.0.24" + "@volar/typescript": "~2.4.1", + "@vue/language-core": "2.1.6", + "semver": "^7.5.4" } }, "vue3-apexcharts": { @@ -6694,12 +6986,6 @@ "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", "dev": true }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/web/package.json b/web/package.json index 738aa9fd..0bf48872 100644 --- a/web/package.json +++ b/web/package.json @@ -13,6 +13,7 @@ "dependencies": { "@auth0/auth0-vue": "^2.0.0", "@mdi/font": "^7.1.96", + "@vueuse/router": "^11.1.0", "apexcharts": "^3.37.1", "axios": "^1.3.3", "lodash": "^4.17.21", @@ -30,17 +31,18 @@ "@types/lodash": "^4.14.191", "@types/luxon": "^3.3.7", "@types/qs": "^6.9.10", - "@typescript-eslint/eslint-plugin": "^6.6.0", - "@typescript-eslint/parser": "^6.6.0", - "@vitejs/plugin-vue": "^5.0.2", - "eslint": "^8.48.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-vue": "^9.17.0", - "prettier": "^3.0.3", - "typescript": "^4.9.3", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "@vitejs/plugin-vue": "^5.1.4", + "eslint": "^8.57.1", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-vue": "^9.28.0", + "prettier": "^3.3.3", + "typescript": "^5.6.2", "vite": "^5.0.10", - "vite-plugin-vuetify": "^2.0.1", + "vite-plugin-vuetify": "^2.0.4", "vitest": "^1.1.1", - "vue-tsc": "^1.0.11" + "vue-tsc": "^2.1.6" } } diff --git a/web/src/api/fiscal-periods-api.ts b/web/src/api/fiscal-periods-api.ts index a2b16d55..809dac7c 100644 --- a/web/src/api/fiscal-periods-api.ts +++ b/web/src/api/fiscal-periods-api.ts @@ -1,53 +1,56 @@ -import { DateTime } from "luxon" - import http from "@/api/http-client" -import DateTimeUtils from "@/utils/date-time-utils" +/** Keep in sync with api/src/models/fiscal-period.ts */ +export enum FiscalPeriodMonths { + APRIL = "april", + MAY = "may", + JUNE = "june", + JULY = "july", + AUGUST = "august", + SEPTEMBER = "september", + OCTOBER = "october", + NOVEMBER = "november", + DECEMBER = "december", + JANUARY = "january", + FEBRUARY = "february", + MARCH = "march", +} export type FiscalPeriod = { id: number fiscalYear: string - month: string - dateStart: DateTime - dateEnd: DateTime - createdAt: DateTime - updatedAt: DateTime + month: FiscalPeriodMonths + dateStart: string + dateEnd: string + createdAt: string + updatedAt: string +} + +export type FiscalPeriodWhereOptions = { + fiscalYear?: string + month?: FiscalPeriodMonths + dateStart?: string + dateEnd?: string } -export type Params = { - where?: { - fiscalYear?: string - month?: string - dateStart?: Date - dateEnd?: Date - } - page?: number - perPage?: number - otherParams?: any +export type FiscalPeriodFiltersOptions = { + // add model scope signatures as needed } export const fiscalPeriodsApi = { - list(params: Params = {}): Promise<{ + async list( + params: { + where?: FiscalPeriodWhereOptions + filters?: FiscalPeriodFiltersOptions + page?: number + perPage?: number + } = {} + ): Promise<{ fiscalPeriods: FiscalPeriod[] + totalCount: number }> { - return http.get("/api/fiscal-periods", { params }).then(({ data }) => { - const { fiscalPeriods } = data - - const fiscalPeriodsWithDates = fiscalPeriods.map((fiscalPeriod: any) => { - const { dateStart, dateEnd, createdAt, updatedAt } = fiscalPeriod - return { - ...fiscalPeriod, - dateStart: DateTimeUtils.fromISO(dateStart).toUTC(), - dateEnd: DateTimeUtils.fromISO(dateEnd).toUTC(), - createdAt: DateTimeUtils.fromISO(createdAt), - updatedAt: DateTimeUtils.fromISO(updatedAt), - } - }) - - return { - fiscalPeriods: fiscalPeriodsWithDates, - } - }) + const { data } = await http.get("/api/fiscal-periods", { params }) + return data }, } diff --git a/web/src/api/funding-submission-line-jsons-api.ts b/web/src/api/funding-submission-line-jsons-api.ts index 8beb5bb1..269196fe 100644 --- a/web/src/api/funding-submission-line-jsons-api.ts +++ b/web/src/api/funding-submission-line-jsons-api.ts @@ -16,52 +16,75 @@ export type FundingSubmissionLineJson = { centreId: number fiscalYear: string dateName: string - dateStart: Date - dateEnd: Date + dateStart: string + dateEnd: string + values: string + createdAt: string + updatedAt: string + + // Virtual Attributes lines: FundingLineValue[] - createdAt: Date - updatedAt: Date } -export type Params = { - where?: { - centreId?: number - fiscalYear?: string - dateName?: string - } - page?: number - perPage?: number - otherParams?: any +export type FundingSubmissionLineJsonAsIndex = Omit +export type FundingSubmissionLineJsonAsDetailed = Omit + +export type FundingSubmissionLineJsonWhereOptions = { + centreId?: number + fiscalYear?: string + dateName?: string + dateStart?: string + dateEnd?: string +} + +export type FundingSubmissionLineJsonFiltersOptions = { + // add model scope signatures as needed } export const fundingSubmissionLineJsonsApi = { - list(params: Params = {}): Promise<{ - fundingSubmissionLineJsons: FundingSubmissionLineJson[] + async list( + params: { + where?: FundingSubmissionLineJsonWhereOptions + filters?: FundingSubmissionLineJsonFiltersOptions + page?: number + perPage?: number + } = {} + ): Promise<{ + fundingSubmissionLineJsons: FundingSubmissionLineJsonAsIndex[] + totalCount: number }> { - return http.get("/api/funding-submission-line-jsons", { params }).then(({ data }) => data) + const { data } = await http.get("/api/funding-submission-line-jsons", { params }) + return data }, - get(fundingSubmissionLineJsonId: number) { - return http - .get(`/api/funding-submission-line-jsons/${fundingSubmissionLineJsonId}`) - .then(({ data }) => data) + async get(fundingSubmissionLineJsonId: number): Promise<{ + fundingSubmissionLineJson: FundingSubmissionLineJsonAsDetailed + }> { + const { data } = await http.get( + `/api/funding-submission-line-jsons/${fundingSubmissionLineJsonId}` + ) + return data }, - create( + async create( attributes: Partial - ): Promise<{ fundingSubmissionLineJson: FundingSubmissionLineJson }> { - return http.post("/api/funding-submission-line-jsons", attributes).then(({ data }) => data) + ): Promise<{ fundingSubmissionLineJson: FundingSubmissionLineJsonAsDetailed }> { + const { data } = await http.post("/api/funding-submission-line-jsons", attributes) + return data }, - update( + async update( fundingSubmissionLineJsonId: number, - attributes: any - ): Promise<{ fundingSubmissionLineJson: FundingSubmissionLineJson }> { - return http - .patch(`/api/funding-submission-line-jsons/${fundingSubmissionLineJsonId}`, attributes) - .then(({ data }) => data) + attributes: Partial + ): Promise<{ fundingSubmissionLineJson: FundingSubmissionLineJsonAsDetailed }> { + const { data } = await http.patch( + `/api/funding-submission-line-jsons/${fundingSubmissionLineJsonId}`, + attributes + ) + return data }, - delete(fundingSubmissionLineJsonId: number): Promise { - return http - .delete(`/api/funding-submission-line-jsons/${fundingSubmissionLineJsonId}`) - .then(({ data }) => data) + async delete(fundingSubmissionLineJsonId: number): Promise { + const { data } = await http.delete( + `/api/funding-submission-line-jsons/${fundingSubmissionLineJsonId}` + ) + return data }, // Nested endpoints diff --git a/web/src/components/FiscalYearSelect.vue b/web/src/components/FiscalYearSelect.vue index daf65409..b5a3d25e 100644 --- a/web/src/components/FiscalYearSelect.vue +++ b/web/src/components/FiscalYearSelect.vue @@ -9,10 +9,10 @@ diff --git a/web/src/components/common/KeyboardShortcutsModal.vue b/web/src/components/common/KeyboardShortcutsModal.vue new file mode 100644 index 00000000..6648586d --- /dev/null +++ b/web/src/components/common/KeyboardShortcutsModal.vue @@ -0,0 +1,178 @@ + + + diff --git a/web/src/components/funding-submission-line-jsons/FundingSubmissionLineJsonEditSheet.vue b/web/src/components/funding-submission-line-jsons/FundingSubmissionLineJsonEditSheet.vue new file mode 100644 index 00000000..c490e2c4 --- /dev/null +++ b/web/src/components/funding-submission-line-jsons/FundingSubmissionLineJsonEditSheet.vue @@ -0,0 +1,208 @@ + + + + + diff --git a/web/src/components/funding-submission-line-jsons/FundingSubmissionLineJsonSectionTable.vue b/web/src/components/funding-submission-line-jsons/FundingSubmissionLineJsonSectionTable.vue new file mode 100644 index 00000000..bf09e3fb --- /dev/null +++ b/web/src/components/funding-submission-line-jsons/FundingSubmissionLineJsonSectionTable.vue @@ -0,0 +1,272 @@ + + + + + diff --git a/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue b/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue index f04b7545..6298ac38 100644 --- a/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue +++ b/web/src/modules/centre/components/EditEmployeeBenefitWidget.vue @@ -150,7 +150,7 @@ Total Cost - + diff --git a/web/src/modules/centre/pages/CentreDashboardWorksheetsMonthlyWorksheetPage.vue b/web/src/modules/centre/pages/CentreDashboardWorksheetsMonthlyWorksheetPage.vue deleted file mode 100644 index e1733e72..00000000 --- a/web/src/modules/centre/pages/CentreDashboardWorksheetsMonthlyWorksheetPage.vue +++ /dev/null @@ -1,196 +0,0 @@ - - - - - diff --git a/web/src/modules/centre/pages/CentreDashboardWorksheetsPage.vue b/web/src/modules/centre/pages/CentreDashboardWorksheetsPage.vue index f9f1682a..74406e26 100644 --- a/web/src/modules/centre/pages/CentreDashboardWorksheetsPage.vue +++ b/web/src/modules/centre/pages/CentreDashboardWorksheetsPage.vue @@ -1,23 +1,10 @@ + + diff --git a/web/src/store/fiscal-periods.ts b/web/src/store/fiscal-periods.ts deleted file mode 100644 index 897593c8..00000000 --- a/web/src/store/fiscal-periods.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { defineStore } from "pinia" -import { reactive, toRefs, watch } from "vue" - -import fiscalPeriodsApi, { type FiscalPeriod, type Params } from "@/api/fiscal-periods-api" - -export { type FiscalPeriod, type Params } - -export const useFiscalPeriodsStore = defineStore("fiscal-periods", () => { - const state = reactive<{ - fiscalPeriods: FiscalPeriod[] - isLoading: boolean - isErrored: boolean - isInitialized: boolean - }>({ - fiscalPeriods: [], - isLoading: false, - isErrored: false, - isInitialized: false, - }) - - async function fetch(params: Params = {}): Promise { - state.isLoading = true - try { - const { fiscalPeriods } = await fiscalPeriodsApi.list(params) - state.isErrored = false - state.fiscalPeriods = fiscalPeriods - state.isInitialized = true - return fiscalPeriods - } catch (error) { - console.error("Failed to fetch fiscal periods:", error) - state.isErrored = true - throw error - } finally { - state.isLoading = false - } - } - - return { - ...toRefs(state), - fetch, - } -}) - -export default useFiscalPeriodsStore diff --git a/web/src/store/funding-submission-line-jsons.ts b/web/src/store/funding-submission-line-jsons.ts deleted file mode 100644 index d211468f..00000000 --- a/web/src/store/funding-submission-line-jsons.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { defineStore } from "pinia" -import { isNil } from "lodash" -import { ref, type Ref } from "vue" - -import fundingSubmissionLineJsonsApi, { - type FundingLineValue, - type FundingSubmissionLineJson, - type Params, -} from "@/api/funding-submission-line-jsons-api" - -export { type FundingLineValue, type FundingSubmissionLineJson, type Params } - -export const useFundingSubmissionLineJsonsStore = defineStore("fundingSubmissionLineJsons", () => { - const items: Ref = ref([]) - // TODO: Implement total_count here and in the back-end - const isLoading = ref(false) - const isErrored = ref(false) - const isInitialized = ref(false) - - async function initialize(params: Params = {}): Promise { - if (isInitialized.value) return items.value - - return fetch(params) - } - - async function fetch(params: Params = {}): Promise { - isLoading.value = true - try { - const { fundingSubmissionLineJsons } = await fundingSubmissionLineJsonsApi.list(params) - isErrored.value = false - items.value = fundingSubmissionLineJsons - isInitialized.value = true - return fundingSubmissionLineJsons - } catch (error) { - console.error("Failed to fetch fundingSubmissionLineJsons:", error) - isErrored.value = true - throw error - } finally { - isLoading.value = false - } - } - - function linesForMonth(dateName: string): FundingLineValue[] | undefined { - const itemForMonth = items.value.find((time) => time.dateName === dateName) - - if (isNil(itemForMonth)) return - - return itemForMonth.lines - } - - return { - items, - isLoading, - isErrored, - isInitialized, - initialize, - fetch, - linesForMonth, - } -}) - -export default useFundingSubmissionLineJsonsStore diff --git a/web/src/use/use-fiscal-periods.ts b/web/src/use/use-fiscal-periods.ts new file mode 100644 index 00000000..ddb7075b --- /dev/null +++ b/web/src/use/use-fiscal-periods.ts @@ -0,0 +1,72 @@ +import { type Ref, reactive, toRefs, ref, unref, watch } from "vue" + +import fiscalPeriodsApi, { + FiscalPeriodMonths, + type FiscalPeriod, + type FiscalPeriodWhereOptions, + type FiscalPeriodFiltersOptions, +} from "@/api/fiscal-periods-api" + +export { + FiscalPeriodMonths, + type FiscalPeriod, + type FiscalPeriodWhereOptions, + type FiscalPeriodFiltersOptions, +} + +export function useFiscalPeriods( + queryOptions: Ref<{ + where?: FiscalPeriodWhereOptions + filters?: FiscalPeriodFiltersOptions + page?: number + perPage?: number + }> = ref({}), + { skipWatchIf = () => false }: { skipWatchIf?: () => boolean } = {} +) { + const state = reactive<{ + fiscalPeriods: FiscalPeriod[] + totalCount: number + isLoading: boolean + isErrored: boolean + }>({ + fiscalPeriods: [], + totalCount: 0, + isLoading: false, + isErrored: false, + }) + + async function fetch(): Promise { + state.isLoading = true + try { + const { fiscalPeriods, totalCount } = await fiscalPeriodsApi.list(unref(queryOptions)) + state.isErrored = false + state.fiscalPeriods = fiscalPeriods + state.totalCount = totalCount + return fiscalPeriods + } catch (error) { + console.error("Failed to fetch fiscal periods:", error) + state.isErrored = true + throw error + } finally { + state.isLoading = false + } + } + + watch( + () => [skipWatchIf(), unref(queryOptions)], + async ([skip]) => { + if (skip) return + + await fetch() + }, + { deep: true, immediate: true } + ) + + return { + ...toRefs(state), + fetch, + refresh: fetch, + } +} + +export default useFiscalPeriods diff --git a/web/src/use/use-funding-submission-line-json.ts b/web/src/use/use-funding-submission-line-json.ts new file mode 100644 index 00000000..d07b6847 --- /dev/null +++ b/web/src/use/use-funding-submission-line-json.ts @@ -0,0 +1,59 @@ +import { type Ref, reactive, toRefs, unref, watch } from "vue" +import { isNil } from "lodash" + +import fundingSubmissionLineJsonsApi, { + type FundingSubmissionLineJsonAsDetailed, +} from "@/api/funding-submission-line-jsons-api" + +export { type FundingSubmissionLineJsonAsDetailed } + +export function useFundingSubmissionLineJsonAsDetailed(id: Ref) { + const state = reactive<{ + fundingSubmissionLineJson: FundingSubmissionLineJsonAsDetailed | null + isLoading: boolean + isErrored: boolean + }>({ + fundingSubmissionLineJson: null, + isLoading: false, + isErrored: false, + }) + + async function fetch(): Promise { + const staticId = unref(id) + if (isNil(staticId)) { + throw new Error("id is required") + } + + state.isLoading = true + try { + const { fundingSubmissionLineJson } = await fundingSubmissionLineJsonsApi.get(staticId) + state.isErrored = false + state.fundingSubmissionLineJson = fundingSubmissionLineJson + return fundingSubmissionLineJson + } catch (error) { + console.error("Failed to fetch funding submission line json:", error) + state.isErrored = true + throw error + } finally { + state.isLoading = false + } + } + + watch( + () => unref(id), + async (newId) => { + if (isNil(newId)) return + + await fetch() + }, + { immediate: true } + ) + + return { + ...toRefs(state), + fetch, + refresh: fetch, + } +} + +export default useFundingSubmissionLineJsonAsDetailed diff --git a/web/src/use/use-funding-submission-line-jsons.ts b/web/src/use/use-funding-submission-line-jsons.ts new file mode 100644 index 00000000..6b623067 --- /dev/null +++ b/web/src/use/use-funding-submission-line-jsons.ts @@ -0,0 +1,85 @@ +import { type Ref, reactive, toRefs, ref, unref, watch } from "vue" +import { isNil } from "lodash" + +import fundingSubmissionLineJsonsApi, { + type FundingLineValue, + type FundingSubmissionLineJsonAsIndex, + type FundingSubmissionLineJsonFiltersOptions, + type FundingSubmissionLineJsonWhereOptions, +} from "@/api/funding-submission-line-jsons-api" + +export { + type FundingLineValue, + type FundingSubmissionLineJsonAsIndex, + type FundingSubmissionLineJsonWhereOptions, + type FundingSubmissionLineJsonFiltersOptions, +} + +export function useFundingSubmissionLineJsons( + queryOptions: Ref<{ + where?: FundingSubmissionLineJsonWhereOptions + filters?: FundingSubmissionLineJsonFiltersOptions + page?: number + perPage?: number + }> = ref({}), + { skipWatchIf = () => false }: { skipWatchIf?: () => boolean } = {} +) { + const state = reactive<{ + fundingSubmissionLineJsons: FundingSubmissionLineJsonAsIndex[] + totalCount: number + isLoading: boolean + isErrored: boolean + }>({ + fundingSubmissionLineJsons: [], + totalCount: 0, + isLoading: false, + isErrored: false, + }) + + function linesForMonth(dateName: string): FundingLineValue[] | undefined { + const itemForMonth = state.fundingSubmissionLineJsons.find((time) => time.dateName === dateName) + + if (isNil(itemForMonth)) return + + return itemForMonth.lines + } + + async function fetch(): Promise { + state.isLoading = true + try { + const { fundingSubmissionLineJsons, totalCount } = await fundingSubmissionLineJsonsApi.list( + unref(queryOptions) + ) + state.isErrored = false + state.fundingSubmissionLineJsons = fundingSubmissionLineJsons + state.totalCount = totalCount + return fundingSubmissionLineJsons + } catch (error) { + console.error("Failed to fetch funding submission line jsons:", error) + state.isErrored = true + throw error + } finally { + state.isLoading = false + } + } + + watch( + () => [skipWatchIf(), unref(queryOptions)], + async ([skip]) => { + if (skip) return + + await fetch() + }, + { deep: true, immediate: true } + ) + + return { + ...toRefs(state), + fetch, + refresh: fetch, + // Special methods + linesForMonth, + } +} + +export default useFundingSubmissionLineJsons diff --git a/web/tsconfig.json b/web/tsconfig.json index 95ae9033..d90d25ba 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -18,6 +18,13 @@ "@models": ["../api/src/data/models"] } }, - "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "src/**/*.js"], + "include": [ + "src/**/*.ts", + "src/**/*.d.ts", + "src/**/*.tsx", + "src/**/*.vue", + "src/**/*.js", + ".eslintrc.cjs" + ], "references": [{ "path": "./tsconfig.node.json" }] }